diff --git a/examples/matplotlib-pyodide/README.md b/examples/matplotlib-pyodide/README.md new file mode 100644 index 0000000..6092b26 --- /dev/null +++ b/examples/matplotlib-pyodide/README.md @@ -0,0 +1,18 @@ +# matplotlib-pyodide Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/matplotlib-pyodide/html5_canvas_backend/code.py b/examples/matplotlib-pyodide/html5_canvas_backend/code.py new file mode 100644 index 0000000..482a3ff --- /dev/null +++ b/examples/matplotlib-pyodide/html5_canvas_backend/code.py @@ -0,0 +1,61 @@ +""" +A first look at matplotlib-pyodide. + +This package provides two HTML5 backends for matplotlib that work +inside Pyodide: + + - module://matplotlib_pyodide.wasm_backend + Renders the Agg buffer to a static . + - module://matplotlib_pyodide.html5_canvas_backend + Draws directly to an HTML5 with native fonts and + crisp vector strokes; supports interactivity. + +You select a backend with matplotlib.use(...), exactly as you would +on the desktop. See: + https://github.com/pyodide/matplotlib-pyodide + +For this example, the html5_canvas_backend has already been activated +in setup.py before pyplot was imported. +""" +from IPython.core.display import display, HTML +# Switch matplotlib to the interactive HTML5 canvas backend provided by +# matplotlib-pyodide. This must happen BEFORE pyplot is imported, because +# pyplot binds to whatever backend is active at import time. +import matplotlib +matplotlib.use("module://matplotlib_pyodide.html5_canvas_backend") + +import numpy as np +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) + + +heading("matplotlib-pyodide: an interactive canvas plot") +note( + "We're rendering through the html5_canvas_backend, " + "which draws to a real HTML5 canvas instead of producing a PNG. " + "Text uses the browser's fonts and lines stay sharp on zoom." +) + +# Confirm the active backend at runtime. +import matplotlib +note(f"Active matplotlib backend: {matplotlib.get_backend()}") + +# A small synthetic signal: a damped oscillation. The kind of plot +# you'd reach for when sanity-checking a model in a notebook. +t = np.linspace(0, 6 * np.pi, 400) +signal = np.exp(-t / 8) * np.sin(t) +envelope = np.exp(-t / 8) + +fig, ax = plt.subplots(figsize=(8, 4)) +ax.plot(t, signal, color="navy", linewidth=2, label="damped sine") +ax.plot(t, envelope, color="crimson", linestyle="--", + linewidth=1, label="envelope") +ax.plot(t, -envelope, color="crimson", linestyle="--", linewidth=1) +ax.axhline(0, color="gray", linewidth=0.5) +ax.set_title("A damped oscillation, drawn on an HTML5 canvas") +ax.set_xlabel("time") +ax.set_ylabel("amplitude") +ax.legend(loc="upper right") +fig.tight_layout() +display(fig, append=True) diff --git a/examples/matplotlib-pyodide/html5_canvas_backend/config.toml b/examples/matplotlib-pyodide/html5_canvas_backend/config.toml new file mode 100644 index 0000000..10f2fb8 --- /dev/null +++ b/examples/matplotlib-pyodide/html5_canvas_backend/config.toml @@ -0,0 +1 @@ +packages = ["matplotlib", "matplotlib-pyodide", "numpy"] diff --git a/examples/matplotlib-pyodide/html5_canvas_backend/setup.py b/examples/matplotlib-pyodide/html5_canvas_backend/setup.py new file mode 100644 index 0000000..0cc7a6f --- /dev/null +++ b/examples/matplotlib-pyodide/html5_canvas_backend/setup.py @@ -0,0 +1,41 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + diff --git a/examples/matplotlib-pyodide/order.json b/examples/matplotlib-pyodide/order.json new file mode 100644 index 0000000..312e88c --- /dev/null +++ b/examples/matplotlib-pyodide/order.json @@ -0,0 +1,5 @@ +[ + "html5_canvas_backend", + "wasm_backend_static", + "subplots_and_styles" +] diff --git a/examples/matplotlib-pyodide/subplots_and_styles/code.py b/examples/matplotlib-pyodide/subplots_and_styles/code.py new file mode 100644 index 0000000..fe45b5f --- /dev/null +++ b/examples/matplotlib-pyodide/subplots_and_styles/code.py @@ -0,0 +1,71 @@ +# --------------------------------------------------------------------- +# Multi-panel dashboards on the html5_canvas_backend. +# --------------------------------------------------------------------- + +import matplotlib +matplotlib.use("module://matplotlib_pyodide.html5_canvas_backend") + +import numpy as np +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) + + +heading("A four-panel dashboard") +note( + "Once a backend is selected, you write ordinary matplotlib. " + "Here's a small dashboard summarizing a week of pretend sensor " + "readings: line, histogram, bar, and box plots all on one figure." +) + +# Seven days of hourly readings from three sensors. +hours = np.arange(24 * 7) +sensor_names = ["kitchen", "garage", "attic"] +baselines = np.array([21.0, 18.0, 24.5]) +readings = ( + baselines[:, None] + + 2.0 * np.sin((hours - 14) * 2 * np.pi / 24)[None, :] + + rng.normal(0, 0.6, size=(3, hours.size)) +) + +fig, axes = plt.subplots(2, 2, figsize=(10, 6)) +((ax_line, ax_hist), (ax_bar, ax_box)) = axes + +# Top-left: time series for each sensor. +for name, series in zip(sensor_names, readings): + ax_line.plot(hours, series, linewidth=1.2, label=name) +ax_line.set_title("Hourly temperature") +ax_line.set_xlabel("hour of week") +ax_line.set_ylabel("°C") +ax_line.legend(loc="upper right", fontsize=8) + +# Top-right: distribution of all readings. +ax_hist.hist(readings.ravel(), bins=30, color="slateblue", + edgecolor="white") +ax_hist.set_title("Distribution of all readings") +ax_hist.set_xlabel("°C") + +# Bottom-left: mean per sensor. +means = readings.mean(axis=1) +colors = ["#4c72b0", "#dd8452", "#55a868"] +ax_bar.bar(sensor_names, means, color=colors) +ax_bar.set_title("Mean temperature per sensor") +ax_bar.set_ylabel("°C") + +# Bottom-right: a box plot to compare spread. +ax_box.boxplot(readings.T, labels=sensor_names, patch_artist=True, + boxprops=dict(facecolor="#eaeaf2")) +ax_box.set_title("Spread per sensor") +ax_box.set_ylabel("°C") + +fig.suptitle("Week-long readings, drawn through matplotlib-pyodide", + fontsize=13) +fig.tight_layout() +display(fig, append=True) + +note( + "Both backends expose the full matplotlib API; the only difference " + "is the canvas they paint on. Pick " + "html5_canvas_backend for crisp interactive charts and " + "wasm_backend when you want exact Agg fidelity." +) diff --git a/examples/matplotlib-pyodide/subplots_and_styles/config.toml b/examples/matplotlib-pyodide/subplots_and_styles/config.toml new file mode 100644 index 0000000..10f2fb8 --- /dev/null +++ b/examples/matplotlib-pyodide/subplots_and_styles/config.toml @@ -0,0 +1 @@ +packages = ["matplotlib", "matplotlib-pyodide", "numpy"] diff --git a/examples/matplotlib-pyodide/subplots_and_styles/setup.py b/examples/matplotlib-pyodide/subplots_and_styles/setup.py new file mode 100644 index 0000000..8943a22 --- /dev/null +++ b/examples/matplotlib-pyodide/subplots_and_styles/setup.py @@ -0,0 +1,19 @@ +"""Setup for the multi-subplot example, back on the html5 canvas backend.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) diff --git a/examples/matplotlib-pyodide/wasm_backend_static/code.py b/examples/matplotlib-pyodide/wasm_backend_static/code.py new file mode 100644 index 0000000..3ba89d9 --- /dev/null +++ b/examples/matplotlib-pyodide/wasm_backend_static/code.py @@ -0,0 +1,55 @@ +# --------------------------------------------------------------------- +# The wasm_backend: static, Agg-rendered figures. +# --------------------------------------------------------------------- + +# This example demonstrates the OTHER backend that matplotlib-pyodide +# ships: wasm_backend, which rasterizes via Agg and shows the result +# as a static image. Selecting it must happen before pyplot import. +import matplotlib +matplotlib.use("module://matplotlib_pyodide.wasm_backend") + +import numpy as np +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) + + +heading("Switching to the static wasm_backend") +note( + "The wasm_backend renders matplotlib's familiar Agg " + "output into an HTML canvas as a static image. Pick it when you " + "want pixel-perfect parity with desktop matplotlib (hatching, " + "complex text, every artist) and don't need interactivity." +) +note(f"Active matplotlib backend: {matplotlib.get_backend()}") + +# A scatter plot of two synthetic clusters, the kind of thing you might +# show after a quick clustering experiment. +n = 200 +cluster_a = rng.normal(loc=(-1.5, 0.5), scale=0.6, size=(n, 2)) +cluster_b = rng.normal(loc=(1.2, -0.3), scale=0.8, size=(n, 2)) + +fig, ax = plt.subplots(figsize=(7, 5)) +ax.scatter( + cluster_a[:, 0], cluster_a[:, 1], + color="seagreen", alpha=0.6, edgecolor="white", label="group A", +) +ax.scatter( + cluster_b[:, 0], cluster_b[:, 1], + color="indianred", alpha=0.6, edgecolor="white", label="group B", +) + +# Mark the centroids with hatched markers; hatching is one of the +# things the Agg-based wasm_backend renders faithfully. +for points, color in [(cluster_a, "seagreen"), (cluster_b, "indianred")]: + cx, cy = points.mean(axis=0) + ax.scatter(cx, cy, s=300, facecolor="white", edgecolor=color, + linewidth=2, hatch="///", zorder=3) + +ax.set_title("Two synthetic clusters (rendered via Agg in WASM)") +ax.set_xlabel("x") +ax.set_ylabel("y") +ax.legend(loc="best") +ax.grid(True, linestyle=":", alpha=0.5) +fig.tight_layout() +display(fig, append=True) diff --git a/examples/matplotlib-pyodide/wasm_backend_static/config.toml b/examples/matplotlib-pyodide/wasm_backend_static/config.toml new file mode 100644 index 0000000..10f2fb8 --- /dev/null +++ b/examples/matplotlib-pyodide/wasm_backend_static/config.toml @@ -0,0 +1 @@ +packages = ["matplotlib", "matplotlib-pyodide", "numpy"] diff --git a/examples/matplotlib-pyodide/wasm_backend_static/setup.py b/examples/matplotlib-pyodide/wasm_backend_static/setup.py new file mode 100644 index 0000000..bb4ee63 --- /dev/null +++ b/examples/matplotlib-pyodide/wasm_backend_static/setup.py @@ -0,0 +1,20 @@ +"""Setup for the static wasm_backend example. No IPython shim needed.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) +