Skip to content

fix(pyplot): close the eight parity gaps from the PR #44 review - #50

Merged
FarhanAliRaza merged 4 commits into
mainfrom
pyplot-review-parity-fixes
Jul 15, 2026
Merged

fix(pyplot): close the eight parity gaps from the PR #44 review#50
FarhanAliRaza merged 4 commits into
mainfrom
pyplot-review-parity-fixes

Conversation

@FarhanAliRaza

Copy link
Copy Markdown
Contributor

Follow-up to #44, addressing all eight findings from its adversarial review. Every fix was verified against Matplotlib 3.11.0, and tests/pyplot/test_adversarial_parity_fixes.py pins the review's reproductions (plus a few neighbors: adjust-before-create, bymonth + interval striding, set_position vs subplots_adjust precedence).

Fixes

  1. P1 — MaxNLocator far-offset ranges (_ticker.py): removed the dv/maxabsv < 1e-12 early return in _scale_range; only a true zero span bails now. MaxNLocator(4).tick_values(1e9+1e-4, 1e9+2e-4) returns matplotlib's exact five ticks instead of two out-of-view ones.
  2. P1 — titled absolute panels (_mplfig.py): _charts builds titled panels 26/30 px taller and _panel_positions grows the placement upward to match, so the title lives in panel chrome and the plot box stays on its requested GridSpec rect.
  3. P1 — dataless-axis fallback (_axes.py): the (0, 1) empty view is applied to the render snapshot (x_props/y_props) only, never persisted into axis state; render-then-plot autoscales like matplotlib.
  4. P1 — GridSpec spans after subplots_adjust (_mplfig.py): span axes retain their originating SubplotSpec and are re-resolved by subplots_adjust; cell_rect falls back per-key to the figure's adjusted SubplotParams. One deviation from the review's suggested fix: 3.11 overrides a prior set_position on subplotspec-backed axes during subplots_adjust, so the spec is kept rather than cleared (test included).
  5. P2 — calendar interval phase (dates.py): interval filtering is anchored at matplotlib's view-derived dtstart (vmin − relativedelta(vmax, vmin), reimplemented without dateutil); month occurrence numbers stride all months so bymonth acts as an rrule filter.
  6. P2 — third-party locators (_axes.py): _nbins_hint is only written to xy Locator instances; slotted/frozen third-party locators render.
  7. P2 — annotation arrows (_axes.py): callout conversion gates on arrowprops is not None and detects offset placement from the entry itself, so empty labels and arrowprops={} keep their arrows (SVG exporter verified).
  8. P2 — tuple facecolors (_mplfig.py): figure facecolor normalizes through resolve_color at construction and in set_facecolor before any HTML escaping.

Verification

  • Full suite: 1,825 passed (17 new regression tests).
  • pre-commit run --all-files, ruff check, ruff format --check, ty check python: all green.
  • Each of the eight reproductions from the review re-run against this branch and compared with Matplotlib 3.11.0 output.

Independent adversarial testing against Matplotlib 3.11 found eight
reproducible defects in the notebook-parity seams; every reference
value below was re-verified against 3.11.0.

- MaxNLocator: keep scale/offset math for narrow far-offset ranges
  instead of bailing to (1, 0), which left the axis tickless.
- Absolute panels: axes titles widen panel chrome instead of eating
  26-30px of the requested plot rect (matplotlib never moves the axes).
- Dataless axes: the (0, 1) empty view applies to the render snapshot
  only; data plotted after a render autoscales as if never rendered.
- GridSpec spans keep their SubplotSpec so subplots_adjust re-resolves
  their rects; cell_rect falls back per-key to the figure's adjusted
  SubplotParams (subplots_adjust also overrides set_position, as 3.11
  does for subplotspec-backed axes).
- Calendar locators: interval counting is phased at matplotlib's
  view-derived dtstart (vmin - relativedelta(vmax, vmin)) rather than
  the 1970 epoch; month occurrences stride all months so bymonth acts
  as an rrule filter.
- Third-party locators are never mutated: the _nbins_hint density hint
  is only written to xy Locator instances.
- annotate arrows survive an empty label or empty arrowprops dict, and
  offset placement is detected from the entry itself, not the defaulted
  copy.
- Figure facecolor specs (RGBA tuples et al.) normalize through
  resolve_color before any HTML escaping.
@codspeed-hq

codspeed-hq Bot commented Jul 15, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 80 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing pyplot-review-parity-fixes (0d28e0d) with main (a7dc00c)

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.

…horing

Two visual parity defects against matplotlib 3.11 (PDSH 04.09 scenes):

- Annotation arrows started exactly at the text anchor, striking through
  their own label ("local minimum") and hiding inside bbox patches.
  matplotlib clips every annotation arrow at the text patch (patchA +
  shrink). The shared arrow geometry (python/xy/_arrowgeom.py +
  js/src/51_annotations.js, kept in sync) now accepts a `label_clear`
  "left,right,up,down" rectangle — the label's estimated extents around
  the start point — and trims the start to where the departure tangent
  exits it. annotate() emits it for data-coordinate arrows and offset
  callouts, sized from text length, font, ha/va, and bbox padding, with
  matplotlib's visible ~0.35em + 2pt gap.

- The DOM client top-anchored annotation labels while the SVG/raster
  exporters (and matplotlib) anchor the text BASELINE at its position, so
  client labels sat a line below the other renderers ("Halloween" under
  its arrow instead of beside it). The client default is now baseline
  (translate -100% + 0.35em), with va top/center/bottom unchanged.

- Bbox'd labels (annotate bbox=...) anchored by their box edge, eating
  matplotlib's clearance and overlapping ("Thanksgiving"/"Christmas");
  the padded box now grows outward around the anchored text edge, as a
  matplotlib bbox patch does.
@FarhanAliRaza

Copy link
Copy Markdown
Contributor Author

Added a second commit (187308a) fixing the annotation rendering defects found in the PDSH 04.09 notebook comparison:

  1. Arrows struck through their labels — arrows started exactly at the text anchor. The shared arrow geometry (_arrowgeom.py + 51_annotations.js) now takes a label_clear rectangle (the label's estimated extents) and trims the arrow start to where the departure tangent exits it, mirroring matplotlib's text-patch clipping with its visible ~0.35em + 2pt gap.
  2. Client labels sat one line below the other renderers — the DOM client top-anchored annotation labels while SVG/raster (and matplotlib) anchor the text baseline; the client default is now baseline.
  3. bbox'd labels anchored by box edge instead of text edge — the padded box now grows outward around the anchored text, restoring matplotlib's clearance (fixes the Thanksgiving/Christmas overlap).

Verified pixel-side against matplotlib 3.11 via headless-Chromium renders of the live client plus the SVG and native-PNG exporters. Full suite now 1,836 passing.

…first

The baseline-anchored arrow start from the previous commit put the shaft
under the label: matplotlib attaches annotation arrows at the text patch
CENTER (relpos default (0.5, 0.5)), clipped at the patch edge — measured
on 3.11.0: for "local minimum" at 13.89px the patch spans
anchor+[0,106]x[-11.0,+3.3] (y-down) and the arrow starts at
(+111.6, -5.5), not at the baseline.

The shared geometry gains `start_offset` ("x,y" px, applied before
control points and tangents); the shim emits anchor->box-center offsets
with box-centered symmetric `label_clear` extents. Rendered pixel
relations are now pinned quantitatively: the tests run the real pipeline
(entries -> spec -> px transform -> shared arrow geometry) and assert
the recorded matplotlib 3.11.0 start positions within a few px — both
the rightward (patch-edge exit) and up-left (patch-TOP exit) cases, so
future attach regressions fail in pytest instead of surfacing in
notebook screenshots.
…fixes

# Conflicts:
#	js/src/51_annotations.js
#	python/xy/static/index.js
#	python/xy/static/standalone.js
@FarhanAliRaza
FarhanAliRaza merged commit 9560978 into main Jul 15, 2026
10 checks passed
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.

1 participant