Improve pyplot legend and dash parity - #42
Conversation
Alek99
left a comment
There was a problem hiding this comment.
Reviewed the full diff (Python shim, payload/exporters, JS client) and dug into the CI failures.
CI status — not this PR's fault
Both failing checks (Test test_pyplot_build_tracks_declarative[10000-60-1.6] and CodSpeed test_first_payload_hexbin_core_2d) are pre-existing regressions from #37 — they fail identically on main (5a58834, eb0c3f7, and still on c36e908). Fixes are in #46. I verified locally that this branch merged with #46 passes all 558 pyplot/plot-family tests plus both previously-failing gates. Once #46 lands, update the branch and CI should go green. One thing to watch: python/xy/static/*.js are committed build artifacts and git will happily automerge them — after rebasing, re-run node js/build.mjs (for the current merge I verified the automerge result matches a fresh rebuild exactly).
What I verified works
_mpl_dashscaling is right:--atlinewidth=2ships[10.28, 4.44]= (3.7, 1.6) × lw × dpi/72, matching Matplotlib'sscale_dashes+ point sizing; line width point-scaling is consistent between the trace and the frozen legend item (no double scaling).- loc parsing (independent h/v anchors with "center" fallback) is consistent across JS, SVG, and raster, and the single-
transformfix in_legendBoxis correct — the old code's twotransform:declarations would indeed clobber each other. _best_legend_locstays bounded (512 samples/entry) and the ±0.02 tie band correctly preserves Matplotlib's candidate preference order.- Second-legend payload shape (
extra_legendswith explicit items) renders in all three renderers and survives primary-legend suppression; good test coverage for both.
Minor findings
Axes.add_artist(_axes.py):artist._parent = hostis reassigned after theLegendalready froze_optionsand_itemsagainst the constructing axes in__init__— the assignment has no effect. If aLegend(ax1, ...)is added toax2with a different dpi/rcParams state, the swatch scaling silently comes fromax1. Either drop the dead assignment or recompute options on attach.- JS
_legendBox: only the primary legend registers asthis._legend, so extra legend boxes never get theirmax-heightrefreshed by_resize— a window resize can leave a tall second legend overflowing the plot. Consider tracking all boxes. - JS line swatch:
it.style?.width || 1.5treats an explicitlw=0as unset and draws a 1.5px handle;?? 1.5(plus letting 0 render nothing) is closer to mpl. Legend.__init__:zip(handles, labels, strict=False)silently truncates a handles/labels length mismatch — Matplotlib warns. Awarnings.warnhere would match shim policy of not deciding silently. Handles without an_entry(e.g. Text) are also silently dropped.- Nit: in
_best_legend_locthe finalreturn "upper right"is unreachable —bestis the min ofscores, so the first candidate within the band always returns.
None of these block merging; 1 and 2 are worth a follow-up. Nice work on keeping the three renderers' legend geometry in lockstep — the shared item shape makes the parity easy to trust.
- Legend now (re)freezes options and swatch scaling via _attach() when added to an axes, so a legend constructed against one axes but attached to another picks up the host's dpi/rcParams (review #1). - Legend.__init__ warns on handles/labels length mismatch and on handles without a legend entry instead of dropping them silently (review #4). - JS tracks every legend box (not just the primary) so _resize refreshes max-height on extra legends too (review #2). - JS line swatch uses ?? so an explicit lw=0 draws nothing instead of falling back to 1.5px (review #3). - Drop unreachable return in _best_legend_loc (review #5).
Merging this PR will not alter performance
Comparing Footnotes
|
No description provided.