FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add pyodide backend based on webagg by ianthomas23 · Pull Request #32148 · matplotlib/matplotlib · GitHub

Add pyodide backend based on webagg - #32148

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

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

Conversation

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 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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

makes sense, I'm 👍🏻 on this approach.

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I think this makes sense to do.

In the case of GUI backends it makes sense to advertise backends the user can not use due to missing dependencies so they learn they can install those dependencies to get the backend, but given that this only works in emscripten and will never work on a desktop, advertising it will just be annoying.

Comment thread pyproject.toml Outdated

[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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

agriyakhetarpal Aug 4, 2026
edited
Loading

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

If pyodide is the default backend in pyemscripten wheels, does anything on the JupyterLite side need to change (in pre-REPL or kernel code) such that the ipympl backend is always chosen there? Do they need to override this? Sorry for the naïve question; I'm just confirming that this doesn't break something.

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@agriyakhetarpal JupyterLite is fine with this.

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We could also (conditionally) put it at the top of the search order for the auto backend selection.

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?

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 added this to the v3.12.0 milestone Jul 31, 2026

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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 Outdated

[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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown

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.

I have an old PR, #29506, that I could revive if I have some time soon. It would connect the JupyterLite deployment to the docs via iframes, making the examples interactive.

agriyakhetarpal 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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Looks great to me. Thanks for upstreaming it, @ianthomas23!

In the medium term, I think some method of running the tests via Playwright and capturing their screenshots to compare (perhaps via @pytest.mark.mpl_image_compare) could be devised; Matplotlib's testing infrastructure be willing.

Copy link
Copy Markdown
Member Author

I am happy to write Playwright tests to cover web-based backends, but I think it unlikely that we (Matplotlib) want to add Playwright test infrastructure that every maintainer and new contributor has to use and understand.

I absolutely want to separate that discussion from this PR.

Copy link
Copy Markdown
Member

I am strongly 👍🏻 in principle and over all everything makes sense. Only comment is the right way to make this the default, but my suggestion (putting it at the top of the search order) may mess with jupyterlite.

@@ -0,0 +1,75 @@
// mpl.js is imported before this, here we override functions from that and define new functions.

class MockJsWebSocket {

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It looks like you may have to change the eslint configuration to accept a newer version of JavaScript for this file.

Also, opened #32183 to ensure that these kind of failures cause the CI job to fail.

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks for pointing this out. I've updated all the eslint and related config in commit 42483ad to the latest versions. This includes a necessary rename of the eslint config file, and a few linting changes that I have manually checked.

story645 pushed a commit that referenced this pull request Aug 9, 2026
In #32148, the eslint check posted errors, but the job did not fail.
This setting ensure that it _will_ fail when it posts errors.

QuLogic commented Aug 11, 2026

Copy link
Copy Markdown
Member

I am happy to write Playwright tests to cover web-based backends, but I think it unlikely that we (Matplotlib) want to add Playwright test infrastructure that every maintainer and new contributor has to use and understand.

See #23540

Copy link
Copy Markdown
Member Author

I am strongly 👍🏻 in principle and over all everything makes sense. Only comment is the right way to make this the default, but my suggestion (putting it at the top of the search order) may mess with jupyterlite.

I am trying setting the default via the backend search order, I need to check downstream if this causes any problems and I will report back.

Copy link
Copy Markdown
Member Author

I am trying setting the default via the backend search order, I need to check downstream if this causes any problems and I will report back.

The search order approach does not affect use JupyterLite's use of ipympl and matplotlib-inline backends.

clin1234 pushed a commit to clin1234/matplotlib that referenced this pull request Aug 23, 2026
In matplotlib#32148, the eslint check posted errors, but the job did not fail.
This setting ensure that it _will_ fail when it posts errors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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: nbagg GUI: webagg topic: pyplot API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants


Back | FazBrowse Home | New Git URL