fix(font): the donut-centre KPI renders the weight it declares - #479
Merged
Conversation
A style names a font family and a decoration, and the decoration is what picks the face within the family. The standard-14 face constants — HELVETICA_BOLD, TIMES_ITALIC, COURIER_BOLD_OBLIQUE and the rest — are aliases of their family: FontLibrary rewrites each one to its base before any lookup, so naming a face contributes nothing. ChartDefaults named the bold face and set no decoration, so every donut chart without an explicit centre style drew its KPI regular and measured it with regular metrics. Nothing announced it — the text laid out and drew. That is the one place in the library where the form actually produced the wrong output; sixteen others named a face beside the decoration that was already doing the work, and four Typography tokens declared a weight the theme never applied. Those sixteen now name their family, which changes no render: sweeping the whole example catalogue before and after gives the same three documents either way. FontLibrary logs one line per distinct face constant it rewrites, so the silent no-op becomes visible without failing a caller whose decoration is correct. FontFaceResolutionTest pins the rule against the resolved PDFont rather than a rendered image, including the case where the name and the decoration disagree — which is the assertion that has to change if the alias is ever made a real face, making that a deliberate edit instead of a silent behaviour change. Three rendered documents move: the engine deck, the feature catalogue and the chart showcase, each on the page carrying a donut. The issue's own inventory needs correcting: of the five library sites it names, four already set the decoration and render bold today.
… own site Typography is a public record and headlineFont() is an observable value on a Stable surface — api-stability.md puts the BrandTheme factories and the layered presets there, changeable in major releases only. Rewriting four factories from a face constant to its family was render-neutral, which is not the same property as API-neutral, and none of it was needed to fix the chart. Reverted. FontFaceResolutionTest pinned the rule and not the site, so a style going back to naming the bold face with no decoration still satisfied every assertion in it. The chart layout tests do not close that either — they measure through fake metrics whose width follows the character count and ignores the face. The donut default is now asserted directly, through the glyph program its own style resolves to; reverting the fix turns it red with "expected Helvetica-Bold but was Helvetica". FontFaceAliasWarningTest covers the signal: one warning per face constant, not per lookup, naming both the constant and the family it resolves to, and silence for a style that names a family. It also states the limit in an assertion rather than in a comment — the warning is keyed on the alias alone, because the library cannot see the decoration where it rewrites the name, so the first use consumes the warning even when that use is correct. The engine-deck and feature-catalog thumbnails are restored. ShowcaseSync renders page 0 and the fix lands on pages 4 and 3; page 0 of both documents is pixel-identical before and after, so those two images were carrying drift from an earlier run, not this change. chart-showcase.png was already byte-identical, which is what prompted the check.
This was referenced Aug 2, 2026
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.
Closes #451.
Why
A style names a font family and a decoration, and the decoration is what picks the face within that family. The standard-14 face constants are aliases of their family —
FontLibrary.resolveBaseFontrewrites each one to its base before any lookup — so naming a face contributes nothing, and a style that names one and sets no decoration renders regular, measured with regular metrics. Nothing announces it: the text lays out and draws.ChartDefaults.DONUT_CENTER_TEXT_STYLEdid exactly that, so every donut chart without an explicit centre style drew its KPI in the wrong weight.The issue's inventory needs correcting, in both directions
The issue names five library sites. Four of them are fine —
HeadingBarStyle:21,TimelineBuilder:69,TimelineMarker:74andModernProfessional:134all set.decoration(BOLD)beside the alias and render bold today. So "every chart axis label, heading bar, timeline marker and those preset headings are currently regular weight in shipped output" does not hold for them.One is genuinely broken:
ChartDefaults:101. Measured by rendering the whole example catalogue before and after and comparing pixels page by page, fixing it changes three documents — the engine deck (page 4), the feature catalogue (page 3) and the chart showcase (page 5). Not "every chart".A hypothesis worth recording as refuted: the four
Typographyfactories that declare a face constant asheadlineFontlooked like a second, larger source. They are not. PatchingBrandTheme.headlineStyle()toBOLDchanges nine documents —classic-serif,minimal-underlined,boxed-sections,centered-headline,blue-banner— none of which are those four themes, and all of which are intentionally regular. The four themes' presets style their headlines themselves and never reach the token. It is a dead declaration, not a defect: rewriting all four to the base family moves nothing.What changed
ChartDefaultsnames the family and setsdecoration(BOLD)— the one real fix.Twelve other library sites that named a face beside a decoration already doing the work now name their family. This is render-neutral by measurement: the full-catalogue sweep gives the same three changed documents with or without these edits. They are worth changing because the library was teaching the trap in its own source, and because they are exactly what the new warning fires on.
Typography's four factory tokens are not among them. Render-neutral is not the same property as API-neutral:Typographyis a public record,headlineFont()is an observable value, andapi-stability.mdputs theBrandThemefactories and layered presets on the Stable tier — major releases only. Those four are unchanged, and the token remains a dead declaration to resolve in a major.FontLibrarylogs one line per distinct face constant it rewrites — once per name, not per lookup, so a document using the form a thousand times says so once. A full catalogue run emits five lines, one per alias still used inexamples/. Warning rather than failing: the form renders correctly when the decoration is right, and rejecting it would break those callers for nothing.Two tests, because the rule and the site need separate cover.
FontFaceResolutionTestpins the rule against the resolvedPDFont— the glyph program the page references — rather than a rendered image, so there is no pixel threshold to argue about. It covers the family+decoration path, the alias-without-decoration path, and the case where the name and the decoration disagree:HELVETICA_BOLDwithdecoration(ITALIC)resolves toHelvetica-Oblique. That last assertion is the one that must change if the alias is ever made a real face, which turns Option 1 into a deliberate edit to a named test instead of a silent behaviour change.It also asserts the donut default directly, through the glyph program that style resolves to — the rule alone does not catch the site going back, and neither do the chart layout tests, which measure through fake metrics whose width follows the character count and ignores the face. Reverting the fix turns it red with
expected "Helvetica-Bold" but was "Helvetica".FontFaceAliasWarningTestcovers the signal itself: one warning per face constant rather than per lookup, naming both the constant and its family, and silence for a style that names a family. It states the warning's limit as an assertion instead of a comment — the key is the alias alone, because the library cannot see the decoration where it rewrites the name, so the first use consumes the warning even when correct and a later broken one is silent. The specific sites are covered by the resolution test and byDocsBoldFaceGuardTest.Docs:
docs/font-coverage.mdgains the rule as its opening section, and theDocumentTextStyleJavadoc states it onfontName. The issue's "also update" note forbackend-capability-matrix.mddoes not apply — the resolution rule is unchanged.Verification
Reactor gate
BUILD SUCCESS, 13/13 modules, 2:01, zero errors.javadoc:javadocexits 0. Core 416 → 420 tests, render-pdf 139 → 143.Blast radius measured, not predicted — every claim above comes from rendering all 91 documents twice and diffing every page:
ChartDefaultsaloneChartDefaults+ all sixteen redundant rewritesBrandTheme.headlineStyle()→BOLD(rejected)Three committed previews and the showcase artifacts for those documents are refreshed. The other 96 files a full sync touches differ only by the render date and were reverted — as were the
engine-deckandfeature-catalogthumbnails, which cannot show this fix:ShowcaseSyncrenders page 0 and the change lands on pages 4 and 3. Measured rather than assumed — page 0 of both documents is pixel-identical before and after (mean 0.0000, max-pixel 0), so those two images were carrying drift from an earlier run.chart-showcase.pngwas already byte-identical, which is what prompted the check.One defect I introduced, caught by a test. The mechanical rewrite matched
Standard14Fonts.FontName.HELVETICA_BOLD— PDFBox's enum, not GraphCompose's — and swapped the face the PPTX watermark measures with, which is the seam #450 fixed.PptxChromeTest.theWatermarkFrameOrbitsThePdfBaselineRotationPivotfailed. Both renderer files are reverted whole: they are backend internals where a face name carries meaning, not style declarations in a document. NoStandard14Fontsreference appears in this diff.Not in scope
Option 1 from the issue — making the alias resolve to the requested face — is not taken. It changes the 97 example sites that use the form without a decoration, and it needs a precedence rule for a name and a decoration that disagree, which does not exist today. The test above pins the current answer so that decision stays available and explicit.
Only the PDF backend's resolution is pinned, though PPTX resolves through the same
FontLibrary. Whether the face constants should be deprecated onfontName(...)is the question the warning raises and does not answer — and the cleanest answer to the warning's alias-only key is probably to move the check to where the name and the decoration are both in hand, which is style construction rather than font resolution.