Issue #618: align Y axis tick labels on advance width#1015
Merged
Conversation
AxisTickLabels measured each tick label by the ink width of its glyph
outline but drew the label by translating to the computed x position and
filling that same outline, whose ink begins at the left side bearing. The
bearing was dropped from the measurement yet still honoured when drawing,
so a right-aligned label's rendered edge landed at
xOffset + maxTickLabelWidth + leftSideBearing
rather than at a constant x. Digit 1 has by far the largest bearing of any
digit, so labels beginning with 1 drifted right of their neighbours. The
error is ~9.9% of the font size, which is the ~1px wobble in the report and
~3px at 30pt.
Measure and align on advance width instead. The advance is the distance the
pen moves, so it accounts for both side bearings and places each glyph
exactly where the font intends; digit advances are tabular, so equal-length
labels now share a drawing origin and the labels form a true column. It also
means the column reserves room for the last glyph's right bearing, which the
ink-width measurement did not.
The width and alignment maths now live in TickLabelMetrics, shared by
AxisTickLabels and ColocatedSlaveLabels, which had drifted apart: the latter
aligned using the integer shape.getBounds() while measuring its column with
TextLayout.getBounds(), adding up to a further 1px of quantisation.
Left and Centre alignment were affected by the same root cause and are fixed
by the same change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The test asserted that the pre-fix ink-width alignment drifted by more than 2px. That figure is specific to the SansSerif face on macOS; CI's Linux font drifts 1.83px, so the assertion failed there. The exact drift in px is a property of the font, not of the fix. Assert the contrast between the two algorithms instead: under ink-width alignment the rendered right edges of equal-length labels differ at all, while under advance alignment their drawing origins are identical. That holds on any font whose digits have differing side bearings. Also scope two claims that are true for text faces but not universal — a survey of the 187 fonts on this machine found 16 script faces (Zapfino, Brush Script) whose glyphs ink wider than they advance. Both tests pin the logical SansSerif font, so they are unaffected; the comments no longer imply the property holds everywhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #618.
The bug
setYAxisLabelAlignment(TextAlignment.Right)did not right-align every Y-axis tick label — labels beginning with1sat visibly right of their neighbours.AxisTickLabelsmeasured each label by the ink width of its glyph outline (outline.getBounds2D().getWidth()) but then drew it by translating to the computed x position and filling that same outline. The outline's ink does not begin at its origin — it begins at the left side bearing,getBounds2D().getX(), which the measurement discarded but the fill still honoured. The rendered right edge therefore landed atxOffset + maxTickLabelWidth + leftSideBearinginstead of at a constant x.Digit
1has by far the largest bearing of any digit, which is why it was the visible outlier. Measured at SansSerif 30pt over labels0–14:01410–14The right edge tracks the bearing one-for-one. The spread is 2.97px, and it scales linearly at 9.9% of the font size — the ~1px wobble in the original report at default size, much worse on large fonts and HiDPI.
LeftandCentrewere affected by the same root cause.The fix
Measure and align on advance width instead. The advance is the distance the pen moves, so it accounts for both side bearings and places each glyph exactly where the font intends. Digit advances are tabular, so equal-length labels now share a drawing origin and the labels form a true column:
The width and alignment maths now live in a shared
TickLabelMetrics, because the two call sites had drifted apart:ColocatedSlaveLabelsaligned using the integershape.getBounds()while measuring its column withTextLayout.getBounds(), adding up to a further 1px of quantisation.Note on what "right-aligned" means here
This aligns advance boxes, not ink edges — so
1, being a narrower glyph, sits slightly inset within its tabular advance box rather than flush. That is deliberate and is what a spreadsheet column does: it keeps the tens digits of10–14in a single column, which pure ink alignment would break. Residual ink raggedness drops from 2.97px to 1.76px at 30pt; what remains is right-side bearing, a property of the glyph outlines that tabular alignment neither can nor should remove.Verification
RegressionIssue618Test— 7 headless tests asserting the geometry directly (noXChartPanel, so it runs on CI): equal-length labels share an origin, the advance column is exact,Left/Centreinvariants hold, rendered ink stays inside the column, and the ragged edge is strictly smaller than the pre-fix maths.TestForIssue618— a standalone demo following the existingstandalone/issuesconvention; prints both old and new alignment maths, then displays the chart.xchartsuite: 161 tests, all passing.Before / after, bottom of the Y axis at 30pt —
1no longer sticks out:before:
1sits ~3px right of0,2,3,4after: digits form a clean column
🤖 Generated with Claude Code