Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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 docs/matplotlib-compat-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,23 @@ colorbar domains) fully cleared.
using typed list reductions; the complete shippable Python package now type
checks cleanly.

### Free-form layout parity — 2026-07-14 (Matplotlib 3.11.0 reference)

- HTML output now places free-form panels absolutely at their figure
rectangles on a fixed-size canvas, matching the PNG compositor: `add_axes`
rects stack and overlap exactly as in Matplotlib (an inset `plt.axes([...])`
draws on top of a default axes instead of rendering as a side-by-side grid
panel), with document order providing Matplotlib's draw order. SVG export
gained the same absolute-placement path.
- `Figure.subplots_adjust(left=, right=, top=, bottom=, wspace=, hspace=)` is
now implemented instead of raising `NotImplementedError` (which superseded
the earlier silent discard): the values update the figure's SubplotParams
frame, subplot grids resolve to per-cell figure rectangles through the
GridSpec geometry (wspace/hspace as fractions of the average cell size, like
Matplotlib), and all three exporters position panels at those rectangles.
Out-of-order frames (`left >= right`, `bottom >= top`) raise `ValueError`
as in Matplotlib. This clears the two loud PDSH `subplots_adjust` cells
(04.08 2×3 labeled grid, 04.10 5×5 zero-gap faces).

Future entries must identify the Matplotlib release/revision, inventory
additions or removals, and any compatibility-level changes.
15 changes: 9 additions & 6 deletions docs/matplotlib-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,17 @@ dependency-free `triangles=` shorthand into Matplotlib's equivalent
| `quiver`, `barbs`, `streamplot` | Native vector endpoint/arrowhead and bounded streamline kernels feeding one instanced segment mark. Barbs are a visual approximation: magnitude maps to a bounded tick count, not WMO 50/10/5 increments. Streamplot always uses the shim's own bounded fixed-step integrator (identical output with or without Matplotlib installed, but paths approximate Matplotlib's adaptive ones); `start_points`, `integration_direction`, array widths/colors and `num_arrows` are honored, and remaining non-default integration options fail loudly |
| `tripcolor`, `triplot`, `tricontour`, `tricontourf` | Explicit topology or native dependency-free Delaunay triangulation; indexed geometry and isolines stay in Rust |
| `pie` / `pie_label` | Native pie/donut tessellation and the Matplotlib 3.11 `PieContainer` (`values`, `fracs`, grouped text labels) |
| `axhline` / `axvline` / `axhspan` / `axvspan`, `text`, `annotate`, `table` | Fractional span bounds plus data/axes/figure text coordinates are supported; `arrowprops` is approximated as callout text |
| `axhline` / `axvline` / `axhspan` / `axvspan`, `text`, `annotate`, `table` | Fractional span bounds plus data/axes/figure text coordinates are supported. `annotate(arrowprops=)` draws real arrows in every output: offset-point text becomes an engine callout (arrow pinned from label to point across zoom), data-coordinate text an arrow annotation; date-string coordinates convert on datetime axes. Arrowstyles map to head/tail shapes (`->` open V, `-|>` filled, `\|-\|`/brackets bar caps, `fancy`/`simple`/`wedge` filled tapered shafts sized by the text's mutation scale) and `connectionstyle` arc3/angle3/angle become quadratic curves (corner rounding approximated); `alpha` dims the arrow only. `bbox=` becomes label box styles (fill/edge/round corners/`pad`) on the HTML label; static exporters keep the plain label |
| `xlabel` / `ylabel` / `title` / `suptitle` | Suptitles are retained in HTML and multi-panel PNG/SVG |
| `legend()` | `loc`, columns, title/font size/colors, frame styling, `borderpad`, `labelspacing`, `fancybox`, `framealpha`, and `shadow` are retained across browser and static output. `loc='best'` chooses the least occupied corner from bounded samples of the current data |
| `grid(True/False)` | toggles the grid via the theme |
| `xlim` / `ylim`, axis scales, `invert_xaxis/yaxis` | linear/log are native; symlog/logit/asinh use dependency-free monotone data transforms with inverse limit/tick semantics. Automatic linear ticks include Matplotlib's 2.5 step and use uniform decimal padding across a tick set; locations refresh as data arrives. Artist `get_data()` reflects the transformed space; logit masks values at/outside (0, 1) |
| `set_major_locator` / `set_major_formatter`, `plt.NullLocator/FixedLocator/MultipleLocator/MaxNLocator/LinearLocator/LogLocator`, `plt.NullFormatter/FixedFormatter/FuncFormatter/FormatStrFormatter/StrMethodFormatter/ScalarFormatter` | xy-owned re-implementations resolved at build time against live data limits (Null/Fixed/Multiple are position-exact; MaxN is a nice-step heuristic, not Matplotlib's edge-extension algorithm). Third-party locator objects work if they implement `tick_values(vmin, vmax)`; minor locators/formatters are retained for round-tripping but minor ticks do not render |
| datetime, timedelta, and string coordinates | datetime inputs use the engine's automatic date ticks, timedeltas are bounded to elapsed seconds, and common strings use categorical ticks; the general Matplotlib units registry is intentionally out of scope |
| `set_major_locator` / `set_major_formatter`, `plt.NullLocator/FixedLocator/MultipleLocator/MaxNLocator/LinearLocator/LogLocator`, `plt.NullFormatter/FixedFormatter/FuncFormatter/FormatStrFormatter/StrMethodFormatter/ScalarFormatter` | xy-owned re-implementations resolved at build time against live data limits (Null/Fixed/Multiple/Linear are position-exact; MaxN/Auto port Matplotlib's `MaxNLocator._raw_ticks` — same step tables, edge extension, and offset handling — with `nbins="auto"` budgeted from the estimated plot rect like `Axis.get_tick_space()`; Log remains approximate). Third-party locator objects work if they implement `tick_values(vmin, vmax)`; minor locators/formatters are retained for round-tripping but minor ticks do not render, except that a labeled minor pair under a blanked major formatter (the centered date-label idiom) is promoted to the drawn tick set |
| `plt.dates.MonthLocator/YearLocator/DayLocator/DateFormatter` | xy-owned equivalents of the `matplotlib.dates` classes gallery scripts use; they locate and format in the engine's canonical ms-since-epoch axis unit (not Matplotlib's day floats), and `interval` approximates rrule by epoch-anchored occurrence counting |
| datetime, timedelta, and string coordinates | datetime inputs use the engine's automatic date ticks, timedeltas are bounded to elapsed seconds, and common strings use categorical ticks; the general Matplotlib units registry is intentionally out of scope. pandas datetime plotting (`series.plot(ax=ax)`) works against that contract: `get_{x,y}data(orig=False)` returns ms-since-epoch floats, and pandas' period-ordinal tickers (`TimeSeries_Date*`) are accepted as no-ops so the native date ticks keep rendering |
| `xticks(positions, labels, rotation=)` / `tick_params(labelrotation=)` | Exact positions and strings render in browser, PNG, and SVG |
| `twinx()`, `secondary_xaxis()`, `secondary_yaxis()` | second data axes and linked tick-only secondary axes with callable forward/inverse conversions. Secondary-axis ticks are evenly spaced conversions of the primary domain (not Matplotlib's secondary-unit locators) and currently reach the interactive HTML client only — PNG/SVG export does not draw them yet |
| `fig, ax = plt.subplots()`; `plt.subplots(n, m, figsize=, dpi=, squeeze=, sharex=, sharey=)` | Grid renders as CSS-grid HTML and stitched PNG/SVG; shared axes use common domains and live linked pan/zoom. `Figure.subplots_adjust(...)` is rejected because those frame/spacing values are not representable by this renderer; use `GridSpec(..., wspace=, hspace=)` for supported explicit layout |
| `fig, ax = plt.subplots()`; `plt.subplots(n, m, figsize=, dpi=, squeeze=, sharex=, sharey=)` | Grid renders as CSS-grid HTML and stitched PNG/SVG; shared axes use common domains and live linked pan/zoom. `Figure.subplots_adjust(left=, right=, top=, bottom=, wspace=, hspace=)` moves the SubplotParams frame: the grid resolves to explicit figure rectangles and every exporter (HTML, PNG, SVG) positions panels at those rectangles |
| `fig.add_subplot(2, 2, 1)` / `add_subplot(221)` | |
| `gca` / `gcf` / `sca` / `figure(num)` / `close(...)` | matplotlib's implicit-state semantics |
| `savefig('x.png' / '.svg' / '.html', dpi=)` | Browser-free PNG/SVG supports both single and multi-panel figures; file-like targets require an explicit `format=` and unsupported metadata/layout/export formats fail loudly |
Expand All @@ -85,7 +86,7 @@ dependency-free `triangles=` shorthand into Matplotlib's equivalent
| `plt.colorbar()` / `fig.colorbar()` / `plt.clim()` / `plt.gci()` | Returns a live handle (`set_label`, `set_ticks`); with no mappable it uses the current image the way pyplot does. `ticks=`/`extend=` render in PNG and SVG (the HTML colorbar stays a minimal gradient without tick text); `clim` retargets the mappable's color window and any colorbar derived from it |
| `rcParams` | Figure size/DPI, line width/marker size, image cmap/origin, axes color cycle, and all four `axes.spines.*` switches affect every exporter. Pyplot axes default to Matplotlib's four-sided box and each spine can be hidden independently. The chrome keys (axes face/edge/label/title styles, font family/size, tick colors/sizes, legend defaults, figure facecolor) reach the HTML renderer and multi-panel PNG stitching; single-chart PNG and SVG export currently render their own fixed chrome and ignore them. Unknown keys warn once |
| `plt.style.use(...)` / `plt.style.context(...)` | `"default"`, `"xy"`, bounded rcParam dictionaries, ordered lists, and the stock sheets fivethirtyeight, ggplot, bmh, dark_background, grayscale, and seaborn-v0_8-white(grid) — reduced to the supported rcParams subset (colors, grid, cycle, line width, font size; per-sheet keys outside that subset are not carried). `context()` snapshots and restores. Unknown sheet names fail precisely |
| `plt.GridSpec(r, c, wspace=, hspace=, width_ratios=)` + slice specs | Spans (`grid[0, 1:]`, `grid[:-1, 0]`) and custom spacing resolve to explicit figure rectangles using Matplotlib's SubplotParams frame; default-geometry single cells keep the uniform grid. Spanning layouts position exactly in PNG; HTML/SVG sequence free-form panels rather than overlaying the true rectangles |
| `plt.GridSpec(r, c, wspace=, hspace=, width_ratios=)` + slice specs | Spans (`grid[0, 1:]`, `grid[:-1, 0]`) and custom spacing resolve to explicit figure rectangles using Matplotlib's SubplotParams frame; default-geometry single cells keep the uniform grid. Spanning layouts position exactly in HTML, PNG, and SVG: free-form panels (including `add_axes` rects and insets) render absolutely at their figure rectangles in every exporter, with later axes stacked above earlier ones |
| `add_subplot(spec, sharex=, sharey=, xticklabels=[], ...)` | per-axes sharing aliases the axis-property store (static domains, as `twiny` does), not Matplotlib's live Grouper; `get_shared_x_axes()` reflects it |

## Outside 2-D chart-method compatibility
Expand All @@ -100,7 +101,9 @@ Unknown keyword arguments on supported calls raise `TypeError` naming the
offending keyword. Known material options that the native marks cannot honor
raise `NotImplementedError`, with these documented exceptions that are accepted
as visual approximations rather than rejected: the barbs glyph and imshow
smoothing collapse above, `annotate(arrowprops=...)` reduced to callout text,
smoothing collapse above, `annotate(arrowprops=...)` connection curves and
fancy/wedge outlines drawn as quadratic-curve tapered fills rather than
Matplotlib's exact patch paths, `bbox=` boxes drawn only by the HTML label,
and errorbar limit flags rendered as one-sided bars without Matplotlib's caret
arrows.

Expand Down
5 changes: 4 additions & 1 deletion docs/matplotlib-shim-todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ The shim can be called complete for ordinary 2-D scripts when:
- [x] Implement `subplots_adjust()` parameters (`left`, `right`, `top`,
`bottom`, `wspace`, `hspace`) for HTML, PNG, and SVG grids. Evidence:
`tests/pyplot/test_layout_noops.py::test_subplots_adjust_records_supported_spacing_values`
records all supported spacing values for grid exporters and rejects unknown kwargs.
verifies the adjusted SubplotParams frame resolves to per-cell figure
rectangles (and rejects unknown kwargs and out-of-order frames);
`tests/pyplot/test_layout_text_parity_fixes.py::test_subplots_adjust_positions_grid_panels_in_every_exporter`
verifies HTML/PNG/SVG all position the adjusted panels.
- [x] Implement `Figure.autofmt_xdate()` label rotation/alignment. Evidence:
`tests/pyplot/test_layout_noops.py::test_autofmt_xdate_rotates_x_tick_labels_on_all_axes`
verifies rotation and horizontal alignment state on every axes.
Expand Down
24 changes: 14 additions & 10 deletions examples/pdsh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,21 @@ signal. A "cell ok" also implies a non-empty `savefig` PNG export.
| 04.05 Histograms and Binnings | 10/10 | 10/10 |
| 04.06 Customizing Legends | 11/11 | 10/11 |
| 04.07 Customizing Colorbars | 13/13 | 13/13 |
| 04.08 Multiple Subplots | 10/10 | 9/10 |
| 04.08 Multiple Subplots | 10/10 | 10/10 |
| 04.09 Text and Annotation | 9/9 | 6/9 |
| 04.10 Customizing Ticks | 11/11 | 10/11 |
| 04.10 Customizing Ticks | 11/11 | 11/11 |
| 04.11 Settings and Stylesheets | 15/15 | 15/15 |
| 04.12 Three-Dimensional Plotting¹ | 17/17 | 7/17 |
| 04.14 Visualization with Seaborn² | 31/31 | 30/31 |
| **Total** | **171/171** | **154/171 (90%)** |
| **Total** | **171/171** | **156/171 (91%)** |

Excluding the out-of-scope 3D notebook: 147/154 (95%). The first
measurement, before the gap-closing pass, was 121/171 (71%).
Excluding the out-of-scope 3D notebook: 149/154 (97%). The first
measurement, before the gap-closing pass, was 121/171 (71%). The 04.08
and 04.10 rows reflect the 2026-07-14 `subplots_adjust` implementation:
both formerly rejected cells re-ran and exported (04.10's re-run used
identically shaped stand-in images because the measurement sandbox
cannot download the Olivetti faces; the previously recorded failure was
the `subplots_adjust` rejection itself, raised before any drawing).

¹ 3D projections are outside xy's 2-D chart-method compatibility target
(see [docs/matplotlib-compat.md](../../docs/matplotlib-compat.md));
Expand Down Expand Up @@ -103,13 +108,12 @@ fallback onto matplotlib will ever be added).

## Remaining failures

All 17 are one of: 3-D projection cells (10, loud rejections by
All 15 are one of: 3-D projection cells (10, loud rejections by
design), the second-legend `Legend` class (1, a documented loud rejection),
`subplots_adjust` calls whose values the renderer cannot honor (2, now loud
rather than silently discarded), pandas
`Series.plot(ax=ax)` datetime interop (3, a real gap — a dtype error
pandas `Series.plot(ax=ax)` datetime interop (3, a real gap — a dtype error
inside the pandas plotting path), and `axhline(marker=)` via seaborn
(1, loud rejection).
(1, loud rejection). The two `subplots_adjust` cells counted here until
2026-07-14 pass now that the shim implements the SubplotParams frame.

The 2026-07-14 fidelity follow-up also compared the affected PDSH cells
directly against Matplotlib 3.11: directional markers and `x`/`+` remain
Expand Down
34 changes: 17 additions & 17 deletions examples/pdsh/pdsh_04_00_introduction_to_matplotlib.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"outputs": [],
"source": [
"# Matplotlib reference\n",
"import matplotlib.pyplot as mpl_mpl\n",
"import matplotlib as mpl_mpl\n",
"import matplotlib.pyplot as mpl_plt"
]
},
Expand Down Expand Up @@ -196,7 +196,7 @@
"\n",
"x = np.linspace(0, 10, 100)\n",
"\n",
"fig = mpl_plt.figure()\n",
"mpl_fig = mpl_plt.figure()\n",
"mpl_plt.plot(x, np.sin(x), \"-\")\n",
"mpl_plt.plot(x, np.cos(x), \"--\");"
]
Expand All @@ -216,7 +216,7 @@
"\n",
"x = np.linspace(0, 10, 100)\n",
"\n",
"fig = xy_plt.figure()\n",
"xy_fig = xy_plt.figure()\n",
"xy_plt.plot(x, np.sin(x), \"-\")\n",
"xy_plt.plot(x, np.cos(x), \"--\");"
]
Expand Down Expand Up @@ -244,7 +244,7 @@
"outputs": [],
"source": [
"# Matplotlib reference\n",
"fig.savefig(\"my_figure.png\")"
"mpl_fig.savefig(\"my_figure.png\")"
]
},
{
Expand All @@ -258,7 +258,7 @@
"outputs": [],
"source": [
"# xy.pyplot\n",
"fig.savefig(\"my_figure.png\")"
"xy_fig.savefig(\"my_figure.png\")"
]
},
{
Expand Down Expand Up @@ -304,7 +304,7 @@
"outputs": [],
"source": [
"# Matplotlib reference\n",
"fig.canvas.get_supported_filetypes()"
"mpl_fig.canvas.get_supported_filetypes()"
]
},
{
Expand All @@ -318,7 +318,7 @@
"outputs": [],
"source": [
"# xy.pyplot\n",
"fig.canvas.get_supported_filetypes()"
"xy_fig.canvas.get_supported_filetypes()"
]
},
{
Expand Down Expand Up @@ -412,13 +412,13 @@
"outputs": [],
"source": [
"# Matplotlib reference\n",
"# First create a grid of plots\n",
"# ax will be an array of two Axes objects\n",
"fig, ax = mpl_plt.subplots(2)\n",
"# First create a mpl_grid of plots\n",
"# mpl_ax will be an array of two Axes objects\n",
"mpl_fig, mpl_ax = mpl_plt.subplots(2)\n",
"\n",
"# Call plot() method on the appropriate object\n",
"ax[0].plot(x, np.sin(x))\n",
"ax[1].plot(x, np.cos(x));"
"mpl_ax[0].plot(x, np.sin(x))\n",
"mpl_ax[1].plot(x, np.cos(x));"
]
},
{
Expand All @@ -432,13 +432,13 @@
"outputs": [],
"source": [
"# xy.pyplot\n",
"# First create a grid of plots\n",
"# ax will be an array of two Axes objects\n",
"fig, ax = xy_plt.subplots(2)\n",
"# First create a xy_grid of plots\n",
"# xy_ax will be an array of two Axes objects\n",
"xy_fig, xy_ax = xy_plt.subplots(2)\n",
"\n",
"# Call plot() method on the appropriate object\n",
"ax[0].plot(x, np.sin(x))\n",
"ax[1].plot(x, np.cos(x));"
"xy_ax[0].plot(x, np.sin(x))\n",
"xy_ax[1].plot(x, np.cos(x));"
]
}
],
Expand Down
32 changes: 16 additions & 16 deletions examples/pdsh/pdsh_04_01_simple_line_plots.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@
"outputs": [],
"source": [
"# Matplotlib reference\n",
"fig = mpl_plt.figure()\n",
"ax = mpl_plt.axes()"
"mpl_fig = mpl_plt.figure()\n",
"mpl_ax = mpl_plt.axes()"
]
},
{
Expand All @@ -99,8 +99,8 @@
"outputs": [],
"source": [
"# xy.pyplot\n",
"fig = xy_plt.figure()\n",
"ax = xy_plt.axes()"
"xy_fig = xy_plt.figure()\n",
"xy_ax = xy_plt.axes()"
]
},
{
Expand All @@ -114,11 +114,11 @@
"outputs": [],
"source": [
"# Matplotlib reference\n",
"fig = mpl_plt.figure()\n",
"ax = mpl_plt.axes()\n",
"mpl_fig = mpl_plt.figure()\n",
"mpl_ax = mpl_plt.axes()\n",
"\n",
"x = np.linspace(0, 10, 1000)\n",
"ax.plot(x, np.sin(x));"
"mpl_ax.plot(x, np.sin(x));"
]
},
{
Expand All @@ -132,11 +132,11 @@
"outputs": [],
"source": [
"# xy.pyplot\n",
"fig = xy_plt.figure()\n",
"ax = xy_plt.axes()\n",
"xy_fig = xy_plt.figure()\n",
"xy_ax = xy_plt.axes()\n",
"\n",
"x = np.linspace(0, 10, 1000)\n",
"ax.plot(x, np.sin(x));"
"xy_ax.plot(x, np.sin(x));"
]
},
{
Expand Down Expand Up @@ -570,9 +570,9 @@
"outputs": [],
"source": [
"# Matplotlib reference\n",
"ax = mpl_plt.axes()\n",
"ax.plot(x, np.sin(x))\n",
"ax.set(xlim=(0, 10), ylim=(-2, 2), xlabel=\"x\", ylabel=\"sin(x)\", title=\"A Simple Plot\");"
"mpl_ax = mpl_plt.axes()\n",
"mpl_ax.plot(x, np.sin(x))\n",
"mpl_ax.set(xlim=(0, 10), ylim=(-2, 2), xlabel=\"x\", ylabel=\"sin(x)\", title=\"A Simple Plot\");"
]
},
{
Expand All @@ -586,9 +586,9 @@
"outputs": [],
"source": [
"# xy.pyplot\n",
"ax = xy_plt.axes()\n",
"ax.plot(x, np.sin(x))\n",
"ax.set(xlim=(0, 10), ylim=(-2, 2), xlabel=\"x\", ylabel=\"sin(x)\", title=\"A Simple Plot\");"
"xy_ax = xy_plt.axes()\n",
"xy_ax.plot(x, np.sin(x))\n",
"xy_ax.set(xlim=(0, 10), ylim=(-2, 2), xlabel=\"x\", ylabel=\"sin(x)\", title=\"A Simple Plot\");"
]
}
],
Expand Down
Loading
Loading