Found while fixing objectui#8266 (the measure axis of the same relay gap) by the dev seat on branch claude/issue-8266-dashboard-count-aggregate-datakey. Filed separately rather than fixed there: it is the CATEGORY half, it changes what renders for a different authoring shape, and objectui#8266's own dispatch scoped that PR to the series binding. objectui#8266 is not addressed by this card and this card is not addressed by objectui#8266 — neither fixes the other.
The defect
A dashboard chart widget bound to an object whose category is declared ONLY as aggregate.groupBy — with no options.xField — renders the category-axis refusal, naming a key the author never wrote:
This chart cannot plot its category axis: no row has a `name` field.
The author wrote groupBy: 'status'. Nothing on screen says groupBy is the key that was ignored, and name appears nowhere in their metadata.
The chain, on origin/main 0fa7a9c83
Both dashboard relays floor the category binding on a literal, without ever looking at the aggregate that decides it:
packages/plugin-dashboard/src/DashboardGridLayout.tsx:226 const xAxisKey = options.xField || 'name';
packages/plugin-dashboard/src/DashboardRenderer.tsx:607 const xAxisKey = options.xField || 'name';
The rows an object-bound aggregate returns are keyed by the RAW groupBy field (status), so no row carries name, and AdvancedChartImpl's hasNoCategoryKey guard (framework#4033) fires correctly on a binding that was wrong before it got there.
⭐ The tree already holds the resolver this needs, in the component one layer down: resolveChartCategoryField in packages/plugin-charts/src/ObjectChart.tsx reads aggregate.groupBy FIRST, then xAxisKey, then xAxis.field — that is objectui#8168's anti-drift move. ObjectChart uses it to decide whether to REFUSE, and then still forwards schema.xAxisKey verbatim as the render binding. So the same component both knows the category is declared and passes on a key that is not it. The contract's own derivation is also published — chartAggregateCategoryKey in @objectstack/spec/ui, the sibling of the chartAggregateValueKey that objectui#8266's fix adopted for the measure axis.
Measurement, not inference
Rendered through ChartRenderer at 480x320 (ResponsiveContainer mocked, the only harness in this repo that can count marks), over the rows a fieldless count actually returns, [{status:'open',count:2},{status:'paid',count:5}]:
xAxisKey |
series[0].dataKey |
result |
status |
value |
surface drawn, ticks open/paid, 0 marks, no refusal — this is objectui#8266 |
status |
count |
2 marks, y ticks 0..8 — objectui#8266 fixed |
name |
value |
refusal missing-category-key, text naming name |
name |
count |
refusal missing-category-key, text naming name |
The last row is the point: objectui#8266's fix does NOT reach this shape. A widget that declares aggregate.groupBy and no xField is still refused after it.
Why it is worth a card
Unlike objectui#8266 this one is LOUD, so it is a lesser harm — but the diagnostic is wrong-cause. It names a binding the author did not write and does not mention the one they did, so it points at the wrong layer. groupBy alone with no xField is a perfectly reasonable way to author a grouped chart, and the renderer already agrees it is a valid category declaration — it says so by not refusing at the ObjectChart level.
Scope note for whoever takes it
The fix shape is the mirror of objectui#8266's: have the relays resolve the category through one authority rather than a literal floor. Clause about moving pictures applies — a widget that renders a refusal today would start drawing, so it needs a render measurement, not only a seam assertion. Check also whether the label-resolution path (resolveGroupByLabels, which rewrites the groupBy column in place) leaves the resolved key still valid at the point the axis reads it.
Dedup
Run on the search_issues channel from this session against objectstack-ai/objectui, phrased as a description of the defect. The result was NON-EMPTY (20 hits), so it is self-validating and no separate control was needed. Nearest neighbours, none of them this: objectui#7547 (the same literal-floor class, but at the ListView / plugin-view ObjectView / app-shell ObjectView faces, not the two dashboard relays), objectui#4695 (a cartesian chart handed no series binding at all), objectui#4683 and objectui#4507 (the series-axis and pivot halves of the hasNoCategoryKey doctrine), objectui#8168 (the ObjectChart refusal this card's diagnostic comes from).
Refs: objectui#8266 · objectui#8168 · objectui#7547 · framework#4033.
Recorded by the objectui#8266 dev seat, session session_01YBWFb5YgMU5dw8p2VKj16S, from a render measurement taken in that card's worktree. This paragraph is prose rather than a footer block because issue creation strips attribution footers (AGENTS.md, the body-bytes section).
Found while fixing objectui#8266 (the measure axis of the same relay gap) by the dev seat on branch
claude/issue-8266-dashboard-count-aggregate-datakey. Filed separately rather than fixed there: it is the CATEGORY half, it changes what renders for a different authoring shape, and objectui#8266's own dispatch scoped that PR to the series binding. objectui#8266 is not addressed by this card and this card is not addressed by objectui#8266 — neither fixes the other.The defect
A dashboard chart widget bound to an object whose category is declared ONLY as
aggregate.groupBy— with nooptions.xField— renders the category-axis refusal, naming a key the author never wrote:The author wrote
groupBy: 'status'. Nothing on screen saysgroupByis the key that was ignored, andnameappears nowhere in their metadata.The chain, on
origin/main0fa7a9c83Both dashboard relays floor the category binding on a literal, without ever looking at the aggregate that decides it:
The rows an object-bound aggregate returns are keyed by the RAW
groupByfield (status), so no row carriesname, andAdvancedChartImpl'shasNoCategoryKeyguard (framework#4033) fires correctly on a binding that was wrong before it got there.⭐ The tree already holds the resolver this needs, in the component one layer down:
resolveChartCategoryFieldinpackages/plugin-charts/src/ObjectChart.tsxreadsaggregate.groupByFIRST, thenxAxisKey, thenxAxis.field— that is objectui#8168's anti-drift move.ObjectChartuses it to decide whether to REFUSE, and then still forwardsschema.xAxisKeyverbatim as the render binding. So the same component both knows the category is declared and passes on a key that is not it. The contract's own derivation is also published —chartAggregateCategoryKeyin@objectstack/spec/ui, the sibling of thechartAggregateValueKeythat objectui#8266's fix adopted for the measure axis.Measurement, not inference
Rendered through
ChartRendererat 480x320 (ResponsiveContainermocked, the only harness in this repo that can count marks), over the rows a fieldless count actually returns,[{status:'open',count:2},{status:'paid',count:5}]:xAxisKeyseries[0].dataKeystatusvaluestatuscountnamevaluemissing-category-key, text namingnamenamecountmissing-category-key, text namingnameThe last row is the point: objectui#8266's fix does NOT reach this shape. A widget that declares
aggregate.groupByand noxFieldis still refused after it.Why it is worth a card
Unlike objectui#8266 this one is LOUD, so it is a lesser harm — but the diagnostic is wrong-cause. It names a binding the author did not write and does not mention the one they did, so it points at the wrong layer.
groupByalone with noxFieldis a perfectly reasonable way to author a grouped chart, and the renderer already agrees it is a valid category declaration — it says so by not refusing at theObjectChartlevel.Scope note for whoever takes it
The fix shape is the mirror of objectui#8266's: have the relays resolve the category through one authority rather than a literal floor. Clause about moving pictures applies — a widget that renders a refusal today would start drawing, so it needs a render measurement, not only a seam assertion. Check also whether the label-resolution path (
resolveGroupByLabels, which rewrites the groupBy column in place) leaves the resolved key still valid at the point the axis reads it.Dedup
Run on the
search_issueschannel from this session againstobjectstack-ai/objectui, phrased as a description of the defect. The result was NON-EMPTY (20 hits), so it is self-validating and no separate control was needed. Nearest neighbours, none of them this: objectui#7547 (the same literal-floor class, but at the ListView / plugin-view ObjectView / app-shell ObjectView faces, not the two dashboard relays), objectui#4695 (a cartesian chart handed no series binding at all), objectui#4683 and objectui#4507 (the series-axis and pivot halves of thehasNoCategoryKeydoctrine), objectui#8168 (theObjectChartrefusal this card's diagnostic comes from).Refs: objectui#8266 · objectui#8168 · objectui#7547 · framework#4033.
Recorded by the objectui#8266 dev seat, session
session_01YBWFb5YgMU5dw8p2VKj16S, from a render measurement taken in that card's worktree. This paragraph is prose rather than a footer block because issue creation strips attribution footers (AGENTS.md, the body-bytes section).