Skip to content

ENH: add a vtk.js backend for MNE's 3D renderer (JupyterLite split 3/5) - #14144

Open
natinew77-creator wants to merge 30 commits into
mne-tools:mainfrom
natinew77-creator:lite-browser-runtime
Open

ENH: add a vtk.js backend for MNE's 3D renderer (JupyterLite split 3/5)#14144
natinew77-creator wants to merge 30 commits into
mne-tools:mainfrom
natinew77-creator:lite-browser-runtime

Conversation

@natinew77-creator

@natinew77-creator natinew77-creator commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Part 3 of the split of #13925. Parts 1 and 2 are #14128 and #14135.

Adds a drawing backend for MNE's 3D renderer that uses vtk.js, since VTK itself cannot load in WebAssembly. MNE's 3D functions do their geometry and coordinate-frame work in numpy and only hand the result to a renderer, so replacing that last step leaves the transform maths with MNE. That matters here, because a subtly wrong head or device transform still produces a plausible-looking picture.

Supported: meshes, surfaces, spheres, tubes and glyphs, which covers the static figures the docs render. Not supported: the interactive Brain time viewer, which needs dock widgets, and scalar colormaps, which pyvista-js 0.15 does not have.

It is a plain string constant, so nothing in the build touches it yet. The setup cell that appends it is #14150.

VTK cannot load in WebAssembly, so the JupyterLite notebooks need a renderer
that draws with vtk.js. MNE does its geometry in numpy and only hands the result
to a renderer, so replacing that last step leaves the transform maths to MNE.
@natinew77-creator natinew77-creator changed the title ENH: add the JupyterLite browser runtime (JupyterLite split 3/4) ENH: add a vtk.js backend for MNE's 3D renderer (JupyterLite split 3/5) Aug 11, 2026
@larsoner

Copy link
Copy Markdown
Member

Maybe you have looked and I am coming late to this... have you thought about adding it as a type of AbstractRenderer and using https://github.com/tkoyama010/pyvista-js ? Maybe it's not too much work to use that in place of our PyVista calls... but if you've tried or looked I could be way off!

@natinew77-creator

Copy link
Copy Markdown
Contributor Author

Maybe you have looked and I am coming late to this... have you thought about adding it as a type of AbstractRenderer and using https://github.com/tkoyama010/pyvista-js ? Maybe it's not too much work to use that in place of our PyVista calls... but if you've tried or looked I could be way off!

Hi Eric, it already uses pyvista-js.

On AbstractRenderer, it sits in doc/ as a string the setup cell appends, to keep browser-only code out of mne/. But it already implements 21 of the 22 abstract methods, so converting it is mostly moving and registering it, not a rewrite.

Happy to do that here, or land this as is and convert in a follow-up. Which would you prefer?

Thanks!

@larsoner

Copy link
Copy Markdown
Member

Yeah if there is some way for it to be a plain renderer and then LITE_RENDERER_CELL can be built from the source rather than being a huge string that would be better. The huge string doesn't have Python linting etc. built in which is a real drawback. It would also maybe facilitate eventually adding selenium-based unit tests for the renderer or something. But maybe @teonbrooks has ideas!

natinew77-creator and others added 2 commits August 17, 2026 16:52
The renderer was a 560-line string literal, which no linter or formatter
could see. It now lives in _lite_renderer_cell.py as ordinary Python and
the cell is read from there, so ruff covers it like any other file. The
code itself is unchanged apart from what the formatter did to it.
@natinew77-creator

Copy link
Copy Markdown
Contributor Author

Yeah if there is some way for it to be a plain renderer and then LITE_RENDERER_CELL can be built from the source rather than being a huge string that would be better. The huge string doesn't have Python linting etc. built in which is a real drawback. It would also maybe facilitate eventually adding selenium-based unit tests for the renderer or something. But maybe @teonbrooks has ideas!

Done in eb3c814, it's a plain module now and LITE_RENDERER_CELL is read from it, so ruff and the formatter cover it.

@larsoner

Copy link
Copy Markdown
Member

I think it's probably best to move this to the mne/viz/backends/_<something>.py and test it. The testing will be a bit tricky... could we use a Jupyter notebook-style test like we have already? If it requires a proper browser, maybe selenium or something... @drammock could maybe advise based on what pydata-sphinx-theme has.

I don't want to add 700 lines with no unit tests, things are bound to break / or be broken...

@natinew77-creator

Copy link
Copy Markdown
Contributor Author

I think it's probably best to move this to the mne/viz/backends/_<something>.py and test it. The testing will be a bit tricky... could we use a Jupyter notebook-style test like we have already? If it requires a proper browser, maybe selenium or something... @drammock could maybe advise based on what pydata-sphinx-theme has.

I don't want to add 700 lines with no unit tests, things are bound to break / or be broken...

Done, moved to mne/viz/backends/_lite.py as a real _AbstractRenderer subclass. That turned up a bug, it reported a _kind that isn't "notebook", sending _coreg.py into _qt_app_exec with no Qt event loop in a browser.

pyvista-js is pure Python, so no selenium, 18 pytest cases plus one nbexec test. It ships in the wheel now too, so the setup cell drops from ~24,500 characters to 438.

I didn't register it in VALID_3D_BACKENDS, since that pulls in the 17 widget classes _do_widget_tests expects. Happy to if you meant that.

Comment thread mne/viz/backends/tests/test_utils.py
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/tests/test_lite.py Outdated
Comment thread mne/viz/backends/tests/test_lite.py Outdated
Comment thread mne/viz/backends/tests/test_lite.py Outdated
Comment thread mne/viz/backends/tests/test_lite.py Outdated
Comment thread mne/viz/backends/tests/test_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment on lines +560 to +562
def _enable_time_interaction(self, *args, **kwargs):
# the figures are static here; there is no time slider to wire up
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some things like this you might get for free if this inherited from the _TimeInteraction mixin class, and there are other mixin classes that might give you lots of other interactivity for free. See, e.g.,

class _Renderer(
_PyVistaRenderer,
_IpyDock,
_IpyToolBar,
_IpyMenuBar,
_IpyStatusBar,
_IpyWindow,
_IpyPlayback,
_IpyDialog,
_IpyKeyPress,
_TimeInteraction,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs _dock_add_slider and the rest of the dock API, and the _Ipy* mixins are unreachable because _notebook.py imports _pyvista at module level, so this raises now instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's too bad. @larsoner would it be worth trying to refactor _notebook.py (to avoid module-level pyvista import) to see if those mixins could be reused here? (In a follow up I guess)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah in a follow-up moving all the ipy* into a _notebook_base.py or similar then importing two places would make sense. It would make _notebook.py very short I think, and make clear what is "ipywidgets vs qt abstraction" and what is actually related to 3D renderering.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That split looks clean, none of the _Ipy* classes touch _pyvista, so _notebook_base.py would need only ipywidgets and _abstract. Happy to open the follow-up.

Comment thread mne/viz/backends/tests/test_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
_mne_rend.backend = _LiteBackend()
# Naming a backend keeps _get_3d_backend() from walking VALID_3D_BACKENDS and
# importing _qt, which would overwrite the stub above on its way to failing.
_mne_rend.MNE_3D_BACKEND = "notebook"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_mne_rend.MNE_3D_BACKEND = "notebook"
_mne_rend.MNE_3D_BACKEND = "jupyterlite_notebook"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, It registers as jupyterlite_notebook now (16d3418), so MNE_3D_BACKEND comes from set_3d_backend instead of being set by hand.

Replaces the _activate monkeypatching with a real "jupyterlite" backend so it
works with the pytest renderer fixtures. Also fixes the glyph templates, which
drew spheres at twice their size and EEG cylinders off their own axis.
@drammock

Copy link
Copy Markdown
Member

Thanks for the quick response @natinew77-creator. I only have phone at the moment but will look again tomorrow morning

Matches what the review asked for and lines the backend name up with _kind.
The capability table goes back to upstream's three columns, with the browser
backend described in the notes instead.
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/_lite.py Outdated
Comment thread mne/viz/backends/tests/test_lite.py Outdated
Comment thread mne/viz/backends/tests/test_lite.py
It was handing back only the last color group and dropping the rest.
Drops a wrong claim about plot_bem, makes the drawing tests assert geometry
rather than actor counts, and stops accepting arguments without saying why
they cannot be honoured.
mne-toolsgh-13074 made mne/viz/_3d.py write channel names onto the second value
instanced_mesh returns, which broke plot_alignment here three ways: a
pyvista-js PolyData has no field_data, several colours came back as a
list, and empty positions came back as None. Return the per-instance
point cloud instead, always one object with the mapping attached, which
is what _PyVistaRenderer returns.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants