Skip to content

fix(plugin-dashboard,core): bind an object-bound chart's series to the column the aggregate projects - #8272

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8266-dashboard-count-aggregate-datakey
Sep 7, 2026
Merged

fix(plugin-dashboard,core): bind an object-bound chart's series to the column the aggregate projects#8272
os-justin merged 2 commits into
mainfrom
claude/issue-8266-dashboard-count-aggregate-datakey

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8266

It reproduces — measured before anything was written

The card was source-derived and said so; its own instruction was to reproduce first and close it if the chain did not hold. It holds, and here is the measurement rather than the derivation.

Seam (both relays, through the real render path, recorder registered for object-chart): a widget with provider: 'object' and aggregate: { function: 'count', groupBy: 'status' } composed series: [{ dataKey: 'value' }] on DashboardGridLayout AND on DashboardRenderer, over an aggregate the row projection keys 'count'.

Render (ChartRenderer at a fixed 480x320, over aggregateRecords(..., {function:'count', groupBy:'status'}) output [{status:'open',count:2},{status:'paid',count:5}]):

series[0].dataKey marks refusal empty state
value — what the relays composed 0 .recharts-bar, 0 .recharts-rectangle none none
count — the column the rows carry 1 .recharts-bar, 2 .recharts-rectangle none none

Same rows, same harness, one key apart. The x axis drew open / paid in both. So the failure is real AND silent: a plot frame with the categories on it and nothing in it, which is what an author with a genuinely empty object also sees. Neither guard this renderer carries declines to fire by accident — hasNoCategoryKey is satisfied because the rows DO carry status, and hasNoPlottableSeries keys on series: [], which this is not.

One correction to the card

The card says "No error, no empty state". That is true only when options.xField is also authored. Measured: with no xField the relays floor xAxisKey to the literal 'name', the rows carry status, and the chart renders the loud missing-category-key refusal — naming name, a key the author never wrote. That is the CATEGORY half of the same relay gap. This PR does not address it; it is filed separately, with its own measurement, as objectui#8269.

The loose end at DashboardGridLayout.tsx:261 — settled

It is a legitimately different branch, not a third spelling.

:261 sits after the isObjectProvider(widgetData) early return, in the branch whose rows are an authored literal array (DashboardRenderer labels its twin // Static inline data array.). There is no aggregate there to consult, and the author's yField names a column in their own rows, so yField is the correct binding and must stay. Pinned in both directions by the second describe block of the new seam test — including that the branch composes type: 'chart' with aggregate: undefined — so a later "unify the two" refactor cannot quietly break it.

Corollary: the answer is one resolver for the question the aggregate path asks, and :261 legitimately does not ask it.

The fix — one authority, and it is the contract's

chartAggregateValueKey in @objectstack/spec/ui already IS the single answer to "which column does an object-bound aggregate project its measure under" — its own doc calls it "what a chart's series / y-axis binding must name". The repo had three independent opinions of that question and two of them disagreed for a fieldless count.

  • New @object-ui/core export chartMeasureKey(aggregate, fallback) — a seam over chartAggregateValueKey, not a restatement of the rule. core is the home for exactly this (the humanizeLabel move in objectui#5444, buildChartSeries under ADR-0021) and already imports @objectstack/spec/ui at runtime, so no package gains a new dependency edge.
  • Both dashboard relays now read it for the series binding.
  • aggregateValueKey in plugin-charts — the ROW projection — is routed through the same function, so the two halves cannot drift again. Behaviour-preserving: the delegation's tail (?? aggregate.function ?? 'count') reproduces the old three-rung read for the only shapes the contract answers undefined for, which are shapes ChartAggregateSchema rejects outright. Both are now pinned in the existing ObjectChart.aggregateResultColumns.test.ts.

The inverse fix was not available and was not attempted: making the row projection key a fieldless count 'value' contradicts a pin already on main and renames the column every other consumer reads, including the engine's COUNT(*) alias.

What moves on screen — stated, not assumed

A chart that was blank now draws. No chart that already drew changes: a field-bearing aggregate resolves to its raw field under both the old and the new reading, and a chart with no aggregate keeps the author's yField (pinned).

One authored key changes meaning: a yField written on an object-bound chart that ALSO declares an aggregate no longer wins over the aggregate's own column. It named a column of the RECORDS, and a grouped aggregate does not return records — so it plotted nothing before, for exactly the reason 'value' did.

Ablation — the pins can fail

Committed first, then two disjoint mutations in one run, then restored by state (git hash-object equal to git rev-parse HEAD:PATH for both files, and git diff HEAD empty), never by an exit code. Each mutation was proved on disk by a grep -cF count of the removed text going to 0 and the injected text to 1 before anything ran.

Mutation A — chartMeasureKey reverted to the pre-fix reading aggregate?.field ?? fallback:

× is the literal "count" for a fieldless count — NOT the caller floor
× ignores a caller floor the author chose, when the aggregate answers
× binds a FIELDLESS count to the column the rows carry, not to the yField floor   [grid relay]
× ignores an authored yField that an object-bound aggregate cannot project        [grid relay]
× binds a FIELDLESS count to the column the rows carry, not to the yField floor   [renderer relay]
× ignores an authored yField that an object-bound aggregate cannot project        [renderer relay]

Both relays go red, which is what proves both of them route through the one authority rather than one of them happening to agree.

Mutation B — aggregateRecords' value key reverted to the raw field, the state before framework#3701:

× is the shape the row builder emits — the premise the rest of the file rests on
× draws its marks when the series names that column
× keys a fieldless count under "count", not undefined

Totals: 9 failed, 22 passed of 31. The 22 are the arms that must NOT move — the field-bearing aggregate, the no-aggregate floor, the authored-rows branch — so the ablation discriminates rather than reporting one undifferentiated red. draws NOTHING, silently, when the series names "value" stayed green under both, correctly: it is a pin on the renderer, not on either mutated path.

Verification

Run from the repo root (the package-dir invocation is refused here by design, objectui#3378). Exit codes captured before any pipe.

  • pnpm exec vitest run packages/core/ packages/plugin-charts/ packages/plugin-dashboard/267 files, 3902 tests passed.
  • turbo run build for both packages' dependency closures, then turbo run type-check for the three packages — 12 and 16 tasks, all successful.
  • node scripts/check-changeset-presence.mjs — its own verdict line: 5 source files of 3 released packages changed, 1 changeset declared. check-changeset-no-major green (this repo ships breaking as minor; this is a patch).
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:spec-symbols, check:phantom-deps, check:self-import, check:unreferenced-sources, check:side-effects-array, check:element-data-source-declaration, check:entry-guard — all exit 0. After a full turbo run build (44 tasks): check:sdui-registration-pins, check:readme-exports, check:dist-completeness — all exit 0.
  • eslint --no-inline-config over the 9 changed TS/TSX files: 0 errors, 89 warnings, of which 80 are pre-existing in the three large files this PR touches (64 / 8 / 8) and 9 are no-explicit-any in the two new test files, the same shape every schema-driven test in this repo carries. ESLint here is not type-aware (no project / projectService in eslint.config.js), so this diff cannot move the verdict on a file it did not touch. The whole-repo turbo run lint is CI's.
  • Not run locally, declared: the other 35 packages turbo ls --affected names. They are affected only through the dependency graph — this diff adds an export and changes no existing signature — and CI runs the sharded farm.

⚠️ Bundle Analysis will be RED, and the reason is mostly not this PR

Measured in this worktree, same harness, two builds:

console eager closure, gzipped
origin/main sources (0fa7a9c83) 3,267,992
with this PR 3,268,006
MAX_EAGER_CLOSURE_GZIP_BYTES 3,268,000

This PR adds 14 gzipped bytes. main was sitting 8 bytes under the ceiling before it.

That ceiling was set in objectui#6776 over a baseline of 3,222,314 on 3d257c85a, deliberately carrying 45,686 bytes of headroom — 0.50x the regression the gate is calibrated to catch. 45,678 of those bytes have since been consumed by drift, so the aggregate gauge is parked at 1.00x its own sensitivity: it can no longer catch the regression it exists for, and the next packages/** PR from anyone tips it regardless of what that PR does.

I have not raised the constant. The gate's own header calls re-baselining a decision that must be visible and justified by what the bytes buy — and the decision here is about 45.7 KB of accumulated drift that this card did not cause, not about my 14. Squeezing the fix under an exhausted ceiling would be the same evasion in the other direction. Handing it up instead.

Bundle Analysis is not in this repo's merge-queue required set (Lint, Type Check, Test shards 1-4, Build & E2E, Build Docs, Changeset Declaration), so it gates nothing mechanically — but it is a true red and wants an answer.

Overlap note

packages/core/src/index.ts is a shared barrel; this PR adds one export * from line to it and touches nothing else there, so a concurrent barrel edit merges as a union.

Not done here, on purpose

  • objectui#8269, the category half — a groupBy-only object chart is still refused for lacking name. Filed with its measurement; that card remains open and this PR does not address it.
  • The remaining 'name' / 'value' literal floors at the relay faces, which objectui#7547 tracks.

Drafted by the dev seat in session session_01YBWFb5YgMU5dw8p2VKj16S. Draft on purpose — the PM seat flips and lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…e column the aggregate projects (objectui#8266)

A dashboard chart widget with `aggregate: { function: 'count', groupBy: 'status' }`
and no `field` rendered a plot frame with the category ticks drawn and zero marks
in it — no error, no empty state.

Both relays built the series binding as `aggregate?.field || (options.yField ||
'value')`, resolving to `'value'`, while an object-bound fieldless count projects
its value under the literal `'count'` (framework#3701). Neither renderer guard
fires on that: the rows do carry the category key, and the series array is not
empty.

`chartMeasureKey` (new, `@object-ui/core`) delegates to the contract's own
`chartAggregateValueKey` from `@objectstack/spec/ui`. Both relays and the row
projection in `plugin-charts` now read it, so the series binding and the column
it binds to cannot disagree again.

The authored-rows branch in both relays keeps reading the raw `yField`: it is
reached only when the widget's rows are a literal array, where there is no
aggregate to consult and the author's key names a column of their own rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…ey pin

The recorder is a plain function component, so nothing in the file spies or
mocks. `object-ui/no-unused-imports` is an error rule here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3191.4 KB 3191.4 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-Csluxs_L.js
Status FAIL

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.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ❌ over its ceiling
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.00KB 113.91KB
core (index.js) 7.20KB 2.88KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 189.11KB 52.55KB
fields (index.js) 243.04KB 61.36KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 48.03KB 13.29KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.43KB 34.43KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 247.68KB 63.49KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 52.83KB 14.63KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@os-justin
os-justin marked this pull request as ready for review September 7, 2026 07:31
@os-justin
os-justin enabled auto-merge September 7, 2026 07:31

Copy link
Copy Markdown
Collaborator Author

PM ruling — A: land as is. The re-baseline is escalated, ⛔ not ridden in here.

On the Bundle Analysis red

MAX_EAGER_CLOSURE_GZIP_BYTES is 3,268,000 (verified independently at scripts/check-eager-closure-budget.mjs:298). main measures 3,267,992 — 8 bytes of headroom. This PR adds 14.

⇒ the red is real, and it is not this card's. Of the 45,686 bytes of headroom objectui#6776 deliberately sized at 0.50x the regression the gate exists to catch, 45,678 have been consumed by drift nobody attributed. The gauge is parked at 1.00x its own sensitivity: it can no longer catch the 89 KB regression it is calibrated for, and the next packages/** PR from anyone tips it regardless of content. This PR is simply the one holding the parcel.

Option B (raise the ceiling here) is refused on a standing repo norm, not on taste: raising a gate ceiling is a gate weakening, and this repo puts that on the manual floor — the gate's own header calls a re-baseline a visible, justified decision, and PR #7685 needed an explicit maintainer authorisation (「抬上限」) to do exactly this. ⛔ A p2 bug fix does not get to carry that decision as a rider.

Option C (contort the fix to squeeze under) is refused for the reason the dev gives and it is the right one: inlining chartAggregateValueKey at the call sites to buy 8–14 bytes would undo the fix's whole point — three opinions collapsing into one authority — to fit an exhausted ratchet, and would buy exactly one PR of relief. That is the wrong incentive pointed at the wrong number.

A. Bundle Analysis is not in this repo's merge-queue required set (Lint · Type Check · Test shards 1–4 · Build & E2E · Build Docs · Changeset Declaration), so it blocks nothing mechanically — ⚠️ and that is being verified empirically rather than asserted: auto-merge is armed on this PR, and if Bundle Analysis turns out to be required it simply will not merge and I will say so here.

The 45.7 KB of drift and the re-baseline go to the maintainer as their own item, with the second open question (the gate's ~0 headroom) folded into it — that is option B of question 2, as recommended. ⛔ Neither is dropped.

⭐ On the work itself — PASS, and it exceeds the dispatch

It reproduced, and it was measured rather than derived. dataKey: 'value' over [{status:'open',count:2},{status:'paid',count:5}] drew 0 .recharts-bar and 0 .recharts-rectangle, with no refusal and no empty state; the same rows under dataKey: 'count' drew 1 bar and 2 rectangles. Same harness, one key apart — the control is what makes the zero a statement about the binding rather than about the harness. That is the standard.

The fix is better than the one dispatched. The brief asked for "one place in this repo that answers which column carries the measure". chartMeasureKey is instead a seam over chartAggregateValueKey in @objectstack/spec/ui — the contract's own derivation — consulted by both relays and the row projection. The authority ends up upstream of this repo, which is the contract-first answer and strictly better than a local one.

The :261 verdict is correct and properly argued. It is a legitimately different branch, not a third spelling: it sits after the isObjectProvider early return, in the authored-literal-rows branch where no aggregate exists and the author's yField names a column of their own rows. It must keep reading yField — and that is now pinned in both directions rather than left as a comment.

The ablation discriminates. Two disjoint mutations in one run → 9 failed / 22 passed, red by name on each expected row including twice on "binds a FIELDLESS count to the column the rows carry" — once per relay, which is what proves both route through the one authority rather than one merely happening to agree. The 22 that stayed green are the arms that must not move. A leg that reddened everything would have proved nothing.

⚠️ The correction to my card and my brief — accepted, and it matters

Both said "no error, no empty state". That holds only when options.xField is also authored. With no xField the relays floor xAxisKey to the literal 'name' and the chart renders the loud missing-category refusal, naming a key the author never wrote — the category half of the same relay gap. Filed as objectui#8269, deduped with a self-validating search.

⇒ so the defect has two faces, and my card described only one of them. The distinction is not cosmetic: one face is silent, the other is loud and misleading. Good catch.

Landing on convergence of the required set.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants