Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-charts): draw every categorical x-axis label on short axes - #7385
Merged
Conversation
A vertical bar chart in a dashboard-width widget dropped most of its categorical x-axis labels: 3 bars drew 1 label and 5 bars drew 2, leaving the bars unnamed with no legend to fall back on. The x axis carried one policy for time and category alike — `interval='preserveStartEnd'` with a 48px `minTickGap` — which is correct for hundreds of dates and wrong for a band axis, where a dropped tick is an unrecoverable identity rather than a skippable sample. It is also keyed to the VIEWPORT (`isMobile`), never the widget, so a 200px chart in an 800px console was treated as a wide one. Short categorical axes now draw every bucket, rotating and ellipsising so the labels fit; longer axes keep the existing measured thinning.
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
hotlong
marked this pull request as ready for review
September 2, 2026 16:47
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 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 freeto 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#7247
The defect
AdvancedChartImpl's shared x-axis props applied one tick policy to time andcategory axes alike —
interval: 'preserveStartEnd'withminTickGap: 48—which is correct for hundreds of dates and wrong for a band axis. On a
categorical axis a tick is the bar's name: drop it and the reader cannot
recover it, because there is nothing to interpolate between neighbours and a
single-series bar chart has no legend to fall back on.
Two aggravating details found while reproducing it:
isMobile = window.innerWidth < 640),never to the widget, so a 174px chart inside an 800px console was treated as a
wide desktop chart and charged the full 48px inter-label gap;
y axis, which uses recharts' defaults (
minTickGap5) and keeps everylabel — the same data, the same widget, a complete axis.
The change
A categorical x axis of
X_AXIS_ALL_LABELS_MAX_BUCKETS(5) buckets or fewer nowdraws every label (
interval={0}), rotating so they fit and ellipsising anover-long name rather than letting it clip. Above the bound nothing changes:
recharts keeps thinning against its own measured text widths.
The bound is derived rather than chosen, and the derivation is in the code:
So at 5 buckets or fewer every label is guaranteed to fit at the narrowest width
the product ships, with nothing measured at runtime — which is what makes
interval={0}safe here though it was not safe as the blanket setting it usedto be (a hundreds-of-points series painted a dense black bar). That guard is
pinned by a test.
Change surface, declared
xAxisCommonPropsinpackages/plugin-charts/src/AdvancedChartImpl.tsx,which is the x axis of the bar / column, line, area and combo families
(its two call sites). The new truncating formatter is deliberately a separate
memo from
xTickFormatter, because that one also formats the horizontal-barfamily's category axis.
packages/plugin-charts/src/ChartImpl.tsx, the simplerbar renderer, already hard-codes
interval={0}with an angled tick and sayswhy in a comment. Untouched.
YAxis type="category"branch), which sizes its own width from the longest label and draws all of
them. Untouched, and pinned by a no-regression test.
family and is deliberately not part of this change), and the three polish items
the issue lists as ride-along candidates: fractional ticks on integer count
y-axes, radar and funnel label clipping, and the sankey's missing node labels.
None is the same defect, and each needs its own evidence.
minTickGap: 48for categorical axesabove the bound. It is the same root cause and would help a 6-plus bucket
chart at 290px, but the right replacement value is a judgement call rather
than something existing evidence pins, and its effect is not measurable in
this repo's DOM test environment (happy-dom reports zero text metrics, so
recharts' size-based thinning cannot be observed there — any test asserting it
would be a phantom). Filed separately instead.
Browser verification
ObjectUI console dev server against the showcase runtime on :3911, Chromium at
an 800px viewport — the three-column dashboard grid, so each chart's measured
plot box is 174px wide. Counted from the DOM
(
.recharts-xAxis-tick-labels .recharts-cartesian-axis-tick-value), not by eye.Before was measured by checking the one changed file back out to
origin/mainin the same worktree, with the fix already committed, and letting HMR reload;
the file was then restored from
HEADand the restore proven by hash (seeVerification below).
YellowGreen,Red,YellowBacklog,To DoBacklog,Done,In Progress,In Review,To DoBacklog,To DoHigh,UrgentHigh,Low,Medium,UrgentThe BEFORE column reproduces the issue's table exactly, at the same widths.
Verification
Everything below ran on the final commit,
git rev-parse --short HEAD=200063bc9.pnpm exec vitest run packages/plugin-charts/—Test Files 41 passed (41),Tests 383 passed (383)pnpm --filter @object-ui/plugin-charts type-check— exit 0 (tsc --noEmitechoed, so this is a real run and not a zero-match filter). Confirmed with
tsc --listFilesthat the new test file is in the type-check's file set.pnpm --filter @object-ui/plugin-charts lint— exit 0,303 problems (0 errors, 303 warnings), all pre-existingnode scripts/check-control-bytes.mjs—OK (scanned 6044 tracked text file(s))node scripts/check-vi-mock-specifiers.mjs,check-vi-mock-inherit.mjs— both OK(this PR adds a
vi.mock('recharts')call site)node scripts/check-changeset-fixed.mjs,check-changeset-no-major.mjs— both OKReverse verification. The regression test was written and run against the
unfixed source, before the policy existed: 5 of its 7 cases failed, with the
received values naming the missing labels —
The other 2 cases — high-cardinality thinning, and the horizontal family —
passed both before and after, which is the point of them: they pin behaviour
this change must not move. The dependency closure
(
pnpm --filter '@object-ui/plugin-charts^...' build) was built once before thered run and not rebuilt between red and green, and the subject under test is
imported from source, so both runs read the same tree.
The browser before/after used the sanctioned mutate-then-restore shape: the fix
was committed first, the mutation was
git checkout origin/main -- FILEand wasconfirmed on disk by blob hash (
2acc581d3toaceb44fcb) plus a zero count ofthe new policy identifier, and the restore was
git checkout HEAD -- FILE,proven by the on-disk hash matching the HEAD blob exactly and
git diff HEADbeing empty.
Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.
Declared deviation — the dispatched test command does not work in this repo.
pnpm --filter @object-ui/plugin-charts exec vitest run FILEis refused bythis repo's own invocation guard (objectui#3378): a package-cwd vitest run
resolves the root projects to nothing, runs
@object-ui/console's 22 filesinstead, and reports them green. Every run above was therefore started from the
repository root, which is what CI does.
Lint scope. Lint here is
turbo run lint, i.e. a per-packageeslint ., andthis diff touches exactly one package — so the package's own lint run above is
the whole lint surface of the change, not a narrowing. No cross-package or
type-aware rule is configured that could move another package's verdict from a
file in this one.
Generated by Claude Code