From 61935f72718c0754a9b94e1569a685ad3c50ae91 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Sat, 25 Nov 2023 07:10:45 +0900 Subject: [PATCH 1/8] Update pypa/build version used in test (#45) --- pyproject.toml | 2 +- tests/conftest.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2bc32e0..7673193 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ build-backend = "setuptools.build_meta" test = [ "pytest-pyodide==0.52.2", "pytest-cov", - "build==0.10", + "build>=1.0", ] diff --git a/tests/conftest.py b/tests/conftest.py index 820c14e..75d5b7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,14 +20,15 @@ def matplotlib_test_decorator(f): def wheel_path(tmp_path_factory): # Build a micropip wheel for testing import build - from build.env import IsolatedEnvBuilder + from build.env import DefaultIsolatedEnv output_dir = tmp_path_factory.mktemp("wheel") - with IsolatedEnvBuilder() as env: - builder = build.ProjectBuilder(Path(__file__).parent.parent) - builder.python_executable = env.executable - builder.scripts_dir = env.scripts_dir + with DefaultIsolatedEnv() as env: + builder = build.ProjectBuilder( + source_dir=Path(__file__).parent.parent, + python_executable=env.python_executable, + ) env.install(builder.build_system_requires) builder.build("wheel", output_directory=output_dir) From df6c97b5370fd86eeb74c54ce4c53bb0183b9c89 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 4 Mar 2024 18:45:45 +0900 Subject: [PATCH 2/8] Update pyodide and package versions in CI (#56) --- .github/workflows/main.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bb7bf71..ee406b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - pyodide-version: [0.24.1] + pyodide-version: [0.25.0] test-config: [ {runner: selenium, runtime: chrome, runtime-version: latest}, ] diff --git a/pyproject.toml b/pyproject.toml index 7673193..27ee5d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "setuptools.build_meta" [project.optional-dependencies] test = [ - "pytest-pyodide==0.52.2", + "pytest-pyodide==0.56.2", "pytest-cov", "build>=1.0", ] From ff8cccf5d33b0a7f330eb1fd0e46a668a01401ea Mon Sep 17 00:00:00 2001 From: yu0A <111437058+yu0A@users.noreply.github.com> Date: Thu, 7 Mar 2024 19:48:31 +0800 Subject: [PATCH 3/8] Add FigureCanvasWasm.destroy() method so that user can call pyplot.close() method to delete previous divs (#55) * fix bug #49 * add test for #55 * update the CHANGELOG.md * add tests for #55 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: yuhongbo Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 5 +++ matplotlib_pyodide/browser_backend.py | 7 ++++ matplotlib_pyodide/html5_canvas_backend.py | 9 ++++++ matplotlib_pyodide/wasm_backend.py | 9 ++++++ tests/test_matplotlib.py | 37 ++++++++++++++++++++++ 5 files changed, 67 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b838a..697fc55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.2] - 2024-03-04 +### Fixed + - Add FigureCanvasWasm.destroy() method so that user can call pyplot.close() method to delete previous divs + ([#55](https://github.com/pyodide/matplotlib-pyodide/pull/55)) + ## [0.2.1] - 2023-10-06 ### Fixed - Improved support for matplotlib canvas methods diff --git a/matplotlib_pyodide/browser_backend.py b/matplotlib_pyodide/browser_backend.py index bf4fc88..a135846 100644 --- a/matplotlib_pyodide/browser_backend.py +++ b/matplotlib_pyodide/browser_backend.py @@ -190,6 +190,13 @@ def ignore(event): self.draw() + def destroy(self, *args, **kwargs): + div = document.getElementById(self._id) + parentElement = div.parentNode + if parentElement: + parentElement.removeChild(div) + div.removeChild(div.firstChild) + def draw(self): pass diff --git a/matplotlib_pyodide/html5_canvas_backend.py b/matplotlib_pyodide/html5_canvas_backend.py index 1cb7e36..740e679 100644 --- a/matplotlib_pyodide/html5_canvas_backend.py +++ b/matplotlib_pyodide/html5_canvas_backend.py @@ -431,6 +431,9 @@ def __init__(self, canvas, num): def show(self, *args, **kwargs): self.canvas.show(*args, **kwargs) + def destroy(self, *args, **kwargs): + self.canvas.destroy(*args, **kwargs) + def resize(self, w, h): pass @@ -448,3 +451,9 @@ def show(*args, **kwargs): from matplotlib import pyplot as plt plt.gcf().canvas.show(*args, **kwargs) + + @staticmethod + def destroy(*args, **kwargs): + from matplotlib import pyplot as plt + + plt.gcf().canvas.destroy(*args, **kwargs) diff --git a/matplotlib_pyodide/wasm_backend.py b/matplotlib_pyodide/wasm_backend.py index adb2cd6..059efc2 100644 --- a/matplotlib_pyodide/wasm_backend.py +++ b/matplotlib_pyodide/wasm_backend.py @@ -95,6 +95,9 @@ def __init__(self, canvas, num): def show(self, *args, **kwargs): self.canvas.show(*args, **kwargs) + def destroy(self, *args, **kwargs): + self.canvas.destroy(*args, **kwargs) + def resize(self, w, h): pass @@ -112,3 +115,9 @@ def show(*args, **kwargs): from matplotlib import pyplot as plt plt.gcf().canvas.show(*args, **kwargs) + + @staticmethod + def destroy(*args, **kwargs): + from matplotlib import pyplot as plt + + plt.gcf().canvas.destroy(*args, **kwargs) diff --git a/tests/test_matplotlib.py b/tests/test_matplotlib.py index 2f129ad..eab7aff 100644 --- a/tests/test_matplotlib.py +++ b/tests/test_matplotlib.py @@ -83,3 +83,40 @@ def test_font_manager(selenium_standalone_matplotlib): fontlist[list].sort(key=lambda x: x["fname"]) assert fontlist_built == fontlist_vendor + + +@matplotlib_test_decorator +@run_in_pyodide(packages=["matplotlib"]) +def test_destroy(selenium_standalone_matplotlib): + from matplotlib import pyplot as plt + + plt.figure() + plt.plot([1, 2, 3]) + plt.show() + plt.close() + + +@matplotlib_test_decorator +@run_in_pyodide(packages=["matplotlib"]) +def test_call_close_multi_times(selenium_standalone_matplotlib): + from matplotlib import pyplot as plt + + plt.figure() + plt.plot([1, 2, 3]) + plt.show() + plt.close() + plt.close() + + +@matplotlib_test_decorator +@run_in_pyodide(packages=["matplotlib"]) +def test_call_show_and_close_multi_times(selenium_standalone_matplotlib): + from matplotlib import pyplot as plt + + plt.figure() + plt.plot([1, 2, 3]) + plt.show() + plt.close() + plt.plot([1, 2, 3]) + plt.show() + plt.close() From 18cd638fb0b70078c0e6bedc4a98331ac7652f33 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 22 Oct 2024 14:27:43 +0200 Subject: [PATCH 4/8] Replace maxdict with lru_cache (#52) Matplotlib removed maxdict: https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.6.0.html#miscellaneous-internals --- matplotlib_pyodide/html5_canvas_backend.py | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/matplotlib_pyodide/html5_canvas_backend.py b/matplotlib_pyodide/html5_canvas_backend.py index 740e679..ac3afd5 100644 --- a/matplotlib_pyodide/html5_canvas_backend.py +++ b/matplotlib_pyodide/html5_canvas_backend.py @@ -1,6 +1,7 @@ import base64 import io import math +from functools import lru_cache import numpy as np from matplotlib import __version__, interactive @@ -10,7 +11,6 @@ RendererBase, _Backend, ) -from matplotlib.cbook import maxdict from matplotlib.colors import colorConverter, rgb2hex from matplotlib.font_manager import findfont from matplotlib.ft2font import LOAD_NO_HINTING, FT2Font @@ -204,8 +204,8 @@ def __init__(self, ctx, width, height, dpi, fig): self.ctx.width = self.width self.ctx.height = self.height self.dpi = dpi - self.fontd = maxdict(50) self.mathtext_parser = MathTextParser("bitmap") + self._get_font_helper = lru_cache(maxsize=50)(self._get_font_helper) # Keep the state of fontfaces that are loading self.fonts_loading = {} @@ -309,22 +309,22 @@ def draw_image(self, gc, x, y, im, transform=None): pixels_proxy.destroy() pixels_buf.release() + def _get_font_helper(self, prop): + """Cached font lookup + + We wrap this in an lru-cache in the constructor. + """ + fname = findfont(prop) + font = FT2Font(str(fname)) + font_file_name = fname.rpartition("/")[-1] + return (font, font_file_name) + def _get_font(self, prop): - key = hash(prop) - font_value = self.fontd.get(key) - if font_value is None: - fname = findfont(prop) - font_value = self.fontd.get(fname) - if font_value is None: - font = FT2Font(str(fname)) - font_file_name = fname[fname.rfind("/") + 1 :] - font_value = font, font_file_name - self.fontd[fname] = font_value - self.fontd[key] = font_value - font, font_file_name = font_value + result = self._get_font_helper(prop) + font = result[0] font.clear() font.set_size(prop.get_size_in_points(), self.dpi) - return font, font_file_name + return result def get_text_width_height_descent(self, s, prop, ismath): w: float From cb42e35b08999a0c5618c27d0a6ee0afb9e0369f Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:41:54 +0530 Subject: [PATCH 5/8] Bump Pyodide version, Python version, and other cleanups (#61) * Reformat * Require 3.12, bump setuptools, remove wheel * Require Python 3.12 for Mypy * Bump Pyodide version, Python, and GHA versions * Coloured output for all tests * Don't shallow-clone repository * Pin to Chrome 125 and bump install-browser action Co-Authored-By: Gyeongjae Choi --------- Co-authored-by: Gyeongjae Choi --- .github/workflows/main.yml | 24 +++++++++++++++--------- pyproject.toml | 14 +++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee406b9..ac220bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,9 @@ name: main on: [push, pull_request] +env: + FORCE_COLOR: 3 + permissions: contents: read @@ -18,24 +21,27 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - pyodide-version: [0.25.0] + pyodide-version: [0.26.4] test-config: [ - {runner: selenium, runtime: chrome, runtime-version: latest}, + # FIXME: timeouts on recent versions of Chrome, same as micropip + {runner: selenium, runtime: chrome, runtime-version: 125}, ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.12 - uses: pyodide/pyodide-actions/download-pyodide@v1 with: version: ${{ matrix.pyodide-version }} to: dist - - uses: pyodide/pyodide-actions/install-browser@v1 + - uses: pyodide/pyodide-actions/install-browser@v2 with: runner: ${{ matrix.test-config.runner }} browser: ${{ matrix.test-config.runtime }} @@ -63,11 +69,11 @@ jobs: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') environment: PyPi-deploy steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.12 - name: Install requirements and build wheel shell: bash -l {0} run: | diff --git a/pyproject.toml b/pyproject.toml index 27ee5d6..49284ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,17 +5,17 @@ authors = [ ] description = "HTML5 backends for Matplotlib compatible with Pyodide" readme = "README.md" -license = { file="LICENSE" } -requires-python = ">=3.10" +license = { file = "LICENSE" } +requires-python = ">=3.12" dynamic = ["version"] classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", - "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Operating System :: OS Independent", ] [build-system] -requires = ["setuptools>=42", "setuptools_scm[toml]>=6.2", "wheel"] +requires = ["setuptools>=71", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" @@ -47,7 +47,7 @@ known_first_party = [ ] [tool.mypy] -python_version = "3.10" +python_version = "3.12" show_error_codes = true warn_unreachable = true ignore_missing_imports = true From 90638fad1feebd2d68b9a5d6e8064512adbd3941 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:58:53 +0530 Subject: [PATCH 6/8] Add Firefox for testing (#62) * Add Firefox for testing * Remove xfail marker from `test_draw_math_text` because it passes now. --- .github/workflows/main.yml | 12 +++++++----- tests/test_html5_canvas_backend.py | 4 ---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac220bb..db409bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ permissions: contents: read concurrency: - group: main-${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: @@ -22,10 +22,12 @@ jobs: matrix: os: [ubuntu-latest] pyodide-version: [0.26.4] - test-config: [ - # FIXME: timeouts on recent versions of Chrome, same as micropip - {runner: selenium, runtime: chrome, runtime-version: 125}, - ] + test-config: + [ + # FIXME: timeouts on recent versions of Chrome, same as micropip + { runner: selenium, runtime: chrome, runtime-version: 125 }, + { runner: selenium, runtime: firefox, runtime-version: latest }, + ] steps: - uses: actions/checkout@v4 diff --git a/tests/test_html5_canvas_backend.py b/tests/test_html5_canvas_backend.py index fb76932..99a341a 100644 --- a/tests/test_html5_canvas_backend.py +++ b/tests/test_html5_canvas_backend.py @@ -1,7 +1,6 @@ import base64 import pathlib -import pytest from conftest import matplotlib_test_decorator from pytest_pyodide import run_in_pyodide @@ -277,9 +276,6 @@ def run(selenium, handle, ref): @matplotlib_test_decorator -@pytest.mark.xfail( - reason="TODO: pytest_pyodide.pyodide.JsException: InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable" -) def test_draw_math_text(selenium_standalone_matplotlib): selenium = selenium_standalone_matplotlib From 3aaa9a2c8a8678e5a91e1fece69db6bdd02f35ea Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:03:49 +0530 Subject: [PATCH 7/8] Matplotlib 3.8.4 update: restore WASM backend and partially restore HTML5 Canvas backend (#64) * Ignore build-related files * Check existence of toolbar * Temporarily disable event listeners, add notes * Use path-based MathTextParser to render math text * Add note about current state of HTML5 backend * Redirect HTMLCanvas backend to WASM backend --------- Co-authored-by: Gyeongjae Choi --- .gitignore | 3 + matplotlib_pyodide/browser_backend.py | 25 ++- matplotlib_pyodide/html5_canvas_backend.py | 232 ++++++++++++++++++--- 3 files changed, 217 insertions(+), 43 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..985736f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Build-related files +/build +/matplotlib_pyodide.egg-info diff --git a/matplotlib_pyodide/browser_backend.py b/matplotlib_pyodide/browser_backend.py index a135846..be24a5a 100644 --- a/matplotlib_pyodide/browser_backend.py +++ b/matplotlib_pyodide/browser_backend.py @@ -164,13 +164,15 @@ def ignore(event): rubberband.setAttribute("tabindex", "0") # Event handlers are added to the canvas "on top", even though most of # the activity happens in the canvas below. - add_event_listener(rubberband, "mousemove", self.onmousemove) - add_event_listener(rubberband, "mouseup", self.onmouseup) - add_event_listener(rubberband, "mousedown", self.onmousedown) - add_event_listener(rubberband, "mouseenter", self.onmouseenter) - add_event_listener(rubberband, "mouseleave", self.onmouseleave) - add_event_listener(rubberband, "keyup", self.onkeyup) - add_event_listener(rubberband, "keydown", self.onkeydown) + # TODO: with 0.2.3, we temporarily disable event listeners for the rubberband canvas. + # This shall be revisited in a future release. + # add_event_listener(rubberband, "mousemove", self.onmousemove) + # add_event_listener(rubberband, "mouseup", self.onmouseup) + # add_event_listener(rubberband, "mousedown", self.onmousedown) + # add_event_listener(rubberband, "mouseenter", self.onmouseenter) + # add_event_listener(rubberband, "mouseleave", self.onmouseleave) + # add_event_listener(rubberband, "keyup", self.onkeyup) + # add_event_listener(rubberband, "keydown", self.onkeydown) context = rubberband.getContext("2d") context.strokeStyle = "#000000" context.setLineDash([2, 2]) @@ -180,8 +182,13 @@ def ignore(event): # The bottom bar, with toolbar and message display bottom = document.createElement("div") - toolbar = self.toolbar.get_element() - bottom.appendChild(toolbar) + + # Check if toolbar exists before trying to get its element + # c.f. https://github.com/pyodide/pyodide/pull/4510 + if self.toolbar is not None: + toolbar = self.toolbar.get_element() + bottom.appendChild(toolbar) + message = document.createElement("div") message.id = self._id + "message" message.setAttribute("style", "min-height: 1.5em") diff --git a/matplotlib_pyodide/html5_canvas_backend.py b/matplotlib_pyodide/html5_canvas_backend.py index ac3afd5..91b723f 100644 --- a/matplotlib_pyodide/html5_canvas_backend.py +++ b/matplotlib_pyodide/html5_canvas_backend.py @@ -1,16 +1,40 @@ +# +# HTMl5 Canvas backend for Matplotlib to use when running Matplotlib in Pyodide, first +# introduced via a Google Summer of Code 2019 project: +# https://summerofcode.withgoogle.com/archive/2019/projects/4683094261497856 +# +# Associated blog post: +# https://blog.pyodide.org/posts/canvas-renderer-matplotlib-in-pyodide +# +# TODO: As of release 0.2.3, this backend is not yet fully functional following +# an update from Matplotlib 3.5.2 to 3.8.4 in Pyodide in-tree, please refer to +# https://github.com/pyodide/pyodide/pull/4510. +# +# This backend has been redirected to use the WASM backend in the meantime, which +# is now fully functional. The source code for the HTML5 Canvas backend is still +# available in this file, and shall be updated to work in a future release. +# +# Readers are advised to look at https://github.com/pyodide/matplotlib-pyodide/issues/64 +# and at https://github.com/pyodide/matplotlib-pyodide/pull/65 for information +# around the status of this backend and on how to contribute to its restoration +# for future releases. Thank you! + import base64 import io import math from functools import lru_cache +import matplotlib.pyplot as plt import numpy as np -from matplotlib import __version__, interactive +from matplotlib import __version__, figure, interactive +from matplotlib._enums import CapStyle from matplotlib.backend_bases import ( FigureManagerBase, GraphicsContextBase, RendererBase, _Backend, ) +from matplotlib.backends import backend_agg from matplotlib.colors import colorConverter, rgb2hex from matplotlib.font_manager import findfont from matplotlib.ft2font import LOAD_NO_HINTING, FT2Font @@ -20,7 +44,9 @@ from PIL import Image from PIL.PngImagePlugin import PngInfo +# Redirect to the WASM backend from matplotlib_pyodide.browser_backend import FigureCanvasWasm, NavigationToolbar2Wasm +from matplotlib_pyodide.wasm_backend import FigureCanvasAggWasm, FigureManagerAggWasm try: from js import FontFace, ImageData, document @@ -28,6 +54,7 @@ raise ImportError( "html5_canvas_backend is only supported in the browser in the main thread" ) from err + from pyodide.ffi import create_proxy _capstyle_d = {"projecting": "square", "butt": "butt", "round": "round"} @@ -144,12 +171,31 @@ def restore(self): self.renderer.ctx.restore() def set_capstyle(self, cs): + """ + Set the cap style for lines in the graphics context. + + Parameters + ---------- + cs : CapStyle or str + The cap style to use. Can be a CapStyle enum value or a string + that can be converted to a CapStyle. + """ + if isinstance(cs, str): + cs = CapStyle(cs) + + # Convert the JoinStyle enum to its name if needed + if hasattr(cs, "name"): + cs = cs.name.lower() + if cs in ["butt", "round", "projecting"]: self._capstyle = cs self.renderer.ctx.lineCap = _capstyle_d[cs] else: raise ValueError(f"Unrecognized cap style. Found {cs}") + def get_capstyle(self): + return self._capstyle + def set_clip_rectangle(self, rectangle): self.renderer.ctx.save() if not rectangle: @@ -204,7 +250,11 @@ def __init__(self, ctx, width, height, dpi, fig): self.ctx.width = self.width self.ctx.height = self.height self.dpi = dpi - self.mathtext_parser = MathTextParser("bitmap") + + # Create path-based math text parser; as the bitmap parser + # was deprecated in 3.4 and removed after 3.5 + self.mathtext_parser = MathTextParser("path") + self._get_font_helper = lru_cache(maxsize=50)(self._get_font_helper) # Keep the state of fontfaces that are loading @@ -240,14 +290,135 @@ def _matplotlib_color_to_CSS(self, color, alpha, alpha_overrides, is_RGB=True): return CSS_color + def _math_to_rgba(self, s, prop, rgb): + """Convert math text to an RGBA array using path parser and figure""" + from io import BytesIO + + # Get the text dimensions and generate a figure + # of the right rize. + width, height, depth, _, _ = self.mathtext_parser.parse(s, dpi=72, prop=prop) + + fig = figure.Figure(figsize=(width / 72, height / 72)) + + # Add text to the figure + # Note: depth/height gives us the baseline position + fig.text(0, depth / height, s, fontproperties=prop, color=rgb) + + backend_agg.FigureCanvasAgg(fig) + + buf = BytesIO() # render to PNG + fig.savefig(buf, dpi=self.dpi, format="png", transparent=True) + buf.seek(0) + + rgba = plt.imread(buf) + return rgba, depth + + def _draw_math_text_path(self, gc, x, y, s, prop, angle): + """Draw mathematical text using paths directly on the canvas. + + This method renders math text by drawing the actual glyph paths + onto the canvas, rather than creating a temporary image. + + Parameters + ---------- + gc : GraphicsContextHTMLCanvas + The graphics context to use for drawing + x, y : float + The position of the text baseline in pixels + s : str + The text string to render + prop : FontProperties + The font properties to use for rendering + angle : float + The rotation angle in degrees + """ + width, height, depth, glyphs, rects = self.mathtext_parser.parse( + s, dpi=self.dpi, prop=prop + ) + + self.ctx.save() + + self.ctx.translate(x, self.height - y) + if angle != 0: + self.ctx.rotate(-math.radians(angle)) + + self.ctx.fillStyle = self._matplotlib_color_to_CSS( + gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha() + ) + + for font, fontsize, _, ox, oy in glyphs: + self.ctx.save() + self.ctx.translate(ox, -oy) + + font.set_size(fontsize, self.dpi) + verts, codes = font.get_path() + + verts = verts * fontsize / font.units_per_EM + + path = Path(verts, codes) + + transform = Affine2D().scale(1.0, -1.0) + self._path_helper(self.ctx, path, transform) + self.ctx.fill() + + self.ctx.restore() + + for x1, y1, x2, y2 in rects: + self.ctx.fillRect(x1, -y2, x2 - x1, y2 - y1) + + self.ctx.restore() + + def _draw_math_text(self, gc, x, y, s, prop, angle): + """Draw mathematical text using the most appropriate method. + + This method tries direct path rendering first, and falls back to + the image-based approach if needed. + + Parameters + ---------- + gc : GraphicsContextHTMLCanvas + The graphics context to use for drawing + x, y : float + The position of the text baseline in pixels + s : str + The text string to render + prop : FontProperties + The font properties to use for rendering + angle : float + The rotation angle in degrees + """ + try: + self._draw_math_text_path(gc, x, y, s, prop, angle) + except Exception as e: + # If path rendering fails, we fall back to image-based approach + print(f"Path rendering failed, falling back to image: {str(e)}") + + rgba, depth = self._math_to_rgba(s, prop, gc.get_rgb()) + + angle = math.radians(angle) + if angle != 0: + self.ctx.save() + self.ctx.translate(x, y) + self.ctx.rotate(-angle) + self.ctx.translate(-x, -y) + + self.draw_image(gc, x, -y - depth, np.flipud(rgba)) + + if angle != 0: + self.ctx.restore() + def _set_style(self, gc, rgbFace=None): if rgbFace is not None: self.ctx.fillStyle = self._matplotlib_color_to_CSS( rgbFace, gc.get_alpha(), gc.get_forced_alpha() ) - if gc.get_capstyle(): - self.ctx.lineCap = _capstyle_d[gc.get_capstyle()] + capstyle = gc.get_capstyle() + if capstyle: + # Get the string name if it's an enum + if hasattr(capstyle, "name"): + capstyle = capstyle.name.lower() + self.ctx.lineCap = _capstyle_d[capstyle] self.ctx.strokeStyle = self._matplotlib_color_to_CSS( gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha() @@ -329,10 +500,13 @@ def _get_font(self, prop): def get_text_width_height_descent(self, s, prop, ismath): w: float h: float + d: float if ismath: - image, d = self.mathtext_parser.parse(s, self.dpi, prop) - image_arr = np.asarray(image) - h, w = image_arr.shape + # Use the path parser to get exact metrics + width, height, depth, _, _ = self.mathtext_parser.parse( + s, dpi=72, prop=prop + ) + return width, height, depth else: font, _ = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) @@ -340,31 +514,7 @@ def get_text_width_height_descent(self, s, prop, ismath): w /= 64.0 h /= 64.0 d = font.get_descent() / 64.0 - return w, h, d - - def _draw_math_text(self, gc, x, y, s, prop, angle): - rgba, descent = self.mathtext_parser.to_rgba( - s, gc.get_rgb(), self.dpi, prop.get_size_in_points() - ) - height, width, _ = rgba.shape - angle = math.radians(angle) - if angle != 0: - self.ctx.save() - self.ctx.translate(x, y) - self.ctx.rotate(-angle) - self.ctx.translate(-x, -y) - self.draw_image(gc, x, -y - descent, np.flipud(rgba)) - if angle != 0: - self.ctx.restore() - - def load_font_into_web(self, loaded_face, font_url): - fontface = loaded_face.result() - document.fonts.add(fontface) - self.fonts_loading.pop(font_url, None) - - # Redraw figure after font has loaded - self.fig.draw() - return fontface + return w, h, d def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: @@ -421,6 +571,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if angle != 0: self.ctx.restore() + def load_font_into_web(self, loaded_face, font_url): + fontface = loaded_face.result() + document.fonts.add(fontface) + self.fonts_loading.pop(font_url, None) + + # Redraw figure after font has loaded + self.fig.draw() + return fontface + class FigureManagerHTMLCanvas(FigureManagerBase): def __init__(self, canvas, num): @@ -443,8 +602,13 @@ def set_window_title(self, title): @_Backend.export class _BackendHTMLCanvas(_Backend): - FigureCanvas = FigureCanvasHTMLCanvas - FigureManager = FigureManagerHTMLCanvas + # FigureCanvas = FigureCanvasHTMLCanvas + # FigureManager = FigureManagerHTMLCanvas + # Note: with release 0.2.3, we've redirected the HTMLCanvas backend to use the WASM backend + # for now, as the changes to the HTMLCanvas backend are not yet fully functional. + # This will be updated in a future release. + FigureCanvas = FigureCanvasAggWasm + FigureManager = FigureManagerAggWasm @staticmethod def show(*args, **kwargs): From 4a1553357b8f4d7d2005510fb309d8d1fac0ba03 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Tue, 20 May 2025 16:50:36 +0900 Subject: [PATCH 8/8] Add archive notice (#68) * update readme * update --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 8e91430..0cd1c3e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +## DISCLAIMER + +This project is no longer used in Pyodide as of Pyodide v0.28 (see [issue#65](https://github.com/pyodide/matplotlib-pyodide/issues/65#issuecomment-2532463697)). +We don't accept any new features or bug fixes. The project is archived and will not be maintained anymore. + +The default matplotlib backend for Pyodide is now the patched version of `webagg` backend. If you were using `matplotlib_pyodide` in your code, +simply removing the `matplotlib.use('module://matplotlib_pyodide...')` line should be enough to make your code work with the new backend. + +If it doesn't, try replacing it with `matplotlib.use('webagg')`. + # matplotlib-pyodide [![PyPI Latest Release](https://img.shields.io/pypi/v/matplotlib-pyodide.svg)](https://pypi.org/project/matplotlib-pyodide/)