Skip to content

Add pyodide backend based on webagg - #32148

Open
ianthomas23 wants to merge 3 commits into
matplotlib:mainfrom
ianthomas23:pyodide-backend
Open

Add pyodide backend based on webagg#32148
ianthomas23 wants to merge 3 commits into
matplotlib:mainfrom
ianthomas23:pyodide-backend

Conversation

@ianthomas23

Copy link
Copy Markdown
Member

PR summary

This PR adds an interactive pyodide backend to the core Matplotlib code which is based on backend_webagg_core in a similar way to the existing webagg and ipympl backends. It has been patched into the Pyodide build of Matplotlib in pyodide-recipes for about a year and a half, but as we are planning to build and upload our own Pyodide wheels to PyPI the backend needs to be part of the core repo here or the functionality will no longer be available. Note that this is for use of Pyodide outside of JupyterLite, as within JupyterLite one would use the interactive ipympl backend as usual.

Because it is not trivial to build and try out Pyodide wheels I have prepared a repo at https://github.com/ianthomas23/pyodide-wheels that uses wheels built from this branch and allows you to try it out for both Python 3.13 and 3.14 in a web browser of your choice. Also here is a screencast of it in action:

pyodide-backend.mp4

Historically a backend similar to this has sat in its own matplotlib-pyodide repo but it became awkward to maintain and was likely to disappear so I stepped in as I was aware that there could be a solution mostly relying on webagg.

Alternatives to this PR:

  • Put it in its own repo. A very reasonable request, but it is likely to end up with the same fate as matplotlib-pyodide. To survive in a standalone repo it would need the solid commitment of a maintainer for say 10 years, and I have no interest in that but I am happy to maintain it as part of the core codebase here.
  • Don't have it anywhere. This would annoy existing Pyodide users.

Details

It is based on webagg_core which remains fully backward-compatible with the webagg and ipympl backends. The implementation follows webagg as much as possible. In webagg tornado is used on the Python side to serve resources (JavaScript, CSS, etc) to the JavaScript side in the browser, and they communicate via websockets. In pyodide the Python and JS code sit side-by-side and can essentially call each other directly (via pyodide converter shims really) and this code uses a mock websocket class to keep the code changes as small as possible.

There is no testing yet. When we have reliable testing of wasm wheels I can add extra tests, similar to the webagg tests, for this.

Pinging pyodide devs @agriyakhetarpal, @hoodmane and @ryanking13 for awareness.

AI Disclosure

No AI used.

PR quality check

  • Use an expressive title, e.g. "Fix title font property precedence"
  • New and changed code is tested
  • Plotting related features are demonstrated in an example
  • New features and API changes have release notes
  • Documentation complies with general and docstring guidelines

@github-actions github-actions Bot added GUI: webagg CI: Run cibuildwheel Run wheel building tests on a PR Documentation: user guide files in galleries/users_explain or doc/users labels Jul 31, 2026

@classmethod
def get_javascript(cls, stream=None):
def get_javascript(cls, stream=None, *, pyodide=False):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The extra kwarg here isn't elegant, but it keeps backward compatibility with minimal code changes.

Alternatives would be to reimplement this entirely in backend_pyodide to keep it unchanged here, but that would be quite a lot of code duplication. Or this function could call a number of other shorter functions and backend_pyodide could just override the 2 that it needs to.

output.write((Path(__file__).parent / "web_backend/js/mpl.js")
.read_text(encoding="utf-8"))
if pyodide:
output.write((Path(__file__).parent / "web_backend/js/mpl_pyodide.js")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Here we have already loaded the default mpl.js JavaScript code into the browser page, and then loading mpl_pyodide.js afterwards adds some new code and replaces some of the previous functions.

}

if sys.platform == 'emscripten':
self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK["pyodide"] = "pyodide"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Here I am only advertising the existence of the pyodide backend if we are running on emscripten. The alternative would be to always have it present in the list of available backends even when it cannot be used.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't know about how other backends (e.g. wegagg) are advertised, but it makes sense to me as people who try to use pyodide-backend in non-emscripten environment would get runtime error.

Comment thread pyproject.toml

[tool.cibuildwheel.pyodide]
config-settings.setup-args = [
"-DrcParams-backend=pyodide"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Here setting pyodide to be the default backend used in pyodide wheels. This is consistent with the current patch in the pyodide matplotlib build. Alternatively we could avoid this and stick with agg as the default backend, and explain to Pyodide users about

import matplotlib as mpl
mpl.use('pyodide')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah, it would be great to make this a default so that users don't need to explicitly set the backend.

@story645

Copy link
Copy Markdown
Member

If this is in core, could it be used as part of the doc builds to make the widgets examples interactive?

@ianthomas23

Copy link
Copy Markdown
Member Author

If this is in core, could it be used as part of the doc builds to make the widgets examples interactive?

I suppose so, but it would need some infrastructure so that each user only has a single pyodide instance running rather than one per plot.

But I think that jupyterlite-sphinx is the go-to project for this, and being JupyterLite-based the python instance runs in a separate thread which gives a better UX. Although at a quick glance I see all the examples produce static plots not interactive, so it would need some checking to see what is currently possible.

@tacaswell tacaswell added this to the v3.12.0 milestone Jul 31, 2026

@ryanking13 ryanking13 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for upstreaming this!

There is no testing yet. When we have reliable testing of wasm wheels I can add extra tests, similar to the webagg tests, for this.

Yeah, it is slightly tricky to test this as it requires a real browser to test.

I think you can test some part of the features that does not requires DOM (document object) using cibuildwheel by adding

[tool.cibuildwheel.pyodide.environment]
test-command = "python -m pytest <test files>"

similar to how scipy is doing. But I guess there will be a lot of tests that need to be skipped.

}

if sys.platform == 'emscripten':
self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK["pyodide"] = "pyodide"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't know about how other backends (e.g. wegagg) are advertised, but it makes sense to me as people who try to use pyodide-backend in non-emscripten environment would get runtime error.

Comment thread pyproject.toml

[tool.cibuildwheel.pyodide]
config-settings.setup-args = [
"-DrcParams-backend=pyodide"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah, it would be great to make this a default so that users don't need to explicitly set the backend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI: Run cibuildwheel Run wheel building tests on a PR Documentation: user guide files in galleries/users_explain or doc/users GUI: webagg

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants