Add pyodide backend based on webagg - #32148
Conversation
|
|
||
| @classmethod | ||
| def get_javascript(cls, stream=None): | ||
| def get_javascript(cls, stream=None, *, pyodide=False): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| [tool.cibuildwheel.pyodide] | ||
| config-settings.setup-args = [ | ||
| "-DrcParams-backend=pyodide" |
There was a problem hiding this comment.
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')There was a problem hiding this comment.
Yeah, it would be great to make this a default so that users don't need to explicitly set the backend.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@agriyakhetarpal JupyterLite is fine with this.
There was a problem hiding this comment.
We could also (conditionally) put it at the top of the search order for the auto backend selection.
|
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. |
ryanking13
left a comment
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
|
|
||
| [tool.cibuildwheel.pyodide] | ||
| config-settings.setup-args = [ | ||
| "-DrcParams-backend=pyodide" |
There was a problem hiding this comment.
Yeah, it would be great to make this a default so that users don't need to explicitly set the backend.
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
There was a problem hiding this comment.
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.
|
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. |
|
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 { | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
In #32148, the eslint check posted errors, but the job did not fail. This setting ensure that it _will_ fail when it posts errors.
See #23540 |
207c084 to
f48a20e
Compare
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. |
8be21cb to
1bde8ee
Compare
The search order approach does not affect use JupyterLite's use of |
PR summary
This PR adds an interactive
pyodidebackend to the core Matplotlib code which is based onbackend_webagg_corein a similar way to the existingwebaggandipymplbackends. 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 interactiveipymplbackend 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-pyodiderepo 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 onwebagg.Alternatives to this PR:
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.Details
It is based on
webagg_corewhich remains fully backward-compatible with thewebaggandipymplbackends. The implementation followswebaggas much as possible. Inwebaggtornadois used on the Python side to serve resources (JavaScript, CSS, etc) to the JavaScript side in the browser, and they communicate via websockets. Inpyodidethe 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
webaggtests, for this.Pinging pyodide devs @agriyakhetarpal, @hoodmane and @ryanking13 for awareness.
AI Disclosure
No AI used.
PR quality check