This commit is contained in:
cjw
2026-02-12 23:22:11 +08:00
parent 7b09eb3d89
commit 89660bba4e
5988 changed files with 2517516 additions and 0 deletions
@@ -0,0 +1,28 @@
"""Trame interface for PyVista."""
from __future__ import annotations
import logging
logging.getLogger('trame.app').disabled = True
from pyvista.trame.jupyter import elegantly_launch
from pyvista.trame.jupyter import launch_server
from pyvista.trame.jupyter import show_trame
from pyvista.trame.ui import get_viewer
from pyvista.trame.ui import plotter_ui
from pyvista.trame.views import PyVistaLocalView
from pyvista.trame.views import PyVistaRemoteLocalView
from pyvista.trame.views import PyVistaRemoteView
# __all__ only left for mypy --strict to work when pyvista is a dependency
__all__ = [
'PyVistaLocalView',
'PyVistaRemoteLocalView',
'PyVistaRemoteView',
'elegantly_launch',
'get_viewer',
'launch_server',
'plotter_ui',
'show_trame',
]
@@ -0,0 +1,485 @@
"""Trame utilities for running in Jupyter."""
from __future__ import annotations
import asyncio
import logging
import os
from typing import TYPE_CHECKING
from typing import Literal
import warnings
from trame.widgets import html as html_widgets
from trame.widgets import vtk as vtk_widgets
from trame.widgets import vuetify as vuetify2_widgets
from trame.widgets import vuetify3 as vuetify3_widgets
from typing_extensions import Concatenate
try:
from ipywidgets.widgets import HTML
except ImportError:
HTML = object
import pyvista
from pyvista.trame.ui import UI_TITLE
from pyvista.trame.ui import get_viewer
from pyvista.trame.views import CLOSED_PLOTTER_ERROR
from pyvista.trame.views import get_server
if TYPE_CHECKING:
from collections.abc import Callable
from IPython.display import IFrame
from pyvista.jupyter import JupyterBackendOptions
from pyvista.plotting.plotter import Plotter
from pyvista.trame.ui.vuetify2 import Viewer
SERVER_DOWN_MESSAGE = """Trame server has not launched.
You must start the trame server before attempting to `show()`
with PyVista.
You can use the following snippet to launch the server:
from pyvista.trame.jupyter import launch_server
await launch_server('{name}').ready
"""
JUPYTER_SERVER_DOWN_MESSAGE = """Trame server has not launched.
Prior to plotting, please make sure to run `set_jupyter_backend('trame')` when using the
`'trame'`, `'server'`, or `'client'` Jupyter backends.
import pyvista as pv
pyvista.set_jupyter_backend('trame')
If this issue persists, please open an issue in PyVista: https://github.com/pyvista/pyvista/issues
"""
logger = logging.getLogger(__name__)
class TrameServerDownError(RuntimeError): # numpydoc ignore=PR01
"""Exception when trame server is down for Jupyter."""
def __init__(self, server_name):
"""Call the base class constructor with the custom message."""
super().__init__(SERVER_DOWN_MESSAGE.format(name=server_name))
class TrameJupyterServerDownError(RuntimeError):
"""Exception when trame server is down for Jupyter."""
def __init__(self):
"""Call the base class constructor with the custom message."""
# Be incredibly verbose on how users should launch trame server
# Both warn so it appears at top
warnings.warn(JUPYTER_SERVER_DOWN_MESSAGE)
# and Error
super().__init__(JUPYTER_SERVER_DOWN_MESSAGE)
class Widget(HTML): # type: ignore[misc] # numpydoc ignore=PR01
"""Custom HTML iframe widget for trame viewer."""
def __init__(self, viewer, src, width=None, height=None, iframe_attrs=None, **kwargs):
"""Initialize."""
if HTML is object:
msg = 'Please install `ipywidgets`.'
raise ImportError(msg)
# eventually we could maybe expose this, but for now make sure we're at least
# consistent with matplotlib's color (light gray)
if iframe_attrs is None:
iframe_attrs = {}
border = 'border: 1px solid rgb(221,221,221);'
iframe_attrs = {
**iframe_attrs,
'src': src,
'class': 'pyvista',
'style': f'width: {width}; height: {height}; {border}',
}
iframe_attrs_str = ' '.join(f'{key}="{value!s}"' for key, value in iframe_attrs.items())
value = f'<iframe {iframe_attrs_str}></iframe>'
super().__init__(value, **kwargs)
self._viewer = viewer
self._src = src
@property
def viewer(self): # numpydoc ignore=RT01
"""Get the associated viewer instance."""
return self._viewer
@property
def src(self): # numpydoc ignore=RT01
"""Get the src URL."""
return self._src
class EmbeddableWidget(HTML): # type: ignore[misc] # numpydoc ignore=PR01
"""Custom HTML iframe widget for embedding the trame viewer."""
def __init__(self, plotter, width, height, **kwargs):
"""Initialize."""
if HTML is object:
msg = 'Please install `ipywidgets`.'
raise ImportError(msg)
scene = plotter.export_html(filename=None)
src = scene.getvalue().replace('"', '&quot;')
# eventually we could maybe expose this, but for now make sure we're at least
# consistent with matplotlib's color (light gray)
border = 'border: 1px solid rgb(221,221,221);'
value = (
f'<iframe srcdoc="{src}" class="pyvista" style="width: {width}; '
f'height: {height}; {border}"></iframe>'
)
super().__init__(value, **kwargs)
self._src = src
def launch_server(server=None, port=None, host=None, wslink_backend=None, **kwargs):
"""Launch a trame server for use with Jupyter.
Parameters
----------
server : str, optional
By default this uses :attr:`pyvista.global_theme.trame.jupyter_server_name
<pyvista.plotting.themes._TrameConfig.jupyter_server_name>`, which by default is
set to ``'pyvista-jupyter'``.
If a server name is given and such server is not available yet, it will
be created otherwise the previously created instance will be returned.
port : int, optional
The port on which to bind the server. Defaults to 0 to automatically
find an available port.
host : str, optional
The host name to bind the server to on launch. Server will bind to
``127.0.0.1`` by default unless user sets the environment variable ``TRAME_DEFAULT_HOST``.
wslink_backend : str, optional
The wslink backend that the server should use
``aiohttp`` by default, ``jupyter`` if the
`trame_jupyter_extension <https://github.com/Kitware/trame-jupyter-extension>`_
is used.
**kwargs : dict, optional
Any additional keyword arguments to pass to ``pyvista.trame.views.get_server``.
Returns
-------
trame_server.core.Server
The launched Trame server. To ``await`` the launch, use the
``.ready`` future attribute on the server.
"""
if server is None:
server = pyvista.global_theme.trame.jupyter_server_name
if isinstance(server, str):
server = get_server(server, **kwargs)
if port is None:
port = pyvista.global_theme.trame.jupyter_server_port
if host is None:
# Default to `127.0.0.1` unless user sets TRAME_DEFAULT_HOST
host = os.environ.get('TRAME_DEFAULT_HOST', '127.0.0.1')
if (
wslink_backend is None and pyvista.global_theme.trame.jupyter_extension_enabled
): # pragma: no cover
wslink_backend = 'jupyter'
# Must enable all used modules
html_widgets.initialize(server)
vtk_widgets.initialize(server)
if server.client_type == 'vue2':
vuetify2_widgets.initialize(server)
else:
vuetify3_widgets.initialize(server)
def on_ready(**_):
logger.debug(f'Server ready: {server}')
if server._running_stage == 0:
server.controller.on_server_ready.add(on_ready)
server.start(
exec_mode='task',
host=host,
port=port,
open_browser=False,
show_connection_info=False,
disable_logging=True,
timeout=0,
backend=wslink_backend,
)
# else, server is already running or launching
return server
def build_url(
_server,
ui=None,
server_proxy_enabled=None,
server_proxy_prefix=None,
host='localhost',
protocol='http',
): # numpydoc ignore=PR01,RT01
"""Build the URL for the iframe."""
params = f'?ui={ui}&reconnect=auto' if ui else '?reconnect=auto'
if server_proxy_enabled is None:
server_proxy_enabled = pyvista.global_theme.trame.server_proxy_enabled
if server_proxy_enabled:
if server_proxy_prefix is None:
server_proxy_prefix = pyvista.global_theme.trame.server_proxy_prefix
# server_proxy_prefix assumes trailing slash
prefix = server_proxy_prefix if server_proxy_prefix else ''
src = f'{prefix}{_server.port}/index.html{params}'
else:
src = f'{protocol}://{host}:{_server.port}/index.html{params}'
logger.debug(src)
return src
def initialize(
server,
plotter,
mode=None,
default_server_rendering=True,
collapse_menu=False,
**kwargs,
): # numpydoc ignore=PR01,RT01
"""Generate the UI for a given plotter."""
state = server.state
state.trame__title = UI_TITLE
viewer = get_viewer(
plotter,
server=server,
suppress_rendering=mode == 'client',
)
with viewer.make_layout(server, template_name=plotter._id_name) as layout:
viewer.layout = layout
viewer.ui(
mode=mode,
default_server_rendering=default_server_rendering,
collapse_menu=collapse_menu,
**kwargs,
)
return viewer
def show_trame(
plotter: Plotter,
mode: JupyterBackendOptions | None = None,
name: str | None = None,
server_proxy_enabled: bool | None = None,
server_proxy_prefix: str | None = None,
jupyter_extension_enabled: bool | None = None,
collapse_menu: bool = False,
add_menu: bool = True,
add_menu_items: Callable[[Literal['trame', 'server', 'client'], bool, bool], None]
| None = None,
default_server_rendering: bool = True,
handler: Callable[Concatenate[Viewer, str, ...], IFrame] | None = None,
**kwargs,
) -> EmbeddableWidget | IFrame | Widget:
"""Run and display the trame application in jupyter's event loop.
Parameters
----------
plotter : pyvista.Plotter
The PyVista plotter to show.
mode : str, optional
The UI view mode. This can be set on the global theme. Options are:
* ``'trame'``: Uses a view that can switch between client and server
rendering modes.
* ``'server'``: Uses a view that is purely server rendering.
* ``'client'``: Uses a view that is purely client rendering (generally
safe without a virtual frame buffer)
* ``'html'``: Exports the scene for client rendering that can be
embedded in a webpage.
name : str, optional
The name of the trame server on which the UI is defined.
server_proxy_enabled : bool, default: False
Build a relative URL for use with ``jupyter-server-proxy``.
server_proxy_prefix : str, optional
URL prefix when using ``server_proxy_enabled``. This can be set
globally in the theme. To ignore, pass ``False``. For use with
``jupyter-server-proxy``, often set to ``proxy/``.
jupyter_extension_enabled : bool, default: False
Build a relative URL for use with ``trame-jupyter-extension``.
collapse_menu : bool, default: False
Collapse the UI menu (camera controls, etc.) on start.
add_menu : bool, default: True
Add a UI controls VCard to the VContainer.
add_menu_items : callable, default: None
Append more UI controls to the VCard menu. Should be a function similar to
Viewer.ui_controls().
default_server_rendering : bool, default: True
Whether to use server-side or client-side rendering on-start when
using the ``'trame'`` mode.
handler : callable, optional
Pass a callable that accptes the viewer instance, the string URL,
and ``**kwargs`` to create custom HTML representations of the output.
.. code-block:: python
import pyvista as pv
from IPython.display import IFrame
mesh = pyvista.Wavelet()
def handler(viewer, src, **kwargs):
return IFrame(src, '75%', '500px')
p = pyvista.Plotter(notebook=True)
_ = p.add_mesh(mesh)
iframe = p.show(
jupyter_backend='trame',
jupyter_kwargs=dict(handler=handler),
return_viewer=True,
)
iframe
**kwargs : dict, optional
Mostly ignored, though ``protocol`` and ``host`` can be use to
override the iframe src url and ``height`` and ``width`` can be
used to override the iframe style. Remaining kwargs are passed to
``ipywidgets.widgets.HTML``.
Returns
-------
ipywidgets.widgets.HTML or handler result
Returns a HTML IFrame widget or the result of the passed handler.
"""
if plotter.render_window is None:
raise RuntimeError(CLOSED_PLOTTER_ERROR)
if plotter._window_size_unset:
dw, dh = '99%', '600px'
else:
width, height = plotter.window_size
dw = f'{width}px'
dh = f'{height}px'
kwargs.setdefault('width', dw)
kwargs.setdefault('height', dh)
if mode == 'html':
return EmbeddableWidget(plotter, **kwargs)
if jupyter_extension_enabled is None:
jupyter_extension_enabled = pyvista.global_theme.trame.jupyter_extension_enabled
if name is None:
server = get_server(name=pyvista.global_theme.trame.jupyter_server_name)
else:
server = get_server(name=name)
if name is None and not server.running:
wslink_backend = 'aiohttp'
if jupyter_extension_enabled: # pragma: no cover
wslink_backend = 'jupyter'
elegantly_launch(server, wslink_backend=wslink_backend)
if not server.running: # pragma: no cover
raise TrameJupyterServerDownError
elif not server.running:
raise TrameServerDownError(name)
# Initialize app
viewer = initialize(
server,
plotter,
mode=mode,
default_server_rendering=default_server_rendering,
collapse_menu=collapse_menu,
add_menu=add_menu,
add_menu_items=add_menu_items,
)
if jupyter_extension_enabled: # pragma: no cover
from trame_client.ui.core import iframe_url_builder_jupyter_extension # noqa: PLC0415
iframe_attrs = iframe_url_builder_jupyter_extension(viewer.layout)
src = iframe_attrs['src']
else:
# TODO: The build_url function could possibly be replaced by
# trame's upstream url builders in trame_client.ui.core
iframe_attrs = {}
src = build_url(
server,
ui=plotter._id_name,
server_proxy_enabled=server_proxy_enabled,
server_proxy_prefix=server_proxy_prefix,
host=kwargs.get('host', 'localhost'),
protocol=kwargs.get('protocol', 'http'),
)
if callable(handler):
return handler(viewer, src, iframe_attrs=iframe_attrs, **kwargs)
return Widget(viewer, src, iframe_attrs=iframe_attrs, **kwargs)
def elegantly_launch(*args, **kwargs): # numpydoc ignore=PR01
"""Elegantly launch the Trame server without await.
This provides a mechanism to launch the Trame Jupyter backend in
a way that does not require users to await the call.
This is a thin wrapper of
:func:`launch_server() <pyvista.trame.jupyter.launch_server>`.
Returns
-------
trame_server.core.Server
The launched trame server.
Warnings
--------
This uses `nest_asyncio <https://github.com/erdewit/nest_asyncio>`_ which
patches the standard lib `asyncio` package and may have unintended
consequences for some uses cases. We advise strongly to make sure PyVista's
Jupyter backend is not set to use Trame when not in a Jupyter environment.
"""
try:
import nest_asyncio # noqa: PLC0415
except ImportError:
msg = (
'Please install `nest_asyncio` to automagically launch the trame server '
'without await. Or, to avoid `nest_asynctio` run:\n\n'
'from pyvista.trame.jupyter import launch_server\n'
'await launch_server().ready'
)
raise ImportError(msg)
async def launch_it():
await launch_server(*args, **kwargs).ready
# Basically monkey patches asyncio to support this
nest_asyncio.apply()
return asyncio.run(launch_it())
@@ -0,0 +1,120 @@
"""PyVista Trame User Interface.
This module builds a base UI for manipulating a PyVista Plotter.
The UI generated here is the default for rendering in Jupyter
environments and provides a starting point for custom user-built
applications.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import warnings
from trame.app import get_server
from .vuetify2 import Viewer as Vue2Viewer
from .vuetify3 import Viewer as Vue3Viewer
if TYPE_CHECKING:
from .base_viewer import BaseViewer
_VIEWERS: dict[str, BaseViewer] = {}
UI_TITLE = 'PyVista'
def get_viewer(plotter, server=None, suppress_rendering=False):
"""Get a Viewer instance for a given Plotter.
There should be only one Viewer instance per plotter. A Viewer
can have multiple UI views though.
Parameters
----------
plotter : pyvista.Plotter
Plotter to return or create the viewer instance for.
server : trame.Server, optional
Current server for Trame application.
suppress_rendering : bool, default: False
Suppress rendering on the plotter.
Returns
-------
pyvista.trame.ui.Viewer
Trame viewer.
"""
if plotter._id_name in _VIEWERS:
viewer = _VIEWERS[plotter._id_name]
if suppress_rendering != plotter.suppress_rendering:
plotter.suppress_rendering = suppress_rendering
warnings.warn(
'Suppress rendering on the plotter is changed to ' + str(suppress_rendering),
UserWarning,
)
return viewer
if not server:
server = get_server()
if server.client_type == 'vue2':
viewer = Vue2Viewer(plotter, suppress_rendering=suppress_rendering, server=server)
else:
viewer = Vue3Viewer(plotter, suppress_rendering=suppress_rendering, server=server)
_VIEWERS[plotter._id_name] = viewer
return viewer
def plotter_ui(
plotter,
mode=None,
default_server_rendering=True,
collapse_menu=False,
add_menu=True,
**kwargs,
):
"""Create a UI view for the given Plotter.
Parameters
----------
plotter : pyvista.Plotter
Plotter to create the UI for.
mode : str, default: 'trame'
The UI view mode. Options are:
* ``'trame'``: Uses a view that can switch between client and server
rendering modes.
* ``'server'``: Uses a view that is purely server rendering.
* ``'client'``: Uses a view that is purely client rendering (generally
safe without a virtual frame buffer)
default_server_rendering : bool, default: True
Whether to use server-side or client-side rendering on-start when
using the ``'trame'`` mode.
collapse_menu : bool, default: False
Collapse the UI menu (camera controls, etc.) on start.
add_menu : bool, default: True
Add a UI controls VCard to the VContainer.
**kwargs : dict, optional
Additional keyword arguments are passed to the viewer being created.
Returns
-------
PyVistaRemoteLocalView | PyVistaRemoteView | PyVistaLocalView
Trame view interface for pyvista.
"""
viewer = get_viewer(plotter, server=kwargs.get('server'), suppress_rendering=mode == 'client')
return viewer.ui(
mode=mode,
default_server_rendering=default_server_rendering,
collapse_menu=collapse_menu,
add_menu=add_menu,
**kwargs,
)
@@ -0,0 +1,306 @@
"""PyVista Trame Base Viewer class.
This base class defines methods to manipulate a PyVista Plotter.
This base class does not define a `ui` method, but its derived classes do.
See `pyvista.trame.ui.vuetify2` and ``pyvista.trame.ui.vuetify3` for its derived classes.
"""
from __future__ import annotations
import io
from typing import TYPE_CHECKING
from trame.app import get_server
import pyvista
if TYPE_CHECKING:
from trame_client.ui.core import AbstractLayout
class BaseViewer:
"""Internal wrapper to sync trame view with Plotter.
Parameters
----------
plotter : pyvista.Plotter
Target Plotter instance to view.
server : trame.Server, optional
Current Server for Trame Application.
suppress_rendering : bool, default=False
Whether to suppress rendering on the Plotter.
"""
def __init__(self, plotter, server=None, suppress_rendering=False):
"""Initialize Viewer."""
self._html_views = set()
if server is None:
server = get_server()
self.server = server
self.plotter = plotter
self.plotter.suppress_rendering = suppress_rendering
# State variable names
self.SHOW_UI = f'{plotter._id_name}_show_ui'
self.GRID = f'{plotter._id_name}_grid_visibility'
self.OUTLINE = f'{plotter._id_name}_outline_visibility'
self.EDGES = f'{plotter._id_name}_edge_visibility'
self.AXIS = f'{plotter._id_name}_axis_visibility'
self.PARALLEL = f'{plotter._id_name}_parallel_projection'
self.SERVER_RENDERING = f'{plotter._id_name}_use_server_rendering'
self.VALID_UI_MODES = [
'trame',
'client',
'server',
]
server.state[self.SHOW_UI] = True
server.state[self.GRID] = False
server.state[self.OUTLINE] = False
server.state[self.EDGES] = False
server.state[self.AXIS] = False
server.state[self.PARALLEL] = False
@property
def views(self): # numpydoc ignore=RT01
"""Get a set of all associate trame views for this viewer."""
return self._html_views
def update(self, **kwargs): # noqa: ARG002
"""Update all associated views.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
for view in self._html_views:
view.update()
def push_camera(self, **kwargs): # noqa: ARG002
"""Push camera to all associated views.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
for view in self._html_views:
view.push_camera()
def reset_camera(self, **kwargs): # noqa: ARG002
"""Reset camera for all associated views.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
for view in self._html_views:
view.reset_camera()
def update_image(self, **kwargs): # noqa: ARG002
"""Update image for all associated views.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
for view in self._html_views:
view.update_image()
def update_camera(self, **kwargs): # noqa: ARG002
"""Update image and camera for all associated views.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
for view in self._html_views:
view.update_camera()
def view_isometric(self):
"""View isometric."""
self.plotter.view_isometric(render=False)
self.update_camera()
def view_yz(self):
"""View YZ plane."""
self.plotter.view_yz(render=False)
self.update_camera()
def view_xz(self):
"""View XZ plane."""
self.plotter.view_xz(render=False)
self.update_camera()
def view_xy(self):
"""View XY plane."""
self.plotter.view_xy(render=False)
self.update_camera()
def on_parallel_projection_change(self, **kwargs):
"""Toggle parallel projection for all renderers.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
value = kwargs[self.PARALLEL]
for renderer in self.plotter.renderers:
if value:
renderer.enable_parallel_projection()
else:
renderer.disable_parallel_projection()
self.update()
def on_edge_visibility_change(self, **kwargs):
"""Toggle edge visibility for all actors.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
value = kwargs[self.EDGES]
for renderer in self.plotter.renderers:
for actor in renderer.actors.values():
if isinstance(actor, pyvista.Actor):
actor.prop.show_edges = value
self.update()
def on_grid_visibility_change(self, **kwargs):
"""Handle axes grid visibility.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
value = kwargs[self.GRID]
for renderer in self.plotter.renderers:
if value:
renderer.show_grid()
else:
renderer.remove_bounds_axes()
self.update()
def on_outline_visibility_change(self, **kwargs):
"""Handle outline visibility.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
value = kwargs[self.OUTLINE]
for renderer in self.plotter.renderers:
if value:
renderer.add_bounding_box(reset_camera=False)
else:
renderer.remove_bounding_box()
self.update()
def on_axis_visibility_change(self, **kwargs):
"""Handle outline visibility.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
value = kwargs[self.AXIS]
for renderer in self.plotter.renderers:
if value:
renderer.show_axes()
else:
renderer.hide_axes()
for view in self._html_views:
if view.set_widgets:
# VtkRemoteView does not have set_widgets function, but
# VtkRemoteLocalView and VtkLocalView do.
view.set_widgets(
[
ren.axes_widget
for ren in self.plotter.renderers
if ren.axes_widget is not None
],
)
self.update()
def on_rendering_mode_change(self, **kwargs):
"""Handle any configurations when the render mode changes between client and server.
Parameters
----------
**kwargs : dict, optional
Unused keyword arguments.
"""
if not kwargs[self.SERVER_RENDERING]:
self.update_camera()
@property
def actors(self): # numpydoc ignore=RT01
"""Get dataset actors."""
return {k: v for k, v in self.plotter.actors.items() if isinstance(v, pyvista.Actor)}
def screenshot(self):
"""Take screenshot and add attachament.
Returns
-------
memoryview
Screenshot as a ``memoryview``.
"""
self.plotter.render()
self.update() # makes sure the plotter and views are in sync
buffer = io.BytesIO()
self.plotter.screenshot(filename=buffer)
buffer.seek(0)
return memoryview(buffer.read())
def export(self): # numpydoc ignore=RT01
"""Export the scene as a zip file."""
for view in self._html_views:
return memoryview(view.export_html())
msg = 'This viewer cannot be exported.'
raise TypeError(msg)
def ui(self):
"""Implement in derived classes."""
raise NotImplementedError
def make_layout(self, *args, **kwargs) -> AbstractLayout: # pragma: no cover
"""Create an instance of an AbstractLayout which is appropriate for a concrete viewer.
Parameters
----------
*args : tuple
Positional arguments.
**kwargs : dict, optional
Keyword arguments.
Returns
-------
AbstractLayout
A layout this viewer can be embedded in.
"""
raise NotImplementedError
@@ -0,0 +1,345 @@
"""PyVista Trame Viewer class for a Vue 2 client.
This class, derived from `pyvista.trame.ui.base_viewer`,
is intended for use with a trame application where the client type is "vue2".
Therefore, the `ui` method implemented by this class utilizes the API of Vuetify 2.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from trame.ui.vuetify2 import VAppLayout
from trame.widgets import html
from trame.widgets import vuetify
from pyvista.trame.views import PyVistaLocalView
from pyvista.trame.views import PyVistaRemoteLocalView
from pyvista.trame.views import PyVistaRemoteView
from .base_viewer import BaseViewer
if TYPE_CHECKING:
from trame_client.ui.core import AbstractLayout
def button(click, icon, tooltip): # numpydoc ignore=PR01
"""Create a vuetify button."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator='{ on, attrs }'):
with vuetify.VBtn(icon=True, v_bind='attrs', v_on='on', click=click):
vuetify.VIcon(icon)
html.Span(tooltip)
def checkbox(model, icons, tooltip): # numpydoc ignore=PR01
"""Create a vuetify checkbox."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator='{ on, attrs }'):
with html.Div(v_on='on', v_bind='attrs'):
vuetify.VCheckbox(
v_model=model,
on_icon=icons[0],
off_icon=icons[1],
dense=True,
hide_details=True,
classes='my-0 py-0 ml-1',
)
html.Span(tooltip)
def slider(model, tooltip, **kwargs): # numpydoc ignore=PR01
"""Create a vuetify slider."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator='{ on, attrs }'):
with html.Div(v_on='on', v_bind='attrs'):
vuetify.VSlider(v_model=model, **kwargs)
html.Span(tooltip)
def text_field(model, tooltip, **kwargs): # numpydoc ignore=PR01
"""Create a vuetify text field."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator='{ on, attrs }'):
with html.Div(v_on='on', v_bind='attrs'):
vuetify.VTextField(v_model=model, **kwargs)
html.Span(tooltip)
def select(model, tooltip, **kwargs): # numpydoc ignore=PR01
"""Create a vuetify select menu."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator='{ on, attrs }'):
with html.Div(v_on='on', v_bind='attrs'):
vuetify.VSelect(v_model=model, **kwargs)
html.Span(tooltip)
def divider(**kwargs): # numpydoc ignore=PR01
"""Create a vuetify divider."""
vuetify.VDivider(**kwargs)
class Viewer(BaseViewer):
"""Viewer implementation compatible with Vue 2 Trame Applications."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def make_layout(self, *args, **kwargs) -> AbstractLayout:
"""Create instance of an AbstractLayout which is appropriate for this viewer.
Parameters
----------
*args : tuple
Positional arguments.
**kwargs : dict, optional
Keyword arguments.
Returns
-------
VAppLayout (vue2)
A layout this viewer can be embedded in.
"""
return VAppLayout(*args, **kwargs)
def ui_controls(self, mode=None, default_server_rendering=True, v_show=None):
"""Create a VRow for the UI controls.
Parameters
----------
mode : str, default: 'trame'
The UI view mode. Options are:
* ``'trame'``: Uses a view that can switch between client and server
rendering modes.
* ``'server'``: Uses a view that is purely server rendering.
* ``'client'``: Uses a view that is purely client rendering (generally
safe without a virtual frame buffer)
default_server_rendering : bool, default: True
Whether to use server-side or client-side rendering on-start when
using the ``'trame'`` mode.
v_show : bool, optional
Conditionally show the viewer controls.
"""
with vuetify.VRow(
v_show=v_show,
classes='pa-0 ma-0 align-center',
) as row:
server = row.server
# Listen to state changes
server.state.change(self.EDGES)(self.on_edge_visibility_change)
server.state.change(self.GRID)(self.on_grid_visibility_change)
server.state.change(self.OUTLINE)(self.on_outline_visibility_change)
server.state.change(self.AXIS)(self.on_axis_visibility_change)
server.state.change(self.PARALLEL)(self.on_parallel_projection_change)
server.state.change(self.SERVER_RENDERING)(self.on_rendering_mode_change)
vuetify.VDivider(vertical=True, classes='mr-1')
button(
click=self.reset_camera,
icon='mdi-arrow-expand-all',
tooltip='Reset Camera',
)
vuetify.VDivider(vertical=True, classes='mx-1')
button(
click=self.view_isometric,
icon='mdi-axis-arrow',
tooltip='Perspective view',
)
button(
click=self.view_yz,
icon='mdi-axis-x-arrow',
tooltip='Reset Camera X',
)
button(
click=self.view_xz,
icon='mdi-axis-y-arrow',
tooltip='Reset Camera Y',
)
button(
click=self.view_xy,
icon='mdi-axis-z-arrow',
tooltip='Reset Camera Z',
)
vuetify.VDivider(vertical=True, classes='mx-1')
checkbox(
model=(self.EDGES, False),
icons=('mdi-grid', 'mdi-grid-off'),
tooltip=f"Toggle edge visibility ({{{{ {self.EDGES} ? 'on' : 'off' }}}})",
)
checkbox(
model=(self.OUTLINE, False),
icons=('mdi-cube', 'mdi-cube-off'),
tooltip=f"Toggle bounding box ({{{{ {self.OUTLINE} ? 'on' : 'off' }}}})",
)
checkbox(
model=(self.GRID, False),
icons=('mdi-ruler-square', 'mdi-ruler-square'),
tooltip=f"Toggle ruler ({{{{ {self.GRID} ? 'on' : 'off' }}}})",
)
checkbox(
model=(self.AXIS, False),
icons=('mdi-axis-arrow-info', 'mdi-axis-arrow-info'),
tooltip=f"Toggle axis ({{{{ {self.AXIS} ? 'on' : 'off' }}}})",
)
# Server rendering options
if mode == 'trame':
vuetify.VDivider(vertical=True, classes='mx-1')
checkbox(
model=(self.SERVER_RENDERING, default_server_rendering),
icons=('mdi-dns', 'mdi-open-in-app'),
tooltip=f'Toggle rendering mode '
f"({{{{ {self.SERVER_RENDERING} ? 'remote' : 'local' }}}})",
)
with vuetify.VRow(
v_show=(self.SERVER_RENDERING, default_server_rendering),
classes='pa-0 ma-0 align-center',
):
checkbox(
model=(self.PARALLEL, False),
icons=('mdi-camera-off', 'mdi-camera-switch'),
tooltip=f'Toggle parallel projection '
f"({{{{ {self.PARALLEL} ? 'on' : 'off' }}}})",
)
def attach_screenshot():
return server.protocol.addAttachment(self.screenshot())
button(
# Must use single-quote string for JS here
click=f"utils.download('screenshot.png', "
f"trigger('{server.trigger_name(attach_screenshot)}'), "
f"'image/png')",
icon='mdi-file-png-box',
tooltip='Save screenshot',
)
def attach_export():
return server.protocol.addAttachment(self.export())
button(
# Must use single-quote string for JS here
click="utils.download('scene-export.html', "
f"trigger('{server.trigger_name(attach_export)}'), "
"'application/octet-stream')",
icon='mdi-download',
tooltip='Export scene as HTML',
)
def ui(
self,
mode=None,
default_server_rendering=True,
collapse_menu=False,
add_menu=True,
add_menu_items=None,
**kwargs,
):
"""Generate VContainer for PyVista Plotter.
Parameters
----------
mode : str, default: 'trame'
The UI view mode. Options are:
* ``'trame'``: Uses a view that can switch between client and server
rendering modes.
* ``'server'``: Uses a view that is purely server rendering.
* ``'client'``: Uses a view that is purely client rendering (generally
safe without a virtual frame buffer)
default_server_rendering : bool, default: True
Whether to use server-side or client-side rendering on-start when
using the ``'trame'`` mode.
collapse_menu : bool, default: False
Collapse the UI menu (camera controls, etc.) on start.
add_menu : bool, default: True
Add a UI controls VCard to the VContainer.
add_menu_items : callable, default: None
Append more UI controls to the VCard menu. Should be a function similar to
`Viewer.ui_controls()`.
**kwargs : dict, optional
Additional keyword arguments are passed to the view being created.
Returns
-------
PyVistaRemoteLocalView, PyVistaRemoteView, or PyVistaLocalView
Trame view interface for pyvista.
"""
if mode is None:
mode = self.plotter._theme.trame.default_mode
if mode not in self.VALID_UI_MODES:
msg = f'`{mode}` is not a valid mode choice. Use one of: {self.VALID_UI_MODES}'
raise ValueError(msg)
if mode != 'trame':
default_server_rendering = mode == 'server'
with vuetify.VContainer(
fluid=True,
classes='pa-0 fill-height',
style='position: relative',
trame_server=self.server,
) as container:
server = container.server
# Initialize state variables
server.state[self.EDGES] = False
server.state[self.GRID] = self.plotter.renderer.cube_axes_actor is not None
server.state[self.OUTLINE] = self.plotter.renderer._box_object is not None
server.state[self.AXIS] = (
self.plotter.renderer.axes_widget is not None
and self.plotter.renderer.axes_widget.GetEnabled()
)
server.state[self.SERVER_RENDERING] = default_server_rendering
if add_menu:
server.state[self.SHOW_UI] = not collapse_menu
with vuetify.VCard(
style='position: absolute; top: 20px; left: 20px; z-index: 1; height: 36px;',
classes=(f"{{ 'rounded-circle': !{self.SHOW_UI} }}",),
) as self.menu:
with vuetify.VRow(classes='pa-0 ma-0'):
button(
click=f'{self.SHOW_UI}=!{self.SHOW_UI}',
icon='mdi-dots-vertical',
tooltip=f"{{{{ {self.SHOW_UI} ? 'Hide' : 'Show' }}}} menu",
)
self.ui_controls(
mode=mode,
default_server_rendering=default_server_rendering,
v_show=(f'{self.SHOW_UI}',),
)
if callable(add_menu_items):
with vuetify.VRow(
v_show=(f'{self.SHOW_UI}',),
classes='pa-0 ma-0 align-center',
):
add_menu_items()
if mode == 'trame':
view = PyVistaRemoteLocalView(
self.plotter,
mode=(
# Must use single-quote string for JS here
f"{self.SERVER_RENDERING} ? 'remote' : 'local'",
'remote' if default_server_rendering else 'local',
),
**kwargs,
)
elif mode == 'server':
view = PyVistaRemoteView(self.plotter, **kwargs)
elif mode == 'client':
view = PyVistaLocalView(self.plotter, **kwargs)
self._html_views.add(view)
if add_menu:
view.menu = self.menu
return view
@@ -0,0 +1,356 @@
"""PyVista Trame Viewer class for a Vue 3 client.
This class, derived from `pyvista.trame.ui.base_viewer`,
is intended for use with a trame application where the client type is "vue3".
Therefore, the `ui` method implemented by this class utilizes the API of Vuetify 3.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from trame.ui.vuetify3 import VAppLayout
from trame.widgets import html
from trame.widgets import vuetify3 as vuetify
from pyvista.trame.views import PyVistaLocalView
from pyvista.trame.views import PyVistaRemoteLocalView
from pyvista.trame.views import PyVistaRemoteView
from .base_viewer import BaseViewer
if TYPE_CHECKING:
from trame_client.ui.core import AbstractLayout
def button(click, icon, tooltip): # numpydoc ignore=PR01
"""Create a vuetify button."""
with vuetify.VTooltip(location='bottom'):
with vuetify.Template(v_slot_activator=('{ props }',)):
with vuetify.VBtn(
icon=True,
v_bind=('props',),
variant='text',
size='small',
click=click,
):
vuetify.VIcon(icon)
html.Span(tooltip)
def checkbox(model, icons, tooltip): # numpydoc ignore=PR01
"""Create a vuetify checkbox."""
with vuetify.VTooltip(location='bottom'):
with vuetify.Template(v_slot_activator=('{ props }',)):
with html.Div(v_bind=('props',)):
vuetify.VCheckbox(
v_model=model,
true_icon=icons[0],
false_icon=icons[1],
density='compact',
hide_details=True,
classes='ma-1 py-1',
)
html.Span(tooltip)
def slider(model, tooltip, **kwargs): # numpydoc ignore=PR01
"""Create a vuetify slider."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator=('{ props }',)):
with html.Div(v_bind=('props',)):
vuetify.VSlider(v_model=model, **kwargs)
html.Span(tooltip)
def text_field(model, tooltip, **kwargs): # numpydoc ignore=PR01
"""Create a vuetify text field."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator=('{ props }',)):
with html.Div(v_bind=('props',)):
vuetify.VTextField(v_model=model, **kwargs)
html.Span(tooltip)
def select(model, tooltip, **kwargs): # numpydoc ignore=PR01
"""Create a vuetify select menu."""
with vuetify.VTooltip(bottom=True):
with vuetify.Template(v_slot_activator=('{ props }',)):
with html.Div(v_bind=('props',)):
vuetify.VSelect(v_model=model, **kwargs)
html.Span(tooltip)
def divider(**kwargs): # numpydoc ignore=PR01
"""Create a vuetify divider."""
vuetify.VDivider(**kwargs)
class Viewer(BaseViewer):
"""Viewer implementation compatible with Vue 3 Trame Applications."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def make_layout(self, *args, **kwargs) -> AbstractLayout:
"""Create instance of an AbstractLayout which is appropriate for this viewer.
Parameters
----------
*args : tuple
Positional arguments.
**kwargs : dict, optional
Keyword arguments.
Returns
-------
VAppLayout (vue3)
A layout this viewer can be embedded in.
"""
return VAppLayout(*args, **kwargs)
def ui_controls(self, mode=None, default_server_rendering=True, v_show=None):
"""Create a VRow for the UI controls.
Parameters
----------
mode : str, default: 'trame'
The UI view mode. Options are:
* ``'trame'``: Uses a view that can switch between client and server
rendering modes.
* ``'server'``: Uses a view that is purely server rendering.
* ``'client'``: Uses a view that is purely client rendering (generally
safe without a virtual frame buffer)
default_server_rendering : bool, default: True
Whether to use server-side or client-side rendering on-start when
using the ``'trame'`` mode.
v_show : bool, optional
Conditionally show the viewer controls.
"""
with vuetify.VRow(
v_show=v_show,
classes='pa-0 ma-0 align-center fill-height',
style='flex-wrap: nowrap',
) as row:
server = row.server
# Listen to state changes
server.state.change(self.EDGES)(self.on_edge_visibility_change)
server.state.change(self.GRID)(self.on_grid_visibility_change)
server.state.change(self.OUTLINE)(self.on_outline_visibility_change)
server.state.change(self.AXIS)(self.on_axis_visibility_change)
server.state.change(self.SERVER_RENDERING)(self.on_rendering_mode_change)
server.state.change(self.PARALLEL)(self.on_parallel_projection_change)
vuetify.VDivider(vertical=True, classes='mr-1')
button(
click=self.reset_camera,
icon='mdi-arrow-expand-all',
tooltip='Reset Camera',
)
vuetify.VDivider(vertical=True, classes='mx-1')
button(
click=self.view_isometric,
icon='mdi-axis-arrow',
tooltip='Perspective view',
)
button(
click=self.view_yz,
icon='mdi-axis-x-arrow',
tooltip='Reset Camera X',
)
button(
click=self.view_xz,
icon='mdi-axis-y-arrow',
tooltip='Reset Camera Y',
)
button(
click=self.view_xy,
icon='mdi-axis-z-arrow',
tooltip='Reset Camera Z',
)
vuetify.VDivider(vertical=True, classes='mx-1')
checkbox(
model=(self.EDGES, False),
icons=('mdi-grid', 'mdi-grid-off'),
tooltip=f"Toggle edge visibility ({{{{ {self.EDGES} ? 'on' : 'off' }}}})",
)
checkbox(
model=(self.OUTLINE, False),
icons=('mdi-cube', 'mdi-cube-off'),
tooltip=f"Toggle bounding box ({{{{ {self.OUTLINE} ? 'on' : 'off' }}}})",
)
checkbox(
model=(self.GRID, False),
icons=('mdi-ruler-square', 'mdi-ruler-square'),
tooltip=f"Toggle ruler ({{{{ {self.GRID} ? 'on' : 'off' }}}})",
)
checkbox(
model=(self.AXIS, False),
icons=('mdi-axis-arrow-info', 'mdi-axis-arrow-info'),
tooltip=f"Toggle axis ({{{{ {self.AXIS} ? 'on' : 'off' }}}})",
)
# Server rendering options
if mode == 'trame':
vuetify.VDivider(vertical=True, classes='mx-1')
checkbox(
model=(self.SERVER_RENDERING, default_server_rendering),
icons=('mdi-dns', 'mdi-open-in-app'),
tooltip=f'Toggle rendering mode '
f"({{{{ {self.SERVER_RENDERING} ? 'remote' : 'local' }}}})",
)
with vuetify.VRow(
v_show=(self.SERVER_RENDERING, default_server_rendering),
classes='pa-0 ma-0 align-center fill-height',
style='flex-wrap: nowrap; flex: unset',
):
checkbox(
model=(self.PARALLEL, False),
icons=('mdi-camera-off', 'mdi-camera-switch'),
tooltip=f'Toggle parallel projection '
f"({{{{ {self.PARALLEL} ? 'on' : 'off' }}}})",
)
def attach_screenshot():
return server.protocol.addAttachment(self.screenshot())
button(
# Must use single-quote string for JS here
click="utils.download('screenshot.png', "
f"trigger('{server.trigger_name(attach_screenshot)}'), "
"'image/png')",
icon='mdi-file-png-box',
tooltip='Save screenshot',
)
def attach_export():
return server.protocol.addAttachment(self.export())
button(
# Must use single-quote string for JS here
click="utils.download('scene-export.html', "
f"trigger('{server.trigger_name(attach_export)}'), "
"'application/octet-stream')",
icon='mdi-download',
tooltip='Export scene as HTML',
)
def ui(
self,
mode=None,
default_server_rendering=True,
collapse_menu=False,
add_menu=True,
add_menu_items=None,
**kwargs,
):
"""Generate VContainer for PyVista Plotter.
Parameters
----------
mode : str, default: 'trame'
The UI view mode. Options are:
* ``'trame'``: Uses a view that can switch between client and server
rendering modes.
* ``'server'``: Uses a view that is purely server rendering.
* ``'client'``: Uses a view that is purely client rendering (generally
safe without a virtual frame buffer)
default_server_rendering : bool, default: True
Whether to use server-side or client-side rendering on-start when
using the ``'trame'`` mode.
collapse_menu : bool, default: False
Collapse the UI menu (camera controls, etc.) on start.
add_menu : bool, default: True
Add a UI controls VCard to the VContainer.
add_menu_items : callable, default: None
Append more UI controls to the VCard menu. Should be a function similar to
`Viewer.ui_controls()`.
**kwargs : dict, optional
Additional keyword arguments are passed to the view being created.
Returns
-------
PyVistaRemoteLocalView, PyVistaRemoteView, or PyVistaLocalView
Trame view interface for pyvista.
"""
if mode is None:
mode = self.plotter._theme.trame.default_mode
if mode not in self.VALID_UI_MODES:
msg = f'`{mode}` is not a valid mode choice. Use one of: {self.VALID_UI_MODES}'
raise ValueError(msg)
if mode != 'trame':
default_server_rendering = mode == 'server'
with vuetify.VContainer(
fluid=True,
classes='pa-0 fill-height',
style='position: relative',
trame_server=self.server,
) as container:
server = container.server
# Initialize state variables
server.state[self.EDGES] = False
server.state[self.GRID] = self.plotter.renderer.cube_axes_actor is not None
server.state[self.OUTLINE] = self.plotter.renderer._box_object is not None
server.state[self.AXIS] = (
self.plotter.renderer.axes_widget is not None
and self.plotter.renderer.axes_widget.GetEnabled()
)
server.state[self.SERVER_RENDERING] = default_server_rendering
if add_menu:
server.state[self.SHOW_UI] = not collapse_menu
with vuetify.VCard(
style='position: absolute; top: 20px; left: 20px; z-index: 1; height: 36px;',
classes=(f"{{ 'rounded-circle': !{self.SHOW_UI} }}",),
) as self.menu:
with vuetify.VRow(
classes='pa-0 ma-0 align-center fill-height',
style='flex-wrap: nowrap',
):
button(
click=f'{self.SHOW_UI}=!{self.SHOW_UI}',
icon='mdi-dots-vertical',
tooltip=f"{{{{ {self.SHOW_UI} ? 'Hide' : 'Show' }}}} menu",
)
self.ui_controls(
mode=mode,
default_server_rendering=default_server_rendering,
v_show=(f'{self.SHOW_UI}',),
)
if callable(add_menu_items):
with vuetify.VRow(
v_show=(f'{self.SHOW_UI}',),
classes='pa-0 ma-0 align-center',
):
add_menu_items()
if mode == 'trame':
view = PyVistaRemoteLocalView(
self.plotter,
mode=(
# Must use single-quote string for JS here
f"{self.SERVER_RENDERING} ? 'remote' : 'local'",
'remote' if default_server_rendering else 'local',
),
**kwargs,
)
elif mode == 'server':
view = PyVistaRemoteView(self.plotter, **kwargs)
elif mode == 'client':
view = PyVistaLocalView(self.plotter, **kwargs)
self._html_views.add(view)
if add_menu:
view.menu = self.menu
return view
@@ -0,0 +1,282 @@
"""Trame view interface for PyVista."""
from __future__ import annotations
import io
import weakref
from trame.app import get_server as trame_get_server
from trame.widgets.vtk import VtkLocalView
from trame.widgets.vtk import VtkRemoteLocalView
from trame.widgets.vtk import VtkRemoteView
from trame_vtk.tools.vtksz2html import write_html
CLOSED_PLOTTER_ERROR = (
'The render window for this plotter has been destroyed. '
'Do not call `show()` for the plotter before passing to trame.'
)
def get_server(*args, **kwargs): # numpydoc ignore=RT01
"""Override trame's get_server.
Parameters
----------
*args :
Any extra args are passed as option to the server instance.
**kwargs :
Any extra keyword args are passed as option to the server instance.
Returns
-------
trame_server.core.Server
Trame server.
"""
server = trame_get_server(*args, **kwargs)
if 'client_type' in kwargs:
server.client_type = kwargs['client_type']
return server
class _BasePyVistaView:
def __init__(self, plotter):
"""Initialize the base PyVista view."""
self._plotter = weakref.ref(plotter)
self.pyvista_initialize()
self._plotter_render_callback = lambda *_: self.update() # type: ignore[attr-defined]
def pyvista_initialize(self):
if self._plotter().render_window is None: # type: ignore[union-attr]
raise RuntimeError(CLOSED_PLOTTER_ERROR)
for renderer in self._plotter().renderers: # type: ignore[union-attr]
if not renderer.camera.is_set:
renderer.camera_position = renderer.get_default_cam_pos()
renderer.ResetCamera()
def _post_initialize(self):
if self._server.running: # type: ignore[attr-defined]
self.update() # type: ignore[attr-defined]
else:
self._server.controller.on_server_ready.add(self.update) # type: ignore[attr-defined]
# Callback to sync view on PyVista's render call when renders are suppressed
self._plotter().add_on_render_callback(self._plotter_render_callback, render_event=False) # type: ignore[union-attr]
def update_camera(self):
"""Update camera or push the image."""
self.push_camera() # type: ignore[attr-defined]
self.update_image() # type: ignore[attr-defined]
def export_html(self):
"""Export scene to HTML as StringIO buffer."""
content = io.StringIO()
if isinstance(self, PyVistaLocalView):
data = self.export(format='zip')
if data is None:
msg = 'No data to write.'
raise ValueError(msg)
write_html(data, content)
content.seek(0)
elif isinstance(self, PyVistaRemoteLocalView):
data = self.export_geometry(format='zip')
if data is None:
msg = 'No data to write.'
raise ValueError(msg)
write_html(data, content)
content.seek(0)
else:
content = self._plotter().export_html(filename=None) # type: ignore[union-attr]
return io.BytesIO(content.read().encode('utf8')).read()
class PyVistaRemoteView(VtkRemoteView, _BasePyVistaView): # type: ignore[misc]
"""PyVista wrapping of trame ``VtkRemoteView`` for server rendering.
This will connect to a PyVista plotter and stream the server-side
renderings.
Parameters
----------
plotter : pyvista.Plotter
The PyVista Plotter to display in the output view.
interactive_ratio : int, optional
Image size scale factor while interacting. Increasing this
value will give higher resulotuion images during interaction
events at the cost of performance. Use lower values (e.g.,
``0.5``) to increase performance while interacting.
Defaults to 1.
still_ratio : int, optional
Image size scale factor while not interacting (still).
Increasing this value will give higher resulotuion images
when not interacting with the scene. Defaults to 1.
namespace : str, optional
The namespace for this view component. A default value is
chosen based on the ``_id_name`` of the plotter.
**kwargs : dict, optional
Any additional keyword arguments to pass to
``trame.widgets.vtk.VtkRemoteView``.
Notes
-----
For optimal rendering results, you may want to have the same
value for ``interactive_ratio`` and ``still_ratio`` so that
the entire rendering is not re-scaled between interaction events.
"""
def __init__(
self,
plotter,
interactive_ratio=None,
still_ratio=None,
namespace=None,
**kwargs,
): # numpydoc ignore=PR01,RT01
"""Create a trame remote view from a PyVista Plotter."""
_BasePyVistaView.__init__(self, plotter)
if namespace is None:
namespace = f'{plotter._id_name}'
if interactive_ratio is None:
interactive_ratio = plotter._theme.trame.interactive_ratio
if still_ratio is None:
still_ratio = plotter._theme.trame.still_ratio
VtkRemoteView.__init__(
self,
self._plotter().render_window, # type: ignore[union-attr]
interactive_ratio=interactive_ratio,
still_ratio=still_ratio,
__properties=[('still_ratio', 'stillRatio')],
ref=f'view_{plotter._id_name}',
namespace=namespace,
**kwargs,
)
self._post_initialize()
def push_camera(self, *args, **kwargs): # pragma: no cover
"""No-op implementation to match local viewers."""
def update_image(self, *args, **kwargs): # numpydoc ignore=RT01
"""Wrap update call."""
return self.update(*args, **kwargs)
class PyVistaLocalView(VtkLocalView, _BasePyVistaView): # type: ignore[misc]
"""PyVista wrapping of trame VtkLocalView for in-browser rendering.
This will connect to and synchronize with a PyVista plotter to
perform client-side rendering with VTK.js in the browser.
Parameters
----------
plotter : pyvista.Plotter
The PyVista Plotter to represent in the output view.
namespace : str, optional
The namespace for this view component. A default value is
chosen based on the ``_id_name`` of the plotter.
**kwargs : dict, optional
Any additional keyword arguments to pass to
``trame.widgets.vtk.VtkLocalView``.
"""
def __init__(self, plotter, namespace=None, **kwargs):
"""Create a trame local view from a PyVista Plotter."""
_BasePyVistaView.__init__(self, plotter)
if namespace is None:
namespace = f'{plotter._id_name}'
VtkLocalView.__init__(
self,
self._plotter().render_window, # type: ignore[union-attr]
ref=f'view_{plotter._id_name}',
namespace=namespace,
**kwargs,
)
self._post_initialize()
def _post_initialize(self):
super()._post_initialize()
self.set_widgets(
[ren.axes_widget for ren in self._plotter().renderers if ren.axes_widget is not None], # type: ignore[union-attr]
)
def update_image(self, *args, **kwargs): # pragma: no cover
"""No-op implementation to match remote viewers."""
class PyVistaRemoteLocalView(VtkRemoteLocalView, _BasePyVistaView): # type: ignore[misc]
"""PyVista wrapping of trame ``VtkRemoteLocalView``.
Dynamically switch between client and server rendering.
Parameters
----------
plotter : pyvista.Plotter
The PyVista Plotter to display in the output view.
interactive_ratio : int, optional
Image size scale factor while interacting. Increasing this
value will give higher resulotuion images during interaction
events at the cost of performance. Use lower values (e.g.,
``0.5``) to increase performance while interacting.
Defaults to 1. This is only valid in the ``'remote'`` mode.
still_ratio : int, optional
Image size scale factor while not interacting (still).
Increasing this value will give higher resulotuion images
when not interacting with the scene. Defaults to 1.
This is only valid in the ``'remote'`` mode.
namespace : str, optional
The namespace for this view component. A default value is
chosen based on the ``_id_name`` of the plotter.
**kwargs : dict, optional
Any additional keyword arguments to pass to
``trame.widgets.vtk.VtkRemoteLocalView``.
"""
def __init__(
self,
plotter,
interactive_ratio=None,
still_ratio=None,
namespace=None,
**kwargs,
): # numpydoc ignore=PR01,RT01
"""Create a trame remote/local view from a PyVista Plotter."""
_BasePyVistaView.__init__(self, plotter)
if namespace is None:
namespace = f'{plotter._id_name}'
if interactive_ratio is None:
interactive_ratio = plotter._theme.trame.interactive_ratio
if still_ratio is None:
still_ratio = plotter._theme.trame.still_ratio
VtkRemoteLocalView.__init__(
self,
self._plotter().render_window, # type: ignore[union-attr]
interactive_ratio=interactive_ratio,
still_ratio=still_ratio,
__properties=[('still_ratio', 'stillRatio')],
ref=f'view_{plotter._id_name}',
namespace=namespace,
**kwargs,
)
# Track namespace for our use since trame attributes are name mangled
self._namespace = namespace
self._post_initialize()
def _post_initialize(self):
super()._post_initialize()
self.set_widgets(
[ren.axes_widget for ren in self._plotter().renderers if ren.axes_widget is not None], # type: ignore[union-attr]
)