ENG-10167 Expand xy.pyplot shim: plot-family depth, scales, export chrome, and pinned Matplotlib 3.11 evidence - #36
Conversation
Merging this PR will degrade performance by 17.93%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_first_payload_scatter_continuous_channels |
8.6 ms | 10.7 ms | -19.88% |
| ❌ | test_first_payload_scatter_medium |
5.6 ms | 6.6 ms | -15.92% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing codex/clone-matplotlib-from-github (4bb7b3b) with main (c06d7e2)2
Footnotes
-
1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports. ↩
-
No successful run was found on
matplotlib-shim(5c14ce8) during the generation of this report, somain(c06d7e2) was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
Replace the hard-coded plotting-surface assertion with a reviewed snapshot of the pinned upstream revision (bde111fb4e), kept in sync by scripts/sync_matplotlib_compat.py, which also generates the method-by-method compatibility matrix from compatibility.json and the executable corpus. A dedicated CI job checks the snapshot against a pinned Matplotlib checkout and runs the dual-engine corpus and optional-interop tests. Tighten the compatibility contract itself: formerly silent option discards now raise loudly (pie/quiver/barbs/contour/table/norm options and more), options the marks can honor are implemented instead of rejected (contour extent, plain Normalize, dashed stem/eventplot/ triplot, streamplot start_points/direction/widths, stepfilled hist), and streamplot always uses the shim's own bounded integrator so output no longer depends on whether Matplotlib is installed. Documented approximation levels, the accepted visual approximations, and a compatibility changelog; added contract tests covering rc chrome, color export, transforms, P3 options, and silent-drop regressions.
Turn the dual-engine evidence from crash-free execution into real comparison: all 54 corpus cases now render PNGs in both engines and compare normalized ink density and bounding-box geometry, semantic oracles extend to contours, triangulations, vector directions, masked scatter, RGBA images, and removable handles, and thresholds gain negative controls proving blank or wrong output fails. A mechanical scan of every public adapter rejects unexplained accepted-and-dropped options (explicit `compat-noop:` rationales required), and the reference CI job moves to the released matplotlib==3.11.0 wheel. On the export side, implement the common savefig options instead of rejecting them: bbox_inches="tight", transparent=, facecolor=, and PNG/SVG metadata= via standards-compliant text chunks. Static PNG export now honors the axes background token, and multi-panel HTML/SVG/PNG composition applies figure backgrounds and styled suptitles (size/weight/family/color/position) instead of dropping the kwargs.
Boxplot notches/bootstrap/user statistics, violin KDE bandwidths/ quantiles/sides, hexbin C aggregation, symlog/logit/asinh scales, secondary axes, and affine data transforms — with the divergences an adversarial cross-engine audit found in both this tranche and the previous one repaired rather than shipped silently: - scatter drops rows masked in x/y/s; fill_between(interpolate=True) draws single-point where-regions; imsave colormaps original values; set_cmap validates names and feeds imshow/scatter defaults; usermedians no longer shift notch CIs; boxplot sym works (empty string suppresses fliers); hexbin mincnt and C aggregation share one bin membership; violinplot survives constant data. - Loud rejections replace silent discards: bxp component linestyles, secondary-axis set_ticks extras, fraction-space transforms on data artists, html-format metadata; singular transforms fail at set_transform time. - Export: SVG/HTML honor savefig(facecolor=); single-chart SVG keeps the suptitle; suptitle y maps as a figure fraction; non-Latin-1 PNG metadata keys raise ValueError. - Scales: logit masks values at/outside (0,1); auto ticks refresh as data arrives and reset when the scale returns to linear; explicit ticks keep data-unit labels. - Docs state the real boundaries (rectangular hexbin geometry, HTML-only secondary axes, xy-owned tick locators, snapshot pin no longer CI-compared); the corrupted todo section is rewritten; the tight-bbox and negative-control tests can now actually fail. Excludes examples/pdsh notebooks (in-flight, currently failing ruff).
The Python Data Science Handbook ch. 4 benchmark (import-swap over 14 notebooks) scored 121/171 runnable cells; the failures clustered on a handful of missing surfaces rather than rendering gaps. This closes them: xy-owned tick locators/formatters resolved against live data limits, style.context with the stock sheets reduced to the supported rcParams subset, RdGy/jet engine tables, gci/clim with colorbars that track their mappable, GridSpec slice spans via explicit figure rectangles, and the pandas-facing Axes surface (get_figure, get_lines, shared-axes groupers, Period coordinates). Scatter vmin/vmax previously crashed at render; they now flow into a real color_domain on the engine's color channel. grid(linestyle= 'solid') injected an invalid style value and broke every later export of the axes. subplot()/axes() silently dropped keyword arguments. Benchmark after: 154/171 (90%); 147/154 (95%) excluding the out-of-scope 3-D notebook; zero savefig errors on passing cells.
Add the Python Data Science Handbook matplotlib-chapter code cells (MIT; the CC-BY-NC-ND prose is omitted) under examples/pdsh with only the import swapped to xy.pyplot, plus their data files and a measured scorecard: 154/171 runnable cells (90%) after the gap-closing pass in the previous commit, 147/154 (95%) excluding the out-of-scope 3-D notebook, with matplotlib 3.11 at 171/171 on the identical code. The notebooks keep upstream's code style, so they are exempted from the repo lint rules that would amount to restyling third-party code.
…rome, and pinned Matplotlib 3.11 evidence (#37) * todo * ENG-10167 Expand xy.pyplot shim: plot-family depth, scales, export chrome, and pinned Matplotlib 3.11 evidence (#36) * Honor pyplot grid and legend options * feat(pyplot): pin Matplotlib 3.11 reference and audit compat contracts Replace the hard-coded plotting-surface assertion with a reviewed snapshot of the pinned upstream revision (bde111fb4e), kept in sync by scripts/sync_matplotlib_compat.py, which also generates the method-by-method compatibility matrix from compatibility.json and the executable corpus. A dedicated CI job checks the snapshot against a pinned Matplotlib checkout and runs the dual-engine corpus and optional-interop tests. Tighten the compatibility contract itself: formerly silent option discards now raise loudly (pie/quiver/barbs/contour/table/norm options and more), options the marks can honor are implemented instead of rejected (contour extent, plain Normalize, dashed stem/eventplot/ triplot, streamplot start_points/direction/widths, stepfilled hist), and streamplot always uses the shim's own bounded integrator so output no longer depends on whether Matplotlib is installed. Documented approximation levels, the accepted visual approximations, and a compatibility changelog; added contract tests covering rc chrome, color export, transforms, P3 options, and silent-drop regressions. * feat(pyplot): compare engines pixel-for-pixel and honor export chrome Turn the dual-engine evidence from crash-free execution into real comparison: all 54 corpus cases now render PNGs in both engines and compare normalized ink density and bounding-box geometry, semantic oracles extend to contours, triangulations, vector directions, masked scatter, RGBA images, and removable handles, and thresholds gain negative controls proving blank or wrong output fails. A mechanical scan of every public adapter rejects unexplained accepted-and-dropped options (explicit `compat-noop:` rationales required), and the reference CI job moves to the released matplotlib==3.11.0 wheel. On the export side, implement the common savefig options instead of rejecting them: bbox_inches="tight", transparent=, facecolor=, and PNG/SVG metadata= via standards-compliant text chunks. Static PNG export now honors the axes background token, and multi-panel HTML/SVG/PNG composition applies figure backgrounds and styled suptitles (size/weight/family/color/position) instead of dropping the kwargs. * feat(pyplot): stats-plot depth, nonlinear scales, and post-audit fixes Boxplot notches/bootstrap/user statistics, violin KDE bandwidths/ quantiles/sides, hexbin C aggregation, symlog/logit/asinh scales, secondary axes, and affine data transforms — with the divergences an adversarial cross-engine audit found in both this tranche and the previous one repaired rather than shipped silently: - scatter drops rows masked in x/y/s; fill_between(interpolate=True) draws single-point where-regions; imsave colormaps original values; set_cmap validates names and feeds imshow/scatter defaults; usermedians no longer shift notch CIs; boxplot sym works (empty string suppresses fliers); hexbin mincnt and C aggregation share one bin membership; violinplot survives constant data. - Loud rejections replace silent discards: bxp component linestyles, secondary-axis set_ticks extras, fraction-space transforms on data artists, html-format metadata; singular transforms fail at set_transform time. - Export: SVG/HTML honor savefig(facecolor=); single-chart SVG keeps the suptitle; suptitle y maps as a figure fraction; non-Latin-1 PNG metadata keys raise ValueError. - Scales: logit masks values at/outside (0,1); auto ticks refresh as data arrives and reset when the scale returns to linear; explicit ticks keep data-unit labels. - Docs state the real boundaries (rectangular hexbin geometry, HTML-only secondary axes, xy-owned tick locators, snapshot pin no longer CI-compared); the corrupted todo section is rewritten; the tight-bbox and negative-control tests can now actually fail. Excludes examples/pdsh notebooks (in-flight, currently failing ruff). * feat(pyplot): close the top PDSH benchmark gaps The Python Data Science Handbook ch. 4 benchmark (import-swap over 14 notebooks) scored 121/171 runnable cells; the failures clustered on a handful of missing surfaces rather than rendering gaps. This closes them: xy-owned tick locators/formatters resolved against live data limits, style.context with the stock sheets reduced to the supported rcParams subset, RdGy/jet engine tables, gci/clim with colorbars that track their mappable, GridSpec slice spans via explicit figure rectangles, and the pandas-facing Axes surface (get_figure, get_lines, shared-axes groupers, Period coordinates). Scatter vmin/vmax previously crashed at render; they now flow into a real color_domain on the engine's color channel. grid(linestyle= 'solid') injected an invalid style value and broke every later export of the axes. subplot()/axes() silently dropped keyword arguments. Benchmark after: 154/171 (90%); 147/154 (95%) excluding the out-of-scope 3-D notebook; zero savefig errors on passing cells. * docs(examples): ship the PDSH notebooks as a measured compat example Add the Python Data Science Handbook matplotlib-chapter code cells (MIT; the CC-BY-NC-ND prose is omitted) under examples/pdsh with only the import swapped to xy.pyplot, plus their data files and a measured scorecard: 154/171 runnable cells (90%) after the gap-closing pass in the previous commit, 147/154 (95%) excluding the out-of-scope 3-D notebook, with matplotlib 3.11 at 171/171 on the identical code. The notebooks keep upstream's code style, so they are exempted from the repo lint rules that would amount to restyling third-party code. * feat(pyplot): close the PDSH visual-parity majors (33 -> 8 measured) An image-level audit rendered every PDSH ch4 cell under both engines (per-cell seeded RNG, matplotlib 3.11 reference) and found that cells which *run* mostly did not *look* like matplotlib: 39 major divergences across 105 pairs. This lands the audit docs, the fix rounds, and the re-measured result: 34 match / 38 minor / 8 major over the 84 graded pairs (majors 33 -> 8 on the same notebooks). Legends render explicit label lists with line/marker samples and honor frameon; plot(x, y_2d) advances the color cycle; colorbars carry real count domains and nice ticks; every colormap stop table is resampled exactly from matplotlib 3.11 (plasma's padded tail merged discrete bands; RdBu was a coolwarm alias) in Python and the JS client; discrete N-band colormaps quantize marks and colorbars; contours dash negatives at matplotlib's level counts; insets keep their rect beside default axes in PNG; sharex/sharey accepts all/col/row with per-group domains and inner-label hiding via a new labels-only "off" strategy; annotate places text at xytext and draws a real arrow; tick labels keep locator precision; log axes label decades 10^k; a bounded TeX-subset converter turns $...$ into unicode across all shim text; scatter s= keeps matplotlib's absolute area semantics; errorbar color=/ecolor= resolve independently (regression caught by re-grading). The native rasterizer bakes greek/super-subscript/math glyphs, takes UTF-8 text, and rotates y-axis titles; the text wire changed, so ABI_VERSION bumps 32 -> 33 and the abi_smoke heatmap expectation moves to the corrected NaN-transparent/zero-opaque semantics. Explicitly out of scope, by direction: no runtime fallback onto real matplotlib, ever (seaborn-internal drawing paths fail loudly at import in a matplotlib-free environment and stay soft evidence only). Remaining boundaries are enumerated in the compat changelog. * test(pyplot): add future annotations import for the 3.11 floor gate CI's check_python_floor.py requires every annotated file to import `from __future__ import annotations`; the new color-pipeline regression tests were missing it. * feat(pyplot): close PDSH visual-fidelity follow-ups; make notebooks dual-engine Visual parity round on the shim and renderers: - Directional triangles and diagonal x get distinct glyphs in SVG, native raster, and WebGL instead of collapsing into up-triangle/plus. - clabel places bounded, separated labels along each contour level. - hexbin uses true offset-grid tessellation (nearest-center metric, complete zero-count lattice, six data-space triangles per cell) so bin membership agrees across browser, PNG, and SVG. - Horizontal bar/step/stepfilled histograms put counts on x with touching horizontal bins. - Legend borderpad/labelspacing/fancybox/framealpha/shadow/title reach all renderers; subplots_adjust now raises NotImplementedError for material values, removing the last known silent option discard. - Automatic ticks add the 2.5 nice step with shared fixed precision, loc='best' picks the least-occupied corner, and a four-sided frame with independently controllable spines ships to browser/PNG/SVG. Each PDSH notebook now interleaves a Matplotlib 3.11 reference cell before every xy cell so outputs compare side by side in one kernel run. sync_dual_engine_notebooks.py regenerates the mirrored cells and a test gates freshness. The notebooks are excluded from ruff format (the two plotting aliases differ in length, so near-limit lines wrap in only one copy — the formatter and the sync check cannot both pass) and F811 is ignored there since paired cells redefine names by design. * fix(export): isolate notebook HTML reprs in a sandboxed srcdoc iframe Notebook frontends treat rich-HTML payloads as fragments even when they contain a full <html> document, so the standalone export's global CSS (html,body resets, font-family) could restyle the surrounding notebook. notebook_iframe() wraps the document in a sandboxed srcdoc iframe sized from the figure, giving the repr a real document boundary while keeping the self-contained offline display path. Both Figure._repr_html_ surfaces (engine and pyplot shim) and pyplot.show() in IPython now route through it. * feat(pyplot): convert pt->px sizing and close axis()/tick-density parity gaps Matplotlib sizes chrome in points while the engine consumes CSS pixels; apply the 96-dpi 4/3 factor uniformly to fonts, line widths, marker diameters (edge width now widens the diameter as mpl does), spine widths, and tick geometry instead of passing point values through. Also close the remaining axis()/layout behavior gaps: full axis() mode set (auto/equal/scaled/image/square, bool/keyword forms) starting from margin-padded autoscale limits, tight domains that honor axes.{x,y}margin, an AutoLocator-style tick-count heuristic from panel size, frame tick marks in the render client, and a bbox_inches="tight"-style notebook repr for single-axes figures. * feat(pyplot): match Matplotlib marker geometry and add missing marker symbols Map ".", ",", "+", "x", and "d" to new dedicated symbols (point, pixel, plus_line, x_line, thin_diamond) instead of approximating them with circle/cross/diamond, and implement those symbols across all three renderers (WebGL shader, Rust raster SDF, SVG export). Triangles now use Matplotlib's normalized path (apex to full-width base) rather than the equilateral IQ SDF, unfilled plus/x markers derive their line width from markeredgewidth, and point/pixel keep Matplotlib's sizing semantics (half-size circle; fixed one-pixel rect). HTML legends render the trace's actual marker shape as the swatch instead of a color block. * Implement feature X to enhance user experience and fix bug Y in module Z * feat(pyplot): match Matplotlib pentagon marker geometry exactly Replace the analytic IQ pentagon SDF with a polygon SDF built from Matplotlib's Path.unit_regular_polygon(5), keeping the raster core, the WebGL marker shader, and the legend SVG path in lockstep so the pentagon marker matches Matplotlib's vertices instead of an approximation. * feat(pyplot): match Matplotlib scatter edge/face color and sizing Adopt Matplotlib's scatter defaults: edgecolors="face" (each point's edge resolves to its own face color, including per-point LUT/palette colors), marker size that includes the edge with the stroke centered on the path boundary, source-over edge compositing, default opacity 1.0, and collection alpha applied to face and edge independently. Suppress the implicit continuous-color "value" legend for unlabeled collections. Keep the raster core, WebGL shader, and SVG emitter in lockstep. * feat(pyplot): outline the full fill_between perimeter at collection alpha Add a stroke_perimeter style option to the area mark so fill_between outlines both its boundaries (the closed polygon) instead of only the value curve, drawn at the collection's line width and alpha to match Matplotlib's PolyCollection edge. Thread it through the public area API, the marks core, and the raster/SVG/WebGL renderers in lockstep. * feat(pyplot): bring contour/contourf and colorbars to Matplotlib parity Resolve marching-squares saddle ambiguity with the bilinear asymptotic decider so contours join like contourpy instead of a fixed table. Densify the contourf field bilinearly before band snapping so boundaries cross between grid points rather than stair-stepping cells, drop the band outlines, and carry the discrete levels/boundaries through to the colorbar. Convert contour linewidths from points to CSS px, scale the negative-level dash preset by width, and render dashes on the GPU via a path-walked arc-length pattern. Rebuild the colorbar as a banded bar with ticks, label, reserved layout room, and Matplotlib-style tick selection that keeps zero labeled. * fix(pyplot): honor equal aspect via the axes box and widen colorbar room Match Matplotlib's adjustable='box': for aspect='equal', shrink the axes rectangle through padding instead of expanding the data domain, so an explicit extent stays pinned to the plot edges rather than floating in blank space. Fold colorbar room into that box computation and the notebook-inline footprint, and widen the vertical colorbar gap and reserved space consistently across the live view, SVG, and raster paths. * fix(pyplot): upsample interpolated imshow to display resolution Raise the non-nearest imshow interpolation surface from 128² to a bounded 512², so smoothed images are at least display resolution in the notebook image box instead of each sample covering ~3x3 pixels. Nearest interpolation still keeps its source cells. * fix(pyplot): scale point-based sizes by figure DPI, not a fixed 4/3 Marker sizes, edge widths, tick geometry, font sizes, and offset-points annotations were converted with a hardcoded 4/3 (96 dpi) factor, while Matplotlib's default figure DPI is 100 — every default chart rendered slightly small and figure(dpi=...) was ignored. Route all conversions through Axes._point_scale() so they follow the owning figure's DPI. Also carried along from the same parity pass: - area marks gain a line_color style channel so hist(histtype= "stepfilled", edgecolor=...) draws its outline (plumbed through components/marks, raster, SVG, and the JS client) - vertical colorbar labels use writing-mode:vertical-rl instead of a rotate(-90deg) transform, which inflated notebook scroll width - default colorbar ticks target 8 divisions so small decimal domains get a usable scale in raster/SVG exports The static/ JS edits are rebuilt from js/src via node js/build.mjs. * fix(pyplot): implement axis("equal") as adjustable='datalim' Matplotlib spells axis("equal") as set_aspect("equal", adjustable="datalim"): it keeps the ordinary axes rectangle and expands the narrower data dimension around its center so one x unit and one y unit span the same number of pixels. The shim previously forced every equal-aspect request through adjustable='box', which shrank the panel instead and left tick 0 floating inside the frame. Track the adjustable mode per-axes and branch at build time: 'datalim' expands a data limit (surfaced through aspect_domains so the renderer's range padding can't move it), while 'box' keeps the existing rectangle-shrinking path used by scaled/image/square and by imshow. The inline-bbox crop in _mplfig only applies to the box path now. set_aspect() also accepts the adjustable/anchor/share keywords instead of silently dropping them: adjustable is honored, anchor/share are documented compat-noops, and unknown keywords raise TypeError.
Motivation
xy.pyplotshim so common pyplot/Axes/Figureworkflows run without importing Matplotlib, with visible mutations preserved and unsupported options failing loudly.Description
Shim surface (earlier commits): state helpers (
get_fignums,figtext,figlegend,subplot2grid,setp/getp,findobj, …), rc helpers (rc_context,rcdefaults, validatedset_cmap), dependency-freeBbox/get_position/GridSpec single-cell support, Figure layout (subplots_adjust,tight_layout, styledsuptitle/supxlabel/supylabel), artist mutation setters with segment-backed rebuilds, dependency-free PNGimread/imsave.Export:
savefigsupportsbbox_inches="tight"(true content-bbox crop) withpad_inches,transparent=,facecolor=(PNG pixels, SVG background rect, HTML container), andmetadata=(PNG tEXt/iTXt chunks, SVG<metadata>); figure and axes backgrounds reach PNG; suptitles reach PNG/SVG/HTML including single-chart SVG.Plot families: boxplot notches/bootstrap/user CIs/percentile whiskers/capwidths (statistics match
matplotlib.cbook.boxplot_stats, bootstrap bit-identical under a seed), violin Scott/Silverman/scalar/callable KDE bandwidths (matchesmlab.GaussianKDEto ~1e-16), quantiles and sides, hexbinC/reducers/mincnt, dashed hlines/vlines, interpolatedfill_between(incl. single-point regions), masked scatter rows (x/y/s/c),data=routes, symlog/logit/asinh scales (transform math bit-exact vs Matplotlib's), secondary axes, affine data transforms with exact vertex parity.Evidence infrastructure: 66-method Plotting inventory pinned as a generated snapshot against released
matplotlib==3.11.0(CI installs the wheel and asserts the version); all 54 corpus scripts execute under both engines and compare PNG geometry/ink; cross-engine semantic oracles for lines, bars, hists, images, triangulations, masked scatter, RGBA, removable handles; per-family perceptual IoU floors with a negative control tied to the live thresholds; a mechanical accepted-but-discarded-parameter scan; step-local CI workflow verification.Post-audit hardening: the branch was adversarially reviewed with cross-engine probes; every silent divergence found was fixed (masked x/y/s, interpolate wedges, imsave range handling, usermedians CIs, hexbin bin-membership consistency, logit ±inf, frozen nonlinear ticks) or converted to a loud rejection (fraction-space transforms on data artists, bxp linestyles, html metadata), and the docs state the real boundaries (rectangular hexbin geometry, HTML-only secondary axes, xy-owned tick locators, per-backend suptitle styling).
Real-world benchmark: Python Data Science Handbook ch. 4
Verbatim
import matplotlib.pyplot as plt→import xy.pyplot as pltswap across all 14 chart notebooks (173 code cells; 2 are IPython-magic-only and skip under both engines). Baseline note: the notebooks are bit-rotted for modern Matplotlib itself — after the four mechanical fixes any reader needs today (removed seaborn style names,cm.get_cmap, pandas.view), Matplotlib 3.11 passes 171/171. Against that baseline:xy.pyplot: 154/171 cells (90%) — 147/154 (95%) excluding the out-of-scope 3D notebook. (Baseline before the gap-closing commit: 121/171, 71%.) Every passing cell exports a non-empty PNG with zero savefig errors; spot checks show matching geometry and colors.
The final commit closes every one of the originally ranked top-seven gaps (locators/formatters,
style.context+ the stock sheets,RdGy,clim/gci,Axes.get_figureand the pandas-facing surface, colorbar handles with renderedticks=/extend=,GridSpecslice spans) plus a tail of one-offs (spine iteration,fig.canvas, scatterlw=,cycler,subplot_kw=, Period coordinates, mixingsubplot()into free-form figures). It also fixes three latent crashes found on the way: scattervmin/vmax(now a realcolor_domainon the engine's color channel),grid(linestyle='solid')poisoning the axis style, andsubplot()/axes()silently dropping kwargs.Remaining non-3D failures (7 cells) are documented boundaries: pandas' dynamic timeseries plotting (its private ordinal-axis locators; 3 cells), legend geometry options rejected loudly by policy (2), a second-
Legendimport (1), andaxhline(marker=)via seaborn (1). 04.12 (3D) dropped 9→7 becauseprojection='3d'now fails loudly at axes creation instead of returning a 2-D axes that fails later — the honest behavior for an out-of-scope feature.Testing
uv run pytest— 1632 passed (includes 54 dual-engine corpus cases, 15 reference-semantics oracles, and regression tests for every audit fix).ruff check/ruff format --check/ty checkclean on package code (2 pre-existingcolumns.pydiagnostics); workflow verifier and compat-snapshot freshness checks pass.examples/pdshnotebook set is still being cleaned up and is not part of this PR.