Fix clipped colorbar labels [mpl compatibility] - #268
Conversation
The native raster exporter drew a vertical colorbar's label horizontally above the bar at `plot.y - 5`, so the glyph ascent overflowed the canvas top edge and the text was clipped. Matplotlib rotates the label 90° CCW and centers it alongside the bar, outboard of the tick labels. The placement was chosen on the assumption that the native text primitive could not rotate. It rotates in quarter turns (TEXT_ROTATED / TEXT_ROTATED_CW, already used for rotated axis titles), so 90° is exact here — only arbitrary text angles still fall back to upright glyphs. The label now shares the SVG exporter's baseline (`x + width + 38`), so the two static paths cannot drift, and it sits inside the extra right-margin room `layout()` already reserves for a labeled colorbar. Browser and SVG output are unchanged; both were already correct. Clears the three visually wrong PDSH ch. 04.05 cells (hist2d, hexbin and imshow colorbars with labels). Composition-API colorbars built with `xy.colorbar(title=...)` share the renderer, so their vertical labels change in PNG the same way; horizontal labels are untouched.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Merging this PR will not alter performance
Comparing Footnotes
|
Summary
spec/api/styling.md, the compatibility matrix, and the compatibility changelogWhy
PDSH ch. 04.05 sets a colorbar label three times —
hist2d+set_label,hexbin(label=...), andimshow+set_label. Matplotlib draws that label rotated a quarter turn alongside the bar and centered on it. xy drew it as a horizontal title above the bar, with the top of every glyph sheared off at the canvas edge. Nothing raised, so no corpus test caught it: the label text was present, just in the wrong place and partly outside the image.Root cause
python/xy/_raster.py:2103(pre-fix) placed the vertical label at(x, plot.y - 5)with no rotation flag, justified by this comment:Both halves were wrong. The native text primitive does rotate —
TEXT_ROTATED/TEXT_ROTATED_CW(src/raster.rs:1188), surfaced as_TEXT_ROT_CCW/_TEXT_ROT_CWand already used for rotated y-axis titles. And the chosen spot does not stay inside the canvas:plot.yis the top margin, so a baseline atplot.y - 5puts the glyph ascent (~0.78em) above row 0. Decoding the exported PNG shows label ink in device rows 0–9, i.e. clipped.The label now emits at
x + width + 38— the SVG exporter's existing baseline — centered along the bar with_TEXT_ROT_CCW, inside the extra right-margin roomlayout()already reserves whenever a vertical colorbar has a label.Rotation fidelity
Quarter turns are exact in the native rasterizer, so this label is fully faithful there. #243's disclosure still stands for arbitrary angles — those render as upright glyphs in native PNG — but a colorbar label is always 0° or 90°, so it never hits that limit. No new fidelity caveat.
Impact
js/src/50_chartview.ts:1927,writing-mode: vertical-rl) and the SVG exporter (_svg.py,rotate(-90 …)) were already correct: right side, rotated, on canvas. Both are deliberately untouched, so no unbuildable TypeScript ships in this PR (see Validation).xy.colorbar(title=...)renders its vertical label through the same code, so its PNG output changes the same way — from a clipped title above the bar to a rotated label beside it. That is the intended appearance in both surfaces, and it is now specified rather than incidental.Scope
Colorbar placement —
location,anchor,shrink, minor ticks, and masked-sample domain inference — belongs to #246, which documents it under### Colorbar placement and ticks. This PR touches none of it: no placement option is read or added, and the bar's ownx/y/width/heightare computed exactly as before. My spec additions live in a separately named### Colorbar label orientationsubsection and a new compatibility-matrix row, so they should not collide with #246's text.Honest limits of the comparison image: the
hist2drow's plot body is still wrong in xy (coarse blocks over a 0–1 domain rather than a 30-bin 2-D histogram). That is an independent, separately ownedhist2dbinning/domain defect, unaffected by this PR and not claimed as fixed here. Thehexbinandimshowrows are visually correct after this change.Deliberate follow-ups
Two more gaps from the same PDSH audit are left out rather than bundled — neither is a few lines, and neither shares this PR's theme:
from xy.pyplot import TriangulationraisesImportError(04.12 cell 17). xy exposes atriangles=shorthand but noTriangulationclass; a faithful shim needs Matplotlib's implicit Delaunay triangulation whentrianglesis omitted, which is real work. Worth noting that fixing the import alone would not make the cell render — it then hits xy's deliberate 3-D rejection.axhline(marker=...)raisesTypeError: unsupported keyword(s): marker, reached through seaborn'sFacetGrid.map(plt.axhline, ...)(04.14 cell 31). Matplotlib accepts and applies it. This is a different subsystem (span-line kwarg threading) from colorbar label geometry.Validation
New tests assert real emitted geometry — the raster command stream and SVG coordinates — not screenshots:
test_native_vertical_colorbar_label_is_rotated_beside_the_bar_inside_the_canvas— anchor is exactly1 | _TEXT_ROT_CCW, baseline is outboard of every tick label,yis the bar's midpoint, and the glyph box stays under the canvas widthtest_native_vertical_colorbar_label_leaves_the_canvas_edges_unpainted— decodes the exported PNG and asserts no ink on any border row/column (the clipping symptom itself)test_native_horizontal_colorbar_label_stays_upright_below_the_bar— guards against over-rotatingtest_svg_vertical_colorbar_label_is_rotated_beside_the_bar_inside_the_canvas— assertsrotate(-90 …),text-anchor="middle", containment, and that the native exporter emits the identical baselinetest_colorbar_set_label_renders_rotated_beside_the_bar_in_both_exports— the PDSH path end to end throughxy.pyplotFull suite, before and after this branch:
commover the two sortedFAILEDlists is empty in both directions — an identical failure set, +5 passing, which are exactly the five new tests.Not verified
Reported precisely rather than glossed:
python/xy/static/bundles, andnpm/nodeare not installed, so every test that reads the JS client fails on a pristine tree. I baselined the pristine tree to confirm the set is unchanged rather than attributing any of them to this change. Five further modules cannot even be collected without the bundles and were excluded from both runs.node js/build.mjswas not run and the TypeScript is not typechecked or bundled here. This PR deliberately contains nojs/changes, so nothing in the render client is left unverified by that gap — but I cannot claim a JS build pass the way Fix pie labels and boxed text [mpl compatibility] #243 and Fix colorbar gallery placement and domains [mpl compatibility] #246 do.ty checkdiagnostics across the package are pre-existing (10 are unresolved imports from the same missing bundles); none are in the files this PR touches, andty checkonpython/xy/_raster.pyis clean.beforepanels are real pre-fix code — the packaged_raster.pyfrom the branch point, rendered throughPYTHONPATH— not a monkeypatch.