Skip to content

fix(charts): a grouped chart whose SECOND dimension was never projected says so - #4696

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4683-series-axis-refusal
Aug 15, 2026
Merged

fix(charts): a grouped chart whose SECOND dimension was never projected says so#4696
yinlianghui merged 1 commit into
mainfrom
claude/issue-4683-series-axis-refusal

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4683

Verified at HEAD bf0d7351e.

What was wrong

"Cannot know refuses loudly" was answered on the first dimension only. AdvancedChartImpl's hasNoCategoryKey (framework#4033) names an unprojected x-axis dimension instead of drawing a bare axis. The series axis had no counterpart, so a pivot whose SECOND dimension was absent from the result rows produced series: [] and rendered axes, grid, tooltip and legend around zero marks. To the author that reads as "no data matched", when what happened is that a dimension they grouped BY never arrived.

Measured on this branch's own fixture with the guard reverted: an svg, 0.recharts-rectangle elements, no warning, no text.

The signal, and the one it is deliberately not

The guard reads buildChartSeries' own output — series is an array that came out empty, and there are rows to draw against.

It does NOT read groupKey in row, which is #4507's trap inverted. This component is handed the PIVOTED bucket rows, whose columns are the group's VALUES, so the second dimension's own key is a column of no pivot ever — ordinary ones included. A key-in-row test here would refuse every grouped chart in the product. #4507 is its own card and is not addressed here; nothing in this PR touches the first dimension's guard beyond extracting the placeholder shell both refusals now render.

The refusal is limited to the families whose marks come from series and nothing else — bar, horizontal-bar, line, area, combo. Measured in the file rather than assumed: pie, donut, funnel, radar, scatter, treemap and sankey all fall back to series[0]?.dataKey || 'value' and draw a working chart with no series declared at all, so a refusal there would blank a good chart instead of explaining a broken one. That set is narrower than the existing CATEGORY_AXIS_CHART_TYPES, and the difference is load-bearing.

Why the message does not name the group key

hasNoCategoryKey names the key it could not find. This one cannot, and saying so is the honest answer rather than a limitation worked around. The renderer is not told which dimension the pivot grouped by, and the two upstream shapes that reach it are byte-identical — measured:

buildChartSeries(rows, ['status','priority'], ['est_hours']) // priority unprojected
=> {"data":[{"status":"Backlog"},{"status":"Done"}],"xAxisKey":"status","series":[]}
buildChartSeries(rows, ['status'], []) // no measure selected
=> {"data":[{"status":"Backlog"},{"status":"Done"}],"xAxisKey":"status","series":[]}

Naming one cause would print a sentence that is false half the time, which is worse than the silence it replaces. So the copy names the failure and BOTH authoring causes, and the console warning carries the same diagnostic pair the model prints: the axis the chart did plot, and the keys its rows actually carry. In the card's shape that second half is the tell — bucket rows carrying the category and nothing else.

Plumbing the pivoted dimension into the renderer would allow a key-naming message; it is costed as an open question in the report rather than taken unilaterally, because it spans three packages and introduces a prop a future call site can forget — the blindable-guard class #4507 is about.

The three-way distinction, each arm pinned

Also pinned: the x-axis refusal wins when both apply (one cause, one diagnosis, one warning); series === undefined is left byte-for-byte alone; an empty result set is still the chart's own empty state.

Copy and i18n convention

Mirrors the model exactly, measured first: the framework#4033 guard produces a hardcoded English string in JSX plus a hardcoded console.warn, with no locale key. This guard matches that convention, so no locale-pack key was added and check:i18n-keys / check:i18n-drift stay clean.

Verification (HEAD bf0d7351e, clean tree)

gateresult
pnpm exec vitest run packages/core/ packages/plugin-charts/117 passed (117) files, 2073 passed (2073) tests
new file alone11 passed (11)
pnpm exec vitest run packages/plugin-dashboard/ (downstream consumer of plugin-charts, run by name)58 passed (58) files, 462 passed (462)
pnpm --filter '@object-ui/plugin-charts^...' build then pnpm --filter @object-ui/plugin-charts type-checkexit 0 (closure built first; the package has type-check, not typecheck)
pnpm exec eslint --quiet on both changed filesclean
node scripts/check-changeset-presence.mjsOK, 1 changeset declared
pnpm run check:control-bytesOK (4225 files)
check:i18n-keys / check:i18n-drift / check:phantom-depsOK

Reverse verification, direction predicted before running: ablate the guard (git checkout origin/main -- AdvancedChartImpl.tsx from the committed state) and exactly the three refusal-arm tests go red while the eight draw/unchanged pins stay green. Observed: 3 failed | 13 passed across the ablated pair of files — the predicted three, named placeholder, warning, family sweep. Restored from the branch and re-run green.

One flaky failure appeared in the first plugin-dashboard sweep (DatasetWidget.dottedDimension.test.tsx:269, expected [] to deeply equal [ 'crm_opportunity' ]). Measured on both sides — ablated full suite green, restored full suite green, file alone green — so it is not this change. It is the second site of the race already filed as #4487, and is attached there rather than re-filed.

Findings filed

#4692 remains open and is untouched; the click-handler region #4694 landed was not edited.


Generated by Claude Code

…ed says so
"Cannot know refuses loudly" was answered on the first dimension only.
`hasNoCategoryKey` (framework#4033) names an unprojected x-axis dimension
instead of drawing a bare axis; the series axis had no counterpart, so a pivot
whose second dimension was absent from the result rows produced `series: []`
and rendered axes, grid, tooltip and legend around zero marks - to the author,
indistinguishable from "no data matched".
The signal is `buildChartSeries`' own output (`series: []` with rows to draw
against), NOT `groupKey in row` - which is objectui#4507's trap inverted: this
component is handed the PIVOTED bucket rows, whose columns are the group's
VALUES, so the second dimension's key is a column of no pivot ever and such a
test would refuse every grouped chart in the product.
Limited to the families whose marks come from `series` and nothing else (bar,
horizontal-bar, line, area, combo) - measured in this file: pie, donut, funnel,
radar, scatter, treemap and sankey fall back to `series[0]?.dataKey || 'value'`
and draw a working chart with no series at all, where a refusal would blank it.
The message names the failure and BOTH authoring causes rather than the group
key, because the renderer is not told it and the two upstream shapes are
byte-identical: an unprojected second dimension and a chart with no measure
selected both return `{data:[{status:'Backlog'},...], xAxisKey:'status',
series:[]}`. Naming one would be a sentence that is false half the time.
The three-way distinction stays pinned: null / empty-string groups DRAW
(objectui#4673), a partially projected key draws what projects - mirroring
`hasNoCategoryKey`'s own `!rows.some(...)` - and an ordinary pivot is unchanged.
Fixes#4683
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnQd8iMMUwXQEV1crFmQiQ
@vercel

vercelBot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 15, 2026 10:32am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-CkVjEjmE.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)489.91KB108.67KB
core (index.js)3.79KB1.52KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)163.56KB44.83KB
fields (index.js)230.37KB57.17KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.86KB12.91KB
plugin-charts (index.js)64.44KB18.28KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)121.90KB31.76KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)239.93KB60.01KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.72KB27.70KB
plugin-gantt (index.js)164.30KB40.02KB
plugin-grid (index.js)192.10KB51.24KB
plugin-kanban (index.js)52.74KB14.53KB
plugin-list (index.js)111.82KB27.23KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.38KB11.09KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.64KB9.44KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.26KB0.67KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 15, 2026 10:49
@yinlianghui
yinlianghui added this pull request to the merge queueAug 15, 2026
Merged via the queue into main with commit e132433Aug 15, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4683-series-axis-refusal branch August 15, 2026 10:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An unprojected SECOND dimension draws a grouped chart with no series and says nothing — hasNoCategoryKey has no counterpart on the series axis

2 participants

@yinlianghui@claude