Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Tagging a commit with the following overrides these behaviors:
# - [circle front] will run the front page examples and perform test-doc
# - [circle full] will run all examples and perform test-doc
# - [circle lite] will also build the JupyterLite site in a partial build
# - [circle linkcheck] will run our linkcheck job
# - [circle deploy] on a main or maint/* commit will try to immediately build
# and deploy docs rather than waiting for the nightly build
Expand Down
1 change: 1 addition & 0 deletions doc/changes/dev/14333.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``jupyterlite_notebook`` 3D backend was renamed ``notebook_js`` and now supports most 3D visualizations, by `Eric Larson`_.
7 changes: 4 additions & 3 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,8 @@

sphinx_gallery_parallel = int(os.getenv("MNE_DOC_BUILD_N_JOBS", "1"))
# The JupyterLite site, and the data it serves (about 1 GB), only belong in a
# full build: `make html` turns this on, pattern and noplot builds leave it off.
# full build: `make html` turns this on, pattern and noplot builds leave it off
# (unless CircleCI's [circle lite] tag sets it).
build_jupyterlite = os.getenv("MNE_DOC_BUILD_JUPYTERLITE", "0") == "1"
if build_jupyterlite:
from jupyterlite_data import stage_lite_data # noqa: E402
Expand Down Expand Up @@ -670,8 +671,8 @@
# plot_field needs the interactive viewer that the browser renderer skips
"examples/visualization/mne_helmet.py",
# Tier 4: mne.viz.Brain features the browser renderer lacks (see the TODOs
# in mne/viz/_brain/_brain.py). Brain itself draws (static, one time point,
# one view), and the fNIRS tutorials and 50_background_freesurfer_mne run
# in mne/viz/_brain/_brain.py). Brain itself draws (one view, with the time
# viewer), and the fNIRS tutorials and 50_background_freesurfer_mne run
# in full, but these lean on add_annotation's hover callback,
# brain.screenshot, legends, silhouettes or the flatmap, so most of their
# cells fail.
Expand Down
5 changes: 5 additions & 0 deletions doc/development/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,11 @@ down the road. Here are the guidelines:
nicely formatted) and the resulting output and figures will be rendered
as part of the tutorial/example.

- ``[circle lite]`` also builds the JupyterLite site in a partial
documentation build, so the "Open in JupyterLite" buttons work on the
tutorials/examples it renders (combine with ``[circle front]`` to
render the front page ones).

- Examples and tutorials should execute as quickly and with as low memory usage as
possible while still conveying necessary information. To see current execution
times and memory usage, visit the `sg_execution_times page`_. To see unused API
Expand Down
9 changes: 6 additions & 3 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ def browser_backend(request, garbage_collect, monkeypatch):
@pytest.fixture(
params=[
pytest.param("pyvistaqt", marks=pytest.mark.pvtest),
pytest.param("jupyterlite_notebook", marks=pytest.mark.pvtest),
pytest.param("notebook_js", marks=pytest.mark.pvtest),
]
)
def renderer(request, options_3d, garbage_collect):
Expand All @@ -755,7 +755,7 @@ def renderer_notebook(request, options_3d):
yield renderer


@pytest.fixture(params=[pytest.param("jupyterlite_notebook", marks=pytest.mark.pvtest)])
@pytest.fixture(params=[pytest.param("notebook_js", marks=pytest.mark.pvtest)])
def renderer_lite(request, options_3d):
"""Yield the JupyterLite (vtk.js) renderer alone, for its own tests."""
with _use_backend(request.param, interactive=False) as renderer:
Expand Down Expand Up @@ -815,9 +815,12 @@ def _use_backend(backend_name, interactive):
def _check_skip_backend(name):
from mne.viz.backends._utils import _notebook_vtk_works

if name == "jupyterlite_notebook":
if name == "notebook_js":
# draws with vtk.js in a browser: no VTK, no Qt, no ffmpeg
pytest.importorskip("pyvista_js")
# ... but the same ipywidgets GUI as the notebook backend
pytest.importorskip("ipympl")
pytest.importorskip("ipyevents")
return
pytest.importorskip("pyvista")
pytest.importorskip("imageio_ffmpeg")
Expand Down
2 changes: 1 addition & 1 deletion mne/gui/_coreg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,7 @@ def _update_fiducials_label(self):
self._forward_widget_command("mri_fiducials_label", "set_value", text)

def _configure_dock(self):
if self._renderer._kind == "notebook":
if self._renderer._kind.startswith("notebook"): # either notebook backend
collapse = True # collapsible and collapsed
else:
collapse = None # not collapsible
Expand Down
2 changes: 1 addition & 1 deletion mne/gui/_dipolefit.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ def __init__(
# window has actually appeared on screen (see `_qt_safe_window`).
self._renderer.figure.splash = self._splash
self._renderer.show()
if block and self._renderer._kind != "notebook":
if block and not self._renderer._kind.startswith("notebook"):
_qt_app_exec(self._renderer.figure.store["app"])

@property
Expand Down
5 changes: 1 addition & 4 deletions mne/viz/_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -2750,10 +2750,7 @@ def _check_st_tv(show_traces, time_viewer, times):
extra="when a string",
)
if time_viewer == "auto":
from .backends.renderer import _get_3d_backend

# the browser backend writes a static scene, so there is no slider to show
time_viewer = _get_3d_backend() != "jupyterlite_notebook"
time_viewer = True
if show_traces == "auto":
show_traces = time_viewer and times is not None and len(times) > 1
if show_traces and not time_viewer:
Expand Down
9 changes: 3 additions & 6 deletions mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,8 @@ def _on_time_change(self, event):
if event.time == self._current_time:
return
time_idx = self._to_time_index(event.time)
self._update_current_time_idx(time_idx)
self._update_current_time_idx(time_idx) # renders
publish(self, _TimeUpdated())
self._renderer._update()

def _on_colormap_range(self, event):
"""Respond to the colormap_range UI event."""
Expand All @@ -581,10 +580,8 @@ def _on_colormap_range(self, event):
# Check if limits have changed at all.
if all(val is None or val == self._data[key] for key, val in lims.items()):
return
# Update the render.
self._update_colormap_range(**lims)
self._update_colormap_range(**lims) # renders
publish(self, _ColormapRangeUpdated())
self._renderer._update()

def _clear_callbacks(self):
# Remove the default key binding
Expand Down Expand Up @@ -1212,7 +1209,7 @@ def _add_volume_data(self, hemi, src, volume_options):
scalars = np.zeros(np.prod(dimensions))
scalars[vertices] = 1.0 # for the outer mesh
# TODO: reaches into VTK through the renderer, which the
# jupyterlite_notebook backend cannot offer (its pages are excluded
# notebook_js backend cannot offer (its pages are excluded
# in doc/conf.py); refactor the renderer interface so Brain only
# uses its abstract methods. Same for the time label and glyph
# actors below.
Expand Down
14 changes: 10 additions & 4 deletions mne/viz/_brain/_time_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,13 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
self._active_data_key = next(iter(self._all_data))
self._configure_time_label()
self._configure_scalar_bar()
self._configure_shortcuts()
self._configure_picking()
self._configure_hover()
# keyboard shortcuts, picking and hover all need mouse and key events
# from the interactor, plus VTK actors (see the TODO in _add_volume_data),
# which a page drawn by the notebook_js backend has neither of
if self._renderer._kind != "notebook_js":
self._configure_shortcuts()
self._configure_picking()
self._configure_hover()
self._configure_dock()
self._configure_tool_bar()
self._configure_status_bar()
Expand Down Expand Up @@ -270,7 +274,9 @@ def current_time_func():
def _configure_dock_orientation_widget(self, name):
layout = self._renderer._dock_add_group_box(name, collapse=True)
# Renderer widget
rends = [str(i) for i in range(len(self._renderer._all_renderers))]
# one entry per subplot, which is what VTK's renderers are (asking the
# renderer for them would reach past its interface, see _add_volume_data)
rends = [str(i) for i in range(int(np.prod(self._subplot_shape)))]
if len(rends) > 1:

@_auto_weakref
Expand Down
12 changes: 10 additions & 2 deletions mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -2309,20 +2309,28 @@ def test_auto_scalar_bar_fmt():
def test_brain_ui_events(renderer_interactive_pyvistaqt, brain_gc):
"""Test responding to Brain related UI events."""
brain = _create_testing_brain(hemi="lh", show_traces="vertex")
# each event must render once: a notebook backend redraws the whole scene
renders, update = list(), brain._renderer._update
brain._renderer._update = lambda: (renders.append(1), update())

ui_events.publish(brain, ui_events.TimeChange(time=1))
assert brain._current_time == 1
new_time = brain._times[-1]
assert brain._current_time != new_time # so this is a change
ui_events.publish(brain, ui_events.TimeChange(time=new_time))
assert brain._current_time == new_time
assert len(renders) == 1

ui_events.publish(brain, ui_events.VertexSelect(hemi="lh", vertex_id=1))
assert 1 in brain.get_picked_points()["lh"]

n_renders = len(renders)
ui_events.publish(
brain,
ui_events.ColormapRange(
kind="distributed_source_power", fmin=1, fmid=2, fmax=3, alpha=True
),
)
assert_array_equal(brain._data["ctable"][:3, 3], [0, 2, 4])
assert len(renders) == n_renders + 1

# This event should be ignored.
ui_events.publish(
Expand Down
2 changes: 1 addition & 1 deletion mne/viz/backends/_jupyterlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def setup_notebook(data_path="/tmp/mne_data"): # nosec B108
from ...viz import set_3d_backend

try:
set_3d_backend("jupyterlite_notebook")
set_3d_backend("notebook_js")
except Exception as exc:
print(f"[JupyterLite] could not select the pyvista-js renderer: {exc!r}")

Expand Down
Loading
Loading