diff --git a/packages/matplotlib-pyodide/meta.yaml b/packages/matplotlib-pyodide/meta.yaml index 5600ce98f48..4ea8c6d1305 100644 --- a/packages/matplotlib-pyodide/meta.yaml +++ b/packages/matplotlib-pyodide/meta.yaml @@ -1,18 +1,11 @@ package: name: matplotlib-pyodide - version: 0.2.2 + version: 0.2.3 top-level: - matplotlib_pyodide source: - url: https://files.pythonhosted.org/packages/5e/1d/7b706e1f529b055a975d359ac3357c0257260a77ab75744c34f35e51b7d6/matplotlib-pyodide-0.2.2.tar.gz - sha256: c1f285208e891112d30e3825bb5a6f5c3a846a5f4080460a518962e2d73aa325 - patches: - # All five of these can be removed when updating to version 0.2.3 - - patches/0001-Replace-maxdict-with-lru_cache.patch - - patches/0002-Check-existence-of-toolbar.patch - - patches/0003-Temporarily-disable-event-listeners-add-notes.patch - - patches/0004-Use-path-based-MathTextParser-to-render-math-text.patch - - patches/0005-Redirect-HTMLCanvas-backend-to-WASM-backend.patch + url: https://files.pythonhosted.org/packages/1a/f9/b5f36d3221e8e211f366544ccc817c61e8f5c2bcc912fe1e1bf9a6044909/matplotlib_pyodide-0.2.3.tar.gz + sha256: 05a0042957f26ac77f824aa3d11ae25f3c0d05f93f78f3ed507d11a577da2ce9 about: home: https://github.com/pyodide/matplotlib-pyodide PyPI: https://pypi.org/project/matplotlib-pyodide diff --git a/packages/matplotlib-pyodide/patches/0001-Replace-maxdict-with-lru_cache.patch b/packages/matplotlib-pyodide/patches/0001-Replace-maxdict-with-lru_cache.patch deleted file mode 100644 index 6652a2ce6a4..00000000000 --- a/packages/matplotlib-pyodide/patches/0001-Replace-maxdict-with-lru_cache.patch +++ /dev/null @@ -1,80 +0,0 @@ -From 18cd638fb0b70078c0e6bedc4a98331ac7652f33 Mon Sep 17 00:00:00 2001 -From: Hood Chatham -Date: Tue, 22 Oct 2024 14:27:43 +0200 -Subject: [PATCH 1/5] Replace maxdict with lru_cache - -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 @@ from matplotlib.backend_bases import ( - 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 @@ class RendererHTMLCanvas(RendererBase): - 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 @@ class RendererHTMLCanvas(RendererBase): - 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 --- -2.39.5 (Apple Git-154) - diff --git a/packages/matplotlib-pyodide/patches/0002-Check-existence-of-toolbar.patch b/packages/matplotlib-pyodide/patches/0002-Check-existence-of-toolbar.patch deleted file mode 100644 index 3d5db29f3df..00000000000 --- a/packages/matplotlib-pyodide/patches/0002-Check-existence-of-toolbar.patch +++ /dev/null @@ -1,33 +0,0 @@ -From c373c570af117de7b4722444395e277a6e2eeffa Mon Sep 17 00:00:00 2001 -From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> -Date: Sat, 30 Nov 2024 01:14:30 +0530 -Subject: [PATCH 2/5] Check existence of toolbar - -Co-Authored-By: Gyeongjae Choi ---- - matplotlib_pyodide/browser_backend.py | 9 +++++++-- - 1 file changed, 7 insertions(+), 2 deletions(-) - -diff --git a/matplotlib_pyodide/browser_backend.py b/matplotlib_pyodide/browser_backend.py -index a135846..130c6f1 100644 ---- a/matplotlib_pyodide/browser_backend.py -+++ b/matplotlib_pyodide/browser_backend.py -@@ -180,8 +180,13 @@ class FigureCanvasWasm(FigureCanvasBase): - - # 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") --- -2.39.5 (Apple Git-154) - diff --git a/packages/matplotlib-pyodide/patches/0003-Temporarily-disable-event-listeners-add-notes.patch b/packages/matplotlib-pyodide/patches/0003-Temporarily-disable-event-listeners-add-notes.patch deleted file mode 100644 index 526429f9125..00000000000 --- a/packages/matplotlib-pyodide/patches/0003-Temporarily-disable-event-listeners-add-notes.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 56a32407ca20c0fc2227a66640ff3eded4fd22c9 Mon Sep 17 00:00:00 2001 -From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> -Date: Tue, 10 Dec 2024 12:34:54 +0530 -Subject: [PATCH 3/5] Temporarily disable event listeners, add notes - ---- - matplotlib_pyodide/browser_backend.py | 16 +++++++++------- - 1 file changed, 9 insertions(+), 7 deletions(-) - -diff --git a/matplotlib_pyodide/browser_backend.py b/matplotlib_pyodide/browser_backend.py -index 130c6f1..be24a5a 100644 ---- a/matplotlib_pyodide/browser_backend.py -+++ b/matplotlib_pyodide/browser_backend.py -@@ -164,13 +164,15 @@ class FigureCanvasWasm(FigureCanvasBase): - 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]) --- -2.39.5 (Apple Git-154) - diff --git a/packages/matplotlib-pyodide/patches/0004-Use-path-based-MathTextParser-to-render-math-text.patch b/packages/matplotlib-pyodide/patches/0004-Use-path-based-MathTextParser-to-render-math-text.patch deleted file mode 100644 index 440b89b77cc..00000000000 --- a/packages/matplotlib-pyodide/patches/0004-Use-path-based-MathTextParser-to-render-math-text.patch +++ /dev/null @@ -1,284 +0,0 @@ -From 99cbb620d37acc936deef7fce91aa193127f84e4 Mon Sep 17 00:00:00 2001 -From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> -Date: Sat, 30 Nov 2024 04:38:54 +0530 -Subject: [PATCH 4/5] Use path-based MathTextParser to render math text - ---- - matplotlib_pyodide/html5_canvas_backend.py | 199 +++++++++++++++++---- - 1 file changed, 167 insertions(+), 32 deletions(-) - -diff --git a/matplotlib_pyodide/html5_canvas_backend.py b/matplotlib_pyodide/html5_canvas_backend.py -index ac3afd5..6e5b81a 100644 ---- a/matplotlib_pyodide/html5_canvas_backend.py -+++ b/matplotlib_pyodide/html5_canvas_backend.py -@@ -3,14 +3,17 @@ 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 -@@ -144,12 +147,31 @@ class GraphicsContextHTMLCanvas(GraphicsContextBase): - 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 +226,11 @@ class RendererHTMLCanvas(RendererBase): - 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 +266,135 @@ class RendererHTMLCanvas(RendererBase): - - 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 +476,13 @@ class RendererHTMLCanvas(RendererBase): - 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 +490,7 @@ class RendererHTMLCanvas(RendererBase): - 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 +547,15 @@ class RendererHTMLCanvas(RendererBase): - 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): --- -2.39.5 (Apple Git-154) - diff --git a/packages/matplotlib-pyodide/patches/0005-Redirect-HTMLCanvas-backend-to-WASM-backend.patch b/packages/matplotlib-pyodide/patches/0005-Redirect-HTMLCanvas-backend-to-WASM-backend.patch deleted file mode 100644 index b34439e90d4..00000000000 --- a/packages/matplotlib-pyodide/patches/0005-Redirect-HTMLCanvas-backend-to-WASM-backend.patch +++ /dev/null @@ -1,50 +0,0 @@ -From 1d26fc0dd8ca6eaf2f740771c9ec767a553f047e Mon Sep 17 00:00:00 2001 -From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> -Date: Mon, 16 Dec 2024 17:37:56 +0530 -Subject: [PATCH 5/5] Redirect HTMLCanvas backend to WASM backend - ---- - matplotlib_pyodide/html5_canvas_backend.py | 12 ++++++++++-- - 1 file changed, 10 insertions(+), 2 deletions(-) - -diff --git a/matplotlib_pyodide/html5_canvas_backend.py b/matplotlib_pyodide/html5_canvas_backend.py -index 37825b7..91b723f 100644 ---- a/matplotlib_pyodide/html5_canvas_backend.py -+++ b/matplotlib_pyodide/html5_canvas_backend.py -@@ -44,7 +44,9 @@ from matplotlib.transforms import Affine2D - 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 -@@ -52,6 +54,7 @@ except ImportError as err: - 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"} -@@ -599,8 +602,13 @@ class FigureManagerHTMLCanvas(FigureManagerBase): - - @_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): --- -2.39.5 (Apple Git-154) -