Skip to content

Issue #618: align Y axis tick labels on advance width#1015

Merged
timmolter merged 2 commits into
developfrom
issue-618-yaxis-label-alignment
Jul 20, 2026
Merged

Issue #618: align Y axis tick labels on advance width#1015
timmolter merged 2 commits into
developfrom
issue-618-yaxis-label-alignment

Conversation

@timmolter

Copy link
Copy Markdown
Member

Fixes #618.

The bug

setYAxisLabelAlignment(TextAlignment.Right) did not right-align every Y-axis tick label — labels beginning with 1 sat visibly right of their neighbours.

AxisTickLabels measured 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 at xOffset + maxTickLabelWidth + leftSideBearing instead of at a constant x.

Digit 1 has by far the largest bearing of any digit, which is why it was the visible outlier. Measured at SansSerif 30pt over labels 014:

label bearing ink width rendered right edge
0 1.904 15.176 33.473
1 4.512 11.572 36.080
4 1.538 15.366 33.106
1014 4.512 ~30 36.080

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

Left and Centre were 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:

label   advance   oldRightEdge   newOrigin
0        19.000         33.473      19.000
1        19.000         36.080      19.000
4        19.000         33.106      19.000
10       38.000         36.080       0.000
14       38.000         36.080       0.000

The width and alignment maths now live in a shared TickLabelMetrics, because the two call sites had drifted apart: ColocatedSlaveLabels aligned using the integer shape.getBounds() while measuring its column with TextLayout.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 of 1014 in 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 (no XChartPanel, so it runs on CI): equal-length labels share an origin, the advance column is exact, Left/Centre invariants 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 existing standalone/issues convention; prints both old and new alignment maths, then displays the chart.
  • Full xchart suite: 161 tests, all passing.

Before / after, bottom of the Y axis at 30pt — 1 no longer sticks out:

before: 1 sits ~3px right of 0, 2, 3, 4
after: digits form a clean column

🤖 Generated with Claude Code

timmolter and others added 2 commits July 20, 2026 14:48
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>
@timmolter
timmolter merged commit 1fa9991 into develop Jul 20, 2026
1 check 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.

AxesChartStyler.setYAxisLabelAlignment(TextAlignment.Right)

1 participant