Files
geMoldInsight/venv/lib/python3.11/site-packages/pyvista/trame/jupyter.py
T
2026-02-12 23:27:48 +08:00

486 lines
16 KiB
Python

"""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())