Skip to content

ENG-10167 Expand xy.pyplot shim: plot-family depth, scales, export chrome, and pinned Matplotlib 3.11 evidence - #36

Merged
Alek99 merged 6 commits into
matplotlib-shimfrom
codex/clone-matplotlib-from-github
Jul 13, 2026
Merged

ENG-10167 Expand xy.pyplot shim: plot-family depth, scales, export chrome, and pinned Matplotlib 3.11 evidence#36
Alek99 merged 6 commits into
matplotlib-shimfrom
codex/clone-matplotlib-from-github

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • Flesh out the Matplotlib-flavored xy.pyplot shim so common pyplot/Axes/Figure workflows run without importing Matplotlib, with visible mutations preserved and unsupported options failing loudly.
  • Pin the compatibility target to a reviewed Matplotlib 3.11 inventory and back the claims with executable, cross-engine evidence instead of assertions.

Description

Shim surface (earlier commits): state helpers (get_fignums, figtext, figlegend, subplot2grid, setp/getp, findobj, …), rc helpers (rc_context, rcdefaults, validated set_cmap), dependency-free Bbox/get_position/GridSpec single-cell support, Figure layout (subplots_adjust, tight_layout, styled suptitle/supxlabel/supylabel), artist mutation setters with segment-backed rebuilds, dependency-free PNG imread/imsave.

Export: savefig supports bbox_inches="tight" (true content-bbox crop) with pad_inches, transparent=, facecolor= (PNG pixels, SVG background rect, HTML container), and metadata= (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 (matches mlab.GaussianKDE to ~1e-16), quantiles and sides, hexbin C/reducers/mincnt, dashed hlines/vlines, interpolated fill_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 pltimport xy.pyplot as plt swap 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.

Notebook mpl xy before xy after
04.00 Introduction 8/8 7/8 8/8
04.01 Simple Line Plots 15/15 15/15 15/15
04.02 Scatter 8/8 7/8 8/8
04.03 Errorbars 5/5 5/5 5/5
04.04 Density & Contours 8/8 4/8 8/8
04.05 Histograms 10/10 8/10 10/10
04.06 Legends 11/11 8/11 8/11
04.07 Colorbars 13/13 6/13 13/13
04.08 Subplots 10/10 6/10 10/10
04.09 Text & Annotation 9/9 6/9 6/9
04.10 Ticks 11/11 4/11 11/11
04.11 Styles 15/15 6/15 15/15
04.12 3D (out of scope) 17/17 9/17 7/17
04.14 Seaborn (draws via real mpl) 31/31 30/31 30/31

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_figure and the pandas-facing surface, colorbar handles with rendered ticks=/extend=, GridSpec slice spans) plus a tail of one-offs (spine iteration, fig.canvas, scatter lw=, cycler, subplot_kw=, Period coordinates, mixing subplot() into free-form figures). It also fixes three latent crashes found on the way: scatter vmin/vmax (now a real color_domain on the engine's color channel), grid(linestyle='solid') poisoning the axis style, and subplot()/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-Legend import (1), and axhline(marker=) via seaborn (1). 04.12 (3D) dropped 9→7 because projection='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 check clean on package code (2 pre-existing columns.py diagnostics); workflow verifier and compat-snapshot freshness checks pass.
  • Benchmark artifacts (dual-engine notebook runner, per-cell results, PNG probes) live outside the repo; the examples/pdsh notebook set is still being cleaned up and is not part of this PR.

@codspeed-hq

codspeed-hq Bot commented Jul 13, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 17.93%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 2 regressed benchmarks
✅ 71 untouched benchmarks
⏩ 1 skipped benchmark1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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

Open in CodSpeed

Footnotes

  1. 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.

  2. No successful run was found on matplotlib-shim (5c14ce8) during the generation of this report, so main (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).
@FarhanAliRaza FarhanAliRaza changed the title Expand xy.pyplot shim: axes/figure features, artist mutations, PNG IO, and tests Expand xy.pyplot shim: plot-family depth, scales, export chrome, and pinned Matplotlib 3.11 evidence Jul 13, 2026
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.
@FarhanAliRaza FarhanAliRaza changed the title Expand xy.pyplot shim: plot-family depth, scales, export chrome, and pinned Matplotlib 3.11 evidence ENG-10167 Expand xy.pyplot shim: plot-family depth, scales, export chrome, and pinned Matplotlib 3.11 evidence Jul 13, 2026
@linear-code

linear-code Bot commented Jul 13, 2026

Copy link
Copy Markdown

@Alek99
Alek99 merged commit e871417 into matplotlib-shim Jul 13, 2026
14 of 15 checks passed
Alek99 pushed a commit that referenced this pull request Jul 14, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants