Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/matplotlib-pyodide/README.md
Original file line number Diff line number Diff line change
@@ -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.
61 changes: 61 additions & 0 deletions examples/matplotlib-pyodide/html5_canvas_backend/code.py
Original file line number Diff line number Diff line change
@@ -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 <canvas>.
- module://matplotlib_pyodide.html5_canvas_backend
Draws directly to an HTML5 <canvas> 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 <code>html5_canvas_backend</code>, "
"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: <code>{matplotlib.get_backend()}</code>")

# 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["matplotlib", "matplotlib-pyodide", "numpy"]
41 changes: 41 additions & 0 deletions examples/matplotlib-pyodide/html5_canvas_backend/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

5 changes: 5 additions & 0 deletions examples/matplotlib-pyodide/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"html5_canvas_backend",
"wasm_backend_static",
"subplots_and_styles"
]
71 changes: 71 additions & 0 deletions examples/matplotlib-pyodide/subplots_and_styles/code.py
Original file line number Diff line number Diff line change
@@ -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 "
"<code>html5_canvas_backend</code> for crisp interactive charts and "
"<code>wasm_backend</code> when you want exact Agg fidelity."
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["matplotlib", "matplotlib-pyodide", "numpy"]
19 changes: 19 additions & 0 deletions examples/matplotlib-pyodide/subplots_and_styles/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
55 changes: 55 additions & 0 deletions examples/matplotlib-pyodide/wasm_backend_static/code.py
Original file line number Diff line number Diff line change
@@ -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 <code>wasm_backend</code> 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: <code>{matplotlib.get_backend()}</code>")

# 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["matplotlib", "matplotlib-pyodide", "numpy"]
20 changes: 20 additions & 0 deletions examples/matplotlib-pyodide/wasm_backend_static/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)