This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Charts: a vertical bar chart rotates any category label over 5 characters and cuts it at 12, even on a full-width report chart with room ("Qualificati…", "Needs Analy…") #13241
Copy link
Copy link
Closed
Labels
area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3
Description
Filing gate ① a product defect with reach measured. Measured while landing objectstack-ai/hotcrm#2073 item 10 (PR objectstack-ai/hotcrm#2198), on HotCRM main b8348e9a with @objectstack/* 18.0.0-next.5, in Chromium at 1440×900 on a locally booted, seeded install. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.
Reader (who acts): objectui triage routes it.
The finding, as measured
- Where: HotCRM Reports → Opportunities by Stage, as the dev admin. It is a vertical
barchart of amount by stage, with 6 categories, drawn 1044 px wide. - Expected: a full stage name on an axis that has room for it: about 170 px per category here. Shortening is for a band that cannot hold the name. objectui#11796's own direction was "truncate with a tooltip" for labels that do not fit.
- Actual: the axis labels are rotated, and two read "Qualificati…" and "Needs Analy…". The full name is only on the bar's tooltip.
- class: a. Reach: every vertical bar chart with more than one category whose name is over 12 characters, at any width. In HotCRM that is the Opportunities by Stage report. HotCRM's other stage charts use
horizontal-bar(chore(deps): bump actions/cache from 5 to 6 #2073 items 2 and 10), which draws every name whole.
Seat review
- Landing:
packages/plugin-charts/src/AdvancedChartImpl.tsxon objectuimain599a8a54b:hasLongLabels(about line 1823) turns rotation on when any label is over 5 characters, whatever the width.xAxisTickFormatter(about line 1910) then cuts every rotated label atROTATED_X_LABEL_MAX_CHARS = 12(line 428).- Neither step reads the band width, so a wide chart is treated like a narrow dashboard widget.
- The cap itself is deliberate: objectui#7247, and objectui#11796 for every rotated label. It bounds the axis band. This card asks only that the rotation and the cut depend on whether the label fits its band, not on its length alone.
Dedupe
Method: a grep of every objectui issue title (#7–#13153 from the repo-scoped issues?state=all listing, plus the issues updated since 2026-10-20) for rotat, truncat, ellips, x-axis and axis label.
-
20 hits. The nearest, all closed:
- objectui#7247: labels dropped on narrow widgets, fixed by objectui#7385;
- objectui#9431: above 5 buckets;
- objectui#11796: rotated labels clipped, fixed by objectui#11851, which introduced the cap.
None covers the cut on a chart wide enough for the name. New.
Metadata
Metadata
Assignees
Labels
area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 17 (
priority:p3)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13241-bar-label-fitWorktree:
objectui-issue-13241Domain:
domain:uiSeat:
domain:ui#2File surface (one root cause: rotation and the 12-character cut key on label length, not on the band width):
packages/plugin-charts/src/AdvancedChartImpl.tsx(hasLongLabels~1823,xAxisTickFormatter~1910,ROTATED_X_LABEL_MAX_CHARS~428);.changeset/13241-bar-label-fit.md(@object-ui/plugin-chartspatch).⛔ No new export or prop; the objectui#7247 / #11796 cap stays for a band that cannot hold the label. Stop on breach; explain in the report.
Container & model:
S,mode:subagent,model: opus(default tier; no path-derived mandate)Clause-②: no (internal chart layout)
Responsibility:
AdvancedChartImplrotates and cuts every label over 5 characters at any width | objectui#11796's "truncate with a tooltip when it does not fit" direction | every vertical bar chart with category names over 12 characters, measured on HotCRM Reports → Opportunities by Stage at 1044 pxThread-read: none
Serial constraints cleared:
5115c1f,AdvancedChartImplaxis ticks) landed.packages/plugin-charts/(read at 13:30Z).Claimed at: 2026-10-20T13:27Z
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 13241, "status": "done", "branch": "claude/issue-13241-bar-label-fit", "pr": "https://github.com/objectstack-ai/objectui/pull/13264", "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": true, "summary": "AdvancedChartImpl now measures its cartesian SVG surface with a ResizeObserver (a ref on the BarChart/LineChart/AreaChart root and on the combo's ComposedChart) and rotates a long x-axis label only when the widest drawn label (estimateLabelWidthPx, the same estimate the rotated band is sized from, plus an 8px gap) does not fit plotWidth / categories. Plot width = surface width - 10px recharts side margins - 48px per drawn value axis. When the label does not fit, the objectui#11796 cap (ROTATED_X_LABEL_MAX_CHARS, not raised) and the full name on the bar tooltip stay as before. Before a width is measured, the length rule applies unchanged; the width changes only the axis height, so there is no feedback loop. Mechanism assumption falsified: objectui#7385 measures nothing at runtime, so the measurement is new and lives inside AdvancedChartImpl. ChartContainer is untouched, and passing it a ref would replace its settle-observer ref. Ruling 2's 'tick tooltip or title, as today': today the full name is only on the bar tooltip, and that is kept with no tick title added. No new export or prop. Zero label writes: the dispatch named no labels, and objectui labels follow labeler.yml.", "tests": "HEAD 1e1578c: (1) vitest run --maxWorkers=1, new pin AdvancedChartImpl.labelFitBand-13241 + rotatedLabelBand-11796 + categoricalAxisDensity (7247/7386) + axisTickFill-13129: Test Files 4 passed, Tests 27 passed, exit 0. Pins: (a) 1044px, 6 stages up to 19 chars, whole and unrotated; (b) 360px, rotated, cut to 'Qualificati…'/'Needs Analy…', and a bar hover opens a tooltip reading 'Needs Analysis'; (c) unmeasured, the length rule holds. (2) Closure build pnpm --filter '@object-ui/plugin-charts^...' build: VERDICT command-exit 0. (3) pnpm --filter @object-ui/plugin-charts run type-check (tsc --noEmit plus tsconfig.test.json; listFilesOnly shows the new test 1x): exit 0. At 42c2a13, the 82 test files naming AdvancedChartImpl, one package dir at a time: plugin-charts 61 files 1046 passed / 9 skipped; plugin-dashboard 15/217; plugin-report 1/19; core 2/78; types 2/53; scripts/__tests__ 1/29; all exit 0. The later commit only inlines the useMemo and types the test mock. Ablation at 42c2a13 via objectstack scripts/ablation-replace.mjs (anchor hit 1 -> 0, blob 453da94 -> 2f756f7, marker grep on disk = 1): forcing 'labelsWantRotation = hasLongLabels' turned pin (a) red (1 failed | 2 passed). Restore: blob after restore 453da94 == HEAD blob, git diff HEAD empty. No dist rebuild was needed: the test imports ./AdvancedChartImpl from src. Lint narrowed: eslint --no-inline-config --format json on the 2 changed .ts/.tsx files, 2 files, 0 errors. AdvancedChartImpl.tsx has 42 warnings, equal to its base blob; the new test has 0. Population is repo eslint.config.js 'eslint .' (both files linted, not ignored). No parserOptions.project/projectService, so no type-aware linting and the diff cannot move untouched files. The full pnpm lint is left to CI. Battery all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, check-hand-rolled-comment-mask, check-changeset-fixed, check-changeset-presence, check:new-line-citations (0 new). check:spec-floors exit 1 'no-artifact' (workspace unbuilt; recorded, not fixed). Self-scan for control bytes: 0. Full directories of packages/plugin-charts, plugin-dashboard, plugin-report, core and types are left to CI. CI: in_progress / not awaited.", "mcp_calls": "0", "api_writes": "2 — fleet-write relay: pr_create (POST /repos/objectstack-ai/objectui/pulls draft + POST /repos/objectstack-ai/objectui/issues/13264/assignees), dispatched as POST /repos/objectstack-ai/objectstack/dispatches; os-dev-report comment POST /repos/objectstack-ai/objectui/issues/13241/comments via the same relay. Plus git pushes of the branch (3).", "gates": { "closure_build": 0, "type-check_plugin-charts": 0, "vitest_targeted_final_head": 0, "vitest_family_42c2a13": 0, "eslint_narrowed": 0, "check:phantom-deps": 0, "check:self-import": 0, "check:unreferenced-sources": 0, "check:doc-example-readers": 0, "check:handler-key-reads": 0, "check:side-effects-array": 0, "check:element-data-source-declaration": 0, "check:spec-symbols": 0, "check:action-forward-parity": 0, "check:designer-field-key-parity": 0, "check:icon-record-names": 0, "check:i18n-keys": 0, "check:i18n-drift": 0, "check:i18n-designer-parity": 0, "check:control-bytes": 0, "check-hand-rolled-comment-mask": 0, "check-changeset-fixed": 0, "check-changeset-presence": 0, "check:new-line-citations": 0, "check:spec-floors": "1 (no-artifact, recorded)" }, "files_changed": [ "packages/plugin-charts/src/AdvancedChartImpl.tsx", "packages/plugin-charts/src/AdvancedChartImpl.labelFitBand-13241.test.tsx", ".changeset/13241-bar-label-fit.md" ], "deviations": [ "The family run of 82 files ran at 42c2a13, not at the final head 1e1578c. The final head reran the 4 axis-pin files and the type-check. The diff between the two commits is a behaviour-identical inline of one useMemo plus test-mock typing.", "The PR body footer uses the role file's session-URL form, not the harness's '🤖 Generated with' form (the role file takes precedence)." ], "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT — PR #13264 @
1e1578c23dd0e18a4e45778ff36668b958ed027c·domain:uiseat 2 (session_01XVp19E1MYUTFM3dnkwz8gB)ChartContaineris untouched. No contract review is owed, and the seat read the report.AdvancedChartImplmeasures its cartesian surface with aResizeObserver(a ref on the Bar/Line/Area root and the combo'sComposedChart). It rotates a long x label only when the widest label (estimateLabelWidthPx, the same estimate that sizes the rotated band, plus an 8px gap) does not fitplotWidth / categories. The plot width is the surface width less the 10px side margins and 48px per value axis. When a label does not fit, the objectui#11796 cap (not raised) and the full name on the bar tooltip stay as before. Before a width is measured, the length rule applies unchanged. The width changes only the axis height, so there is no feedback loop.AdvancedChartImplfamilies across charts, dashboard, report, core and types (82 files) are green.@object-ui/plugin-chartspatch.Next: ready + auto-merge; landing is verified by content on
main.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gBmain9a4ddb2a95.9a4ddb2a95as at the accepted head. The merge message carries no model identifier.pm:dispatchedis removed.Generated by Claude Code
fix(plugin-charts): rotate and cut an x-axis label only when its band…