Skip to content

Commit cfa26db

Browse files
committed
Implement trame backend as st component
1 parent 025210a commit cfa26db

File tree

8 files changed

+189
-16
lines changed

8 files changed

+189
-16
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "stpyvista"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
authors = [
99
{ name = "Edwin Saavedra C.", email = "esaavedrac@u.northwestern.edu" },
1010
]

src/stpyvista/panel_backend.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def stpyvista(
105105
Pyvista plotter object to render.
106106
107107
use_container_width : bool = True
108-
If True, set the dataframe width to the width of the parent container. \
108+
If True, set the 3D view width to the width of the parent container. \
109109
This takes precedence over the `horizontal_align` argument. \
110110
Defaults to `True`.
111111
@@ -171,7 +171,6 @@ def stpyvista(
171171
use_container_width=1 if use_container_width else 0,
172172
bgcolor=plotter.background_color.hex_rgba,
173173
key=key,
174-
default=0,
175174
)
176175

177176
return component_value

src/stpyvista/trame_backend.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from multiprocessing import Process, Queue
22
from warnings import warn
3-
3+
from pathlib import Path
44
import streamlit.components.v1 as components
55

66
from pyvista.plotting import Plotter
@@ -10,7 +10,47 @@ def _export_html(queue: Queue, plotter: Plotter):
1010
queue.put(plotter.export_html(filename=None))
1111

1212

13-
def stpyvista(plotter: Plotter, **kwargs) -> None:
13+
def _as_html(plotter: Plotter, **kwargs) -> None:
14+
"""Plan to remove this function in the future"""
15+
if not isinstance(plotter, Plotter):
16+
raise TypeError(f"{plotter} is not a `pyvista.Plotter` instance.")
17+
18+
if "panel_kwargs" in kwargs:
19+
warn(
20+
"panel_kwargs is not supported by the trame backend.\n"
21+
"They will be ignored"
22+
)
23+
24+
if "horizontal_align" in kwargs:
25+
warn(
26+
"horizontal_align is not supported by the trame backend.\n"
27+
"It will be ignored"
28+
)
29+
30+
queue = Queue(maxsize=1)
31+
process = Process(target=_export_html, args=(queue, plotter))
32+
33+
process.start()
34+
html_plotter = queue.get().read()
35+
process.join()
36+
37+
if kwargs.get("use_container_width", True):
38+
width = None
39+
else:
40+
width = plotter.window_size[0]
41+
42+
components.html(html_plotter, height=plotter.window_size[1], width=width)
43+
44+
45+
## Using component declaration
46+
47+
frontend_dir = (Path(__file__).parent / "trame_based").absolute()
48+
_component_func = components.declare_component(
49+
"stpyvista_trame", path=str(frontend_dir)
50+
)
51+
52+
53+
def stpyvista(plotter: Plotter, use_container_width=True, key=None, **kwargs) -> None:
1454
"""
1555
Renders an interactive Pyvista Plotter in streamlit using the
1656
trame backend.
@@ -19,7 +59,17 @@ def stpyvista(plotter: Plotter, **kwargs) -> None:
1959
----------
2060
plotter: pv.Plotter
2161
Pyvista plotter object to render.
62+
use_container_width: bool = True
63+
If True, set the 3D view width to the width of the parent container. \
64+
If False, the width is taken from the plotter window size.
65+
key: Optional[str] = None
66+
An optional key that uniquely identifies this component. If this is
67+
None, and the component's arguments are changed, the component will
68+
be re-mounted in the Streamlit frontend and lose its current state.
69+
2270
"""
71+
72+
## Checks
2373
if not isinstance(plotter, Plotter):
2474
raise TypeError(f"{plotter} is not a `pyvista.Plotter` instance.")
2575

@@ -35,16 +85,22 @@ def stpyvista(plotter: Plotter, **kwargs) -> None:
3585
"It will be ignored"
3686
)
3787

88+
## Get HTML of plotter
3889
queue = Queue(maxsize=1)
3990
process = Process(target=_export_html, args=(queue, plotter))
4091

4192
process.start()
4293
html_plotter = queue.get().read()
4394
process.join()
4495

45-
if kwargs.get("use_container_width", True):
46-
width = None
47-
else:
48-
width = plotter.window_size[0]
96+
## Set dimensions
97+
width = None if use_container_width else plotter.window_size[0]
98+
height = plotter.window_size[1]
4999

50-
components.html(html_plotter, height=plotter.window_size[1], width=width)
100+
_component_func(
101+
trame_html=html_plotter,
102+
height=height,
103+
width=width,
104+
use_container_width=use_container_width,
105+
key=key,
106+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
6+
<meta name="viewport"
7+
content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
8+
<title>🧊 stpyvista 🧊</title>
9+
<script type="text/javascript" src="./streamlit-component-lib.js"></script>
10+
<script type="text/javascript" src="./main.js"></script>
11+
</head>
12+
13+
<body>
14+
<iframe id="stpyvistaframe"
15+
sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads"
16+
frameborder="0" allowfullscreen allowtransparency="true"></iframe>
17+
</body>

src/stpyvista/trame_based/main.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// The `Streamlit` object exists because our html file includes
2+
// `streamlit-component-lib.js`.
3+
// If you get an error about "Streamlit" not being defined, that
4+
// means you're missing that file.
5+
6+
function sendValue(value) {
7+
Streamlit.setComponentValue(value)
8+
}
9+
10+
/**
11+
* The component's render function. This will be called immediately after
12+
* the component is initially loaded, and then again every time the
13+
* component gets new data from Python.
14+
*/
15+
function onRender(event) {
16+
17+
// Only run the render code the first time the component is loaded.
18+
if (!window.rendered) {
19+
20+
// You most likely want to get the data passed in like this
21+
const { trame_html, height, width, use_container_width, key } = event.detail.args;
22+
23+
const stpyvistaframe = document.getElementById("stpyvistaframe");
24+
25+
function updateFrameWidth() {
26+
stpyvistaframe.width = document.body.offsetWidth;
27+
}
28+
29+
if (Boolean(use_container_width)) {
30+
updateFrameWidth();
31+
32+
window.onresize = function (event) {
33+
updateFrameWidth();
34+
}
35+
} else {
36+
stpyvistaframe.width = width;
37+
}
38+
39+
stpyvistaframe.srcdoc = trame_html;
40+
stpyvistaframe.scrolling = "yes";
41+
42+
stpyvistaframe.height = height;
43+
Streamlit.setFrameHeight(height + 5);
44+
45+
// Send some value to python
46+
// Not very useful at the moment but keep it for later
47+
// stpyvistadiv.addEventListener('click', event => sendValue(50), false);
48+
49+
window.rendered = true;
50+
51+
}
52+
}
53+
54+
// Render the component whenever python send a "render event"
55+
Streamlit.events.addEventListener(Streamlit.RENDER_EVENT, onRender)
56+
57+
// Tell Streamlit that the component is ready to receive events
58+
Streamlit.setComponentReady()
59+
60+
// Render with the correct height, if this is a fixed-height component
61+
Streamlit.setFrameHeight()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
// Borrowed minimalistic Streamlit API from Thiago
3+
// https://discuss.streamlit.io/t/code-snippet-create-components-without-any-frontend-tooling-no-react-babel-webpack-etc/13064
4+
function sendMessageToStreamlitClient(type, data) {
5+
console.log(type, data)
6+
const outData = Object.assign({
7+
isStreamlitMessage: true,
8+
type: type,
9+
}, data);
10+
window.parent.postMessage(outData, "*");
11+
}
12+
13+
const Streamlit = {
14+
setComponentReady: function() {
15+
sendMessageToStreamlitClient("streamlit:componentReady", {apiVersion: 1});
16+
},
17+
setFrameHeight: function(height) {
18+
sendMessageToStreamlitClient("streamlit:setFrameHeight", {height: height});
19+
},
20+
setComponentValue: function(value) {
21+
sendMessageToStreamlitClient("streamlit:setComponentValue", {value: value});
22+
// sendMessageToStreamlitClient("streamlit:setComponentValue", {value});
23+
},
24+
RENDER_EVENT: "streamlit:render",
25+
events: {
26+
addEventListener: function(type, callback) {
27+
window.addEventListener("message", function(event) {
28+
if (event.data.type === type) {
29+
event.detail = event.data
30+
callback(event);
31+
}
32+
});
33+
}
34+
}
35+
}

test/cube.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55

66
st.title("🧊 `stpyvista`")
77
st.sidebar.header("Show PyVista 3D visualizations in Streamlit")
8-
backend = st.sidebar.radio(
9-
"Select backend",
10-
["panel", "trame"],
11-
)
8+
backend = st.sidebar.radio("Select backend", ["panel", "trame_html", "trame"], index=2)
129
st.sidebar.divider()
1310

1411
if backend == "panel":
1512
from stpyvista.panel_backend import stpyvista
16-
else:
13+
elif backend == "trame_html":
14+
from stpyvista.trame_backend import _as_html as stpyvista
15+
elif backend == "trame":
1716
from stpyvista.trame_backend import stpyvista
1817

1918

@@ -55,3 +54,8 @@
5554
for i, col in enumerate(cols):
5655
with col:
5756
stpyvista(plotter, use_container_width=chk, key=f"cube_{i}")
57+
58+
stpyvista(plotter, use_container_width=chk)
59+
print(hash(plotter))
60+
61+
st.sidebar.button("Rerun")

test/dataview.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import streamlit as st
22
import pyvista as pv
33
import numpy as np
4-
from stpyvista import stpyvista, dataview
4+
from stpyvista import dataview
5+
from stpyvista.trame_backend import stpyvista
56

67

78
def put_in_plotter(actor: pv.DataSet):

0 commit comments

Comments
 (0)