Skip to content

Commit

Permalink
MAINT: Update to use trame backend
Browse files Browse the repository at this point in the history
  • Loading branch information
larsoner committed Sep 5, 2023
1 parent 9fd1869 commit 41ba99d
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 40 deletions.
2 changes: 1 addition & 1 deletion doc/overview/roadmap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Historically we have used Mayavi for 3D visualization, but have faced
limitations and challenges with it. We should work to use some other backend
(e.g., PyVista) to get major improvements, such as:

1. *Proper notebook support (through ipyvtklink)* (complete)
1. *Proper notebook support (through ``ipyvtklink``)* (complete; updated to use ``trame``)
2. *Better interactivity with surface plots* (complete)
3. Time-frequency plotting (complementary to volume-based
:ref:`time-frequency-viz`)
Expand Down
3 changes: 2 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ dependencies:
- ipyevents
- ipywidgets
- ipympl
- ipyvtklink
- trame
- trame-vtk
- jupyter_client
- nbformat
- nbclient
Expand Down
18 changes: 15 additions & 3 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,7 @@ def _nbclient():
from jupyter_client import AsyncKernelManager
from nbclient import NotebookClient
from ipywidgets import Button # noqa
import ipyvtklink # noqa
import trame # noqa
except Exception as exc:
return pytest.skip(f"Skipping Notebook test: {exc}")
km = AsyncKernelManager(config=None)
Expand Down Expand Up @@ -1033,14 +1033,26 @@ def _nbclient():
def nbexec(_nbclient):
"""Execute Python code in a notebook."""
# Adapted/simplified from nbclient/client.py (BSD-3-Clause)
from nbclient.exceptions import CellExecutionError

_nbclient._cleanup_kernel()

def execute(code, reset=False):
_nbclient.reset_execution_trackers()
with _nbclient.setup_kernel():
assert _nbclient.kc is not None
cell = Bunch(cell_type="code", metadata={}, source=dedent(code))
_nbclient.execute_cell(cell, 0, execution_count=0)
cell = Bunch(cell_type="code", metadata={}, source=dedent(code), outputs=[])
try:
_nbclient.execute_cell(cell, 0, execution_count=0)
except CellExecutionError: # pragma: no cover
for kind in ("stdout", "stderr"):
print(
"\n".join(
o["text"] for o in cell.outputs if o.get("name", "") == kind
),
file=getattr(sys, kind),
)
raise
_nbclient.set_widgets_metadata()

yield execute
Expand Down
6 changes: 5 additions & 1 deletion mne/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,12 +651,16 @@ def sys_info(fid=None, show_paths=False, *, dependencies="user", unicode=True):
"# Visualization (optional)",
"pyvista",
"pyvistaqt",
"ipyvtklink",
"vtk",
"qtpy",
"ipympl",
"pyqtgraph",
"mne-qt-browser",
"ipywidgets",
"trame",
"trame_client",
"trame_server",
"trame_vtk",
"",
"# Ecosystem (optional)",
"mne-bids",
Expand Down
14 changes: 7 additions & 7 deletions mne/viz/_brain/tests/test_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def test_notebook_alignment(renderer_notebook, brain_gc, nbexec):
def test_notebook_interactive(renderer_notebook, brain_gc, nbexec):
"""Test interactive modes."""
from contextlib import contextmanager
import os
from pathlib import Path
import tempfile
import time
Expand Down Expand Up @@ -97,9 +96,12 @@ def interactive(on):
tmp_path = Path(tempfile.mkdtemp())
movie_path = tmp_path / "test.gif"
screenshot_path = tmp_path / "test.png"
brain._renderer.actions["movie_field"].value = str(movie_path)
actions = brain._renderer.actions
assert actions["movie_field"]._action.value == ""
actions["movie_field"]._action.value = str(movie_path)
assert not movie_path.is_file()
brain._renderer.actions["screenshot_field"].value = str(screenshot_path)
assert actions["screenshot_field"]._action.value == ""
brain._renderer.actions["screenshot_field"]._action.value = str(screenshot_path)
assert not screenshot_path.is_file()
total_number_of_buttons = sum(
"_field" not in k for k in brain._renderer.actions.keys()
Expand All @@ -117,11 +119,9 @@ def interactive(on):
assert number_of_buttons == total_number_of_buttons
time.sleep(0.5)
assert "movie" in button_names, button_names
# TODO: this fails on GHA for some reason, need to figure it out
if os.getenv("GITHUB_ACTIONS", "") != "true":
assert movie_path.is_file()
assert movie_path.is_file(), movie_path
assert "screenshot" in button_names, button_names
assert screenshot_path.is_file()
assert screenshot_path.is_file(), screenshot_path
img_nv = brain.screenshot()
assert img_nv.shape == (300, 300, 3), img_nv.shape
img_v = brain.screenshot(time_viewer=True)
Expand Down
60 changes: 34 additions & 26 deletions mne/viz/backends/_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import os
import os.path as op
import re
from contextlib import contextmanager, nullcontext

from IPython.display import display, clear_output
Expand Down Expand Up @@ -85,6 +86,7 @@
_take_3d_screenshot, # noqa: F401
)
from ._utils import _notebook_vtk_works
from ...utils import check_version


# dict values are icon names from: https://fontawesome.com/icons
Expand All @@ -110,6 +112,11 @@
_BASE_MIN_SIZE = "20px"
_BASE_KWARGS = dict(layout=Layout(min_width=_BASE_MIN_SIZE, min_height=_BASE_MIN_SIZE))

if check_version("pyvista", "0.38"):
_JUPYTER_BACKEND = "trame"
else:
_JUPYTER_BACKEND = "ipyvtklink"

# %%
# Widgets
# -------
Expand Down Expand Up @@ -646,8 +653,8 @@ def _handle_scroll(self, scroll=None):
def _add_widget(self, widget):
# if pyvista plotter, needs to be shown
if isinstance(widget, Plotter):
widget = widget.show(jupyter_backend="ipyvtklink", return_viewer=True)
if hasattr(widget, "layout"):
widget = widget.show(jupyter_backend=_JUPYTER_BACKEND, return_viewer=True)
if hasattr("widget", "layout"):
widget.layout.width = None # unlock the fixed layout
widget.layout.margin = "2px 0px 2px 0px"
if not isinstance(widget, Play):
Expand Down Expand Up @@ -780,17 +787,12 @@ def __init__(self, *args, **kwargs):
if "show" in kwargs and kwargs["show"]:
self.show()

def _update(self):
if self.figure.display is not None:
self.figure.display.update_canvas()

@contextmanager
def _ensure_minimum_sizes(self):
yield

def show(self):
viewer = self.plotter.show(jupyter_backend="ipyvtklink", return_viewer=True)
viewer.layout.width = None # unlock the fixed layout
viewer = self.plotter.show(jupyter_backend=_JUPYTER_BACKEND, return_viewer=True)
display(viewer)


Expand Down Expand Up @@ -1247,8 +1249,8 @@ def _tool_bar_add_spacer(self):
pass

def _tool_bar_add_file_button(self, name, desc, func, *, shortcut=None):
def callback():
fname = self.actions[f"{name}_field"].value
def callback(name=name):
fname = self.actions[f"{name}_field"]._action.value
func(None if len(fname) == 0 else fname)

self._tool_bar_add_text(
Expand Down Expand Up @@ -1361,18 +1363,21 @@ def _window_initialize(self, *, window=None, central_layout=None, fullscreen=Fal

def _window_load_icons(self):
# from: https://fontawesome.com/icons
self._icons["help"] = "question"
for key in (
"help",
"reset",
"scale",
"clear",
"movie",
"restore",
"screenshot",
"visibility_on",
"visibility_off",
"folder",
): # noqa: E501
self._icons[key] = _ICON_LUT[key]
self._icons["play"] = None
self._icons["pause"] = None
self._icons["reset"] = "history"
self._icons["scale"] = "magic"
self._icons["clear"] = "trash"
self._icons["movie"] = "video-camera"
self._icons["restore"] = "replay"
self._icons["screenshot"] = "camera"
self._icons["visibility_on"] = "eye"
self._icons["visibility_off"] = "eye"
self._icons["folder"] = "folder"

def _window_close_connect(self, func, *, after=True):
pass
Expand Down Expand Up @@ -1542,10 +1547,6 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._window_initialize(fullscreen=fullscreen)

def _update(self):
if self.figure.display is not None:
self.figure.display.update_canvas()

def _display_default_tool_bar(self):
self._tool_bar_initialize()
self._tool_bar_add_file_button(
Expand All @@ -1565,8 +1566,15 @@ def show(self):
else:
self._display_default_tool_bar()
# viewer
viewer = self.plotter.show(jupyter_backend="ipyvtklink", return_viewer=True)
viewer.layout.width = None # unlock the fixed layout
viewer = self.plotter.show(jupyter_backend=_JUPYTER_BACKEND, return_viewer=True)
if _JUPYTER_BACKEND == "trame":
# Remove scrollbars
viewer.value = re.sub(
r" style=[\"'](.+)[\"']></iframe>",
# value taken from matplotlib's widget
r" style='\1; border: 1px solid rgb(221,221,221);' scrolling='no'></iframe>", # noqa: E501
viewer.value,
)
rendering_row = list()
if self._docks is not None and "left" in self._docks:
rendering_row.append(self._docks["left"][0])
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ mffpy>=0.5.7
ipywidgets
ipympl; platform_system!="Windows" # XXX: Add support when ipympl CIs include Windows
ipyevents
ipyvtklink
trame
trame-vtk
mne-qt-browser
darkdetect
qdarkstyle
Expand Down

0 comments on commit 41ba99d

Please sign in to comment.