Skip to content

Commit

Permalink
Merge pull request #9 from edsaac/panel_backend
Browse files Browse the repository at this point in the history
Panel backend
  • Loading branch information
edsaac authored Mar 26, 2023
2 parents 967ed05 + f07c77f commit 7ee58e9
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 142 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ pip install stpyvista

![textures-stpyvista|508x500, 85%](https://aws1.discourse-cdn.com/business7/uploads/streamlit/original/3X/e/6/e64c7054ffadafee7c8ad66e5a2dfc5b0f702cbd.gif)

🍞 Physically Based Rendering (PBR)

![textures-stpyvista|508x500, 85%](https://aws1.discourse-cdn.com/business7/uploads/streamlit/original/3X/8/d/8dd4a20952a798c917180ec187edaac77a766cee.gif)

******

## Usage example:
Expand All @@ -39,7 +35,7 @@ import pyvista as pv
import streamlit as st
from stpyvista import stpyvista

# ipythreejs does not support scalar bars :(
# pythreejs does not support scalar bars :(
pv.global_theme.show_scalar_bar = False

st.title("A cube")
Expand Down
9 changes: 8 additions & 1 deletion stpyvista/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import streamlit as st
import pyvista as pv
from stpyvista import stpyvista

# ipythreejs does not support scalar bars :(
# pythreejs does not support scalar bars :(
pv.global_theme.show_scalar_bar = False

## Initialize a plotter object
Expand All @@ -42,6 +42,13 @@ stpyvista(plotter, key="pv_cube")

## Log changes

<details>
<summary>
v 0.0.6
</summary>
- Replaced pythreejs backend for panel backend. This is a temporary solution as pyvista will remove panel support in favor of trame.
</details>

<details>
<summary>
v 0.0.5
Expand Down
8 changes: 4 additions & 4 deletions stpyvista/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "stpyvista"
version = "0.0.5"
version = "0.0.6"
authors = [
{ name="Edwin S", email="[email protected]" },
]
Expand All @@ -20,9 +20,9 @@ classifiers = [
dependencies = [
"streamlit",
"pyvista",
"pythreejs",
"ipywidgets==7.7.1",
"ipython==8.4.0"
"bokeh",
"panel",
"ipython"
]

[project.urls]
Expand Down
6 changes: 3 additions & 3 deletions stpyvista/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
streamlit
pyvista
pythreejs==2.3.0
ipywidgets==7.7.1
ipython==8.4.0
ipython
bokeh
panel
6 changes: 3 additions & 3 deletions stpyvista/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setuptools.setup(
name="stpyvista",
version="0.0.5",
version="0.0.6",
author="Edwin S",
author_email="[email protected]",
description="Streamlit component that allows you to visualize pyvista 3D visualizations",
Expand All @@ -17,6 +17,6 @@
package_data={'stpyvista': ['frontend/*']},
include_package_data=True,
classifiers=[],
python_requires=">=3.7",
install_requires=["streamlit>=1.2", "jinja2"],
python_requires=">=3.8",
install_requires=["streamlit>=1.18", "jinja2", "panel"],
)
151 changes: 42 additions & 109 deletions stpyvista/src/stpyvista/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import streamlit.components.v1 as components

import pyvista as pv
pv.set_jupyter_backend('pythreejs')
pv.set_jupyter_backend('panel')

from io import StringIO
from ipywidgets.embed import embed_minimal_html
from tempfile import NamedTemporaryFile

import pythreejs as tjs
import panel as pn
pn.extension('vtk')
from bokeh.resources import INLINE

# Tell streamlit that there is a component called stpyvista,
# and that the code to display that component is in the "frontend" folder
Expand All @@ -23,93 +24,34 @@
path=str(frontend_dir)
)

def get_Meshes(renderer: tjs.Renderer) -> list[tjs.Mesh]:
return [child for child in renderer._trait_values["scene"].children if isinstance(child, tjs.Mesh)]
# def get_Meshes(renderer: tjs.Renderer) -> list[tjs.Mesh]:
# return [child for child in renderer._trait_values["scene"].children if isinstance(child, tjs.Mesh)]

def spin_element_on_axis(renderer: tjs.Renderer, axis:str = "z", revolution_time:float = 4.0):
# def spin_element_on_axis(renderer: tjs.Renderer, axis:str = "z", revolution_time:float = 4.0):

## Makes a full spin in a second
spin_track = tjs.NumberKeyframeTrack(name=f'.rotation[{axis}]', times=[0, revolution_time], values=[0, 6.28])
spin_clip = tjs.AnimationClip(tracks=[spin_track])
# ## Makes a full spin in a second
# spin_track = tjs.NumberKeyframeTrack(name=f'.rotation[{axis}]', times=[0, revolution_time], values=[0, 6.28])
# spin_clip = tjs.AnimationClip(tracks=[spin_track])

## Animate all meshes in scene
## This adds a separate control to all the meshes
## Need to implement this as a tjs.AnimationObjectGroup in the AnimationMixer,
## but that is not implemented pythreejs: https://github.com/jupyter-widgets/pythreejs/issues/372
# spin_action = [tjs.AnimationAction(tjs.AnimationMixer(mesh), spin_clip, mesh) for mesh in get_Meshes(renderer)]
# ## Animate all meshes in scene
# ## This adds a separate control to all the meshes
# ## Need to implement this as a tjs.AnimationObjectGroup in the AnimationMixer,
# ## but that is not implemented pythreejs: https://github.com/jupyter-widgets/pythreejs/issues/372
# # spin_action = [tjs.AnimationAction(tjs.AnimationMixer(mesh), spin_clip, mesh) for mesh in get_Meshes(renderer)]

# This adds controls for only the firts mesh in the plotter
mesh = get_Meshes(renderer)[0]
spin_action = [tjs.AnimationAction(tjs.AnimationMixer(mesh), spin_clip, mesh)]
return spin_action
# # This adds controls for only the firts mesh in the plotter
# mesh = get_Meshes(renderer)[0]
# spin_action = [tjs.AnimationAction(tjs.AnimationMixer(mesh), spin_clip, mesh)]
# return spin_action

class stpyvistaTypeError(TypeError):
""" Unsupported format for input? """
pass

class HTML_stpyvista:
"""
Receives a pyvista Plotter object and builds an HTML model
using pythreejs. This takes care of temporal files needed to
dump the generated HTML and store the dimensions to render the
iframe in the frontend
Usage
----------
model = HTML_stpyvista(plotter)
plotter: pv.Plotter
Plotter to export to html
rotation: dict{"axis":'z', "revolution_time":float}
transparent_background: bool
"""
def __init__(
self,
plotter:pv.Plotter,
rotation:dict = None,
opacity_background:float = 0.0) -> None:

model_html = StringIO()
pv_to_tjs = plotter.to_pythreejs()

## Animation controls
animations = []
if rotation:
animations = spin_element_on_axis(pv_to_tjs, **rotation)
else:
pass

# Build transparent background
## Remove background
pv_to_tjs._trait_values["scene"].background = None

## Support transparency
pv_to_tjs._alpha = True

## Retrieve intended color from pv.Plotter
pv_to_tjs.clearColor = plotter.background_color.hex_rgb

## Assign alpha
pv_to_tjs.clearOpacity = opacity_background

# Create HTML file
embed_minimal_html(model_html, [pv_to_tjs, *animations], title="🧊-stpyvista")
threejs_html = model_html.getvalue()
model_html.close()

dimensions = plotter.window_size
self.threejs_html = threejs_html
self.window_dimensions = dimensions

# Create the python function that will be called from the front end
def stpyvista(
input : Union[pv.Plotter, HTML_stpyvista],
plotter : pv.Plotter,
horizontal_align : str = "center",
rotation : Union[bool, dict] = None,
opacity_background : float = 0.0,
key: Optional[str] = None
) -> None:

Expand All @@ -124,15 +66,6 @@ def stpyvista(
horizontal_align: str = "center"
Either "center", "left" or "right"
rotation: dict = None
[Experimental]. Add a play button to spin the mesh along an axis.
If not False, expects a dictionary with keys {"axis": "z", "revolution_time":5}.
>> It only works for a single mesh, as it rotates the mesh rather than moving the camera
>> Also, edges are left behind - a lot to fix
opacity_background: float = 0.0
[Experimental]. Ignore background color.
key: str|None
An optional key that uniquely identifies this component. If this is
None, and the component's arguments are changed, the component will
Expand All @@ -143,27 +76,27 @@ def stpyvista(
None
"""

if isinstance(input, pv.Plotter):
input = HTML_stpyvista(
input,
rotation=rotation,
opacity_background=opacity_background)
elif isinstance(input, HTML_stpyvista): pass
else: raise(stpyvistaTypeError)

if rotation: has_controls = 1.0
else: has_controls = 0.0

component_value = _component_func(
threejs_html = input.threejs_html,
width = input.window_dimensions[0],
height = input.window_dimensions[1],
horizontal_align = horizontal_align,
has_controls = has_controls,
key = key,
default = 0)

return component_value
if isinstance(plotter, pv.Plotter):
width, height = plotter.window_size
geo_pan_pv = pn.panel(plotter.ren_win, width=width, height=height)

# Create HTML file
with NamedTemporaryFile(mode='a+', suffix='.html') as model_html:
geo_pan_pv.save(model_html.name, resources=INLINE)
panel_html = model_html.read()

component_value = _component_func(
panel_html = panel_html,
width = width,
height = height+100,
horizontal_align = horizontal_align,
key = key,
default = 0)

return component_value

else:
raise(stpyvistaTypeError)

def main():
pass
Expand Down
2 changes: 0 additions & 2 deletions stpyvista/src/stpyvista/frontend/jupyter-threejs.js

This file was deleted.

25 changes: 10 additions & 15 deletions stpyvista/src/stpyvista/frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function onRender(event) {
if (!window.rendered) {

// You most likely want to get the data passed in like this
const {threejs_html, width, height, horizontal_align, has_controls, key} = event.detail.args;
const {panel_html, width, height, horizontal_align, key} = event.detail.args;
const stpyvistadiv = document.getElementById("stpyvistadiv");
const stpyvistaframe = document.getElementById("stpyvistaframe");

Expand All @@ -28,27 +28,22 @@ function onRender(event) {

// Overwrite default iframe dimensions and put model in the iframe
// just CSS styling does not apply to the iframe
stpyvistaframe.srcdoc = threejs_html;
stpyvistaframe.srcdoc = panel_html;
stpyvistaframe.width = width + 15;
console.log("WIDTH", width)
console.log("CONTROLS", has_controls)

if (has_controls > 0) {
stpyvistaframe.height = height + 60;
Streamlit.setFrameHeight(height + 95)
} else {
stpyvistaframe.height = height + 15;
Streamlit.setFrameHeight(height + 40)
}

stpyvistaframe.scrolling = "no";
stpyvistaframe.style.border = "none";

stpyvistaframe.height = height + 15;
Streamlit.setFrameHeight(height + 40)
console.log("HEIGHT", width)
stpyvistaframe.scrolling = "no";
stpyvistaframe.style.border = "2px red";

// Send some value to python
// Not very useful at the moment but keep it for later
// stpyvistadiv.addEventListener('click', event => sendValue(50), false);

window.rendered = true;

}
}

Expand Down
29 changes: 29 additions & 0 deletions stpyvista/test/cube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import streamlit as st
import pyvista as pv
from stpyvista import stpyvista

st.set_page_config(page_icon="🧊", layout="wide")
st.title("🧊 `stpyvista`")
st.sidebar.header("Show PyVista 3D visualizations in Streamlit")

# pythreejs does not support scalar bars :(
pv.global_theme.show_scalar_bar = False

## Initialize a plotter object
plotter = pv.Plotter(window_size=[400,400])

## Create a mesh with a cube
mesh = pv.Cube(center=(0,0,0))

## Add some scalar field associated to the mesh
mesh['myscalar'] = mesh.points[:, 2]*mesh.points[:, 0]

## Add mesh to the plotter
plotter.add_mesh(mesh, scalars='myscalar', cmap='bwr', line_width=1)

## Final touches
plotter.view_isometric()
plotter.background_color = 'white'

## Pass a key to avoid re-rendering at each time something changes in the page
stpyvista(plotter, key="pv_cube")

0 comments on commit 7ee58e9

Please sign in to comment.