Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

A NULL-keyed group is dropped from a bar chart, leaving an axis with no marks and no empty state — the default first-boot state of System Overview's "Events by User" #4466

Closed

Description

@baozhoutao

Symptom

When a chart's group-by returns a row whose category key is null, the renderer draws the axes, gridlines and axis label but no marks, and shows no empty state — the widget looks like a rendering failure with nothing to explain it.

This is not an edge case on this platform: it is the shipped default state of a freshly provisioned instance. Every seeded sys_audit_log row is written by the system with user_id = NULL, so the built-in System Overview board's "Events by User" widget groups to exactly one row — {user_id: null, event_count: 50} — and draws nothing.

Reproduction (3×)

  1. Boot the showcase fresh: os dev -a dist/objectstack.json --seed-admin -d file:/tmp/x/data.db -p <port>
  2. Sign in as admin at http://localhost:<port>/_console/
  3. Open /_console/apps/setup/dashboard/system_overview, let the board settle, screenshot.

Expected bars, or a designed empty state.
Actual y-axis (0/15/30/45/60), gridlines and the "Events" x-label with zero bars, no empty state, no error. DOM: .recharts-bar = 1, .recharts-bar-rectangle = 0. The server answered 200 with a real row: {"rows":[{"user_id":null,"event_count":50}]}.

Three independent confirmations:

  1. Organic first-boot seed state (50 events, all null) → 0 bars, screenshot-confirmed.
  2. After UPDATE sys_audit_log SET user_id = NULL, a fresh browser launch → 0 bars, server rows [{user_id:null,event_count:50}].
  3. The partial case, which is the sharpest — rows [{user_id:null,event_count:51},{user_id:'Dev Admin',event_count:2}] renders exactly one bar: the null-keyed group (the dominant one, 51 of 53 events) is silently dropped while the y-axis scale still accommodates it. So the chart is not merely empty — it is quietly wrong, and the axis proves the data was there.

Root cause (located, not line-pinned)

packages/core/src/utils/chart-series.ts (single-dimension branch) passes rows through verbatim with xAxisKey = 'user_id'. hasNoCategoryKey in AdvancedChartImpl does not fire, because 'user_id' in row is true — the key is present, its value is null. The row therefore reaches recharts with a null category and no mark is drawn.

Not a stale-bundle artifact

chart-series.ts is unchanged between the vendored console pin (6314e87f2d49) and current origin/main (b4d3c2204).

The renderer's two "nothing to draw" paths disagree

Worth fixing together: an empty result set already gets a designed state — the same board's widgets repaint to a bordered "No rows" panel with a chart glyph when a filter matches nothing. Only a non-empty result whose group key is null falls through to the silent blank. One renderer, two answers for "nothing to draw".

Suggested shape (not prescriptive)

Either render null-keyed groups under an explicit bucket label (the platform already has this concept — the analytics layer returns JSON null rather than a '(null)' sentinel, so the label belongs in the renderer), or treat "every category key is null" as the empty state that already exists. Silently dropping the row is the one option that should be off the table, because case 3 above makes the chart understate its own data without saying so.

Source

Found by the platform checklist retest of dashboards.system-overview-live-counts (framework 279ee48a, console pin 6314e87f2d49). Previously noted once in passing on objectstack-ai/objectstack#7515 and carried in #7531's body without its own card; it now reproduces deterministically.

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    Contributor

    CLAIM — session session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4466-null-group-bucket.

    Ruling (delegated authority; open veto window): render null-keyed groups under an explicit bucket label — the card's own case 3 rules out silent dropping (the chart understates its own data while the axis proves it was there), and the bucket answer SUBSUMES the all-null case: one (None)-style bar is more honest than an empty state when the server returned 50 real events. This also resolves the two-answers disagreement the card names: empty result set → the existing designed empty state; non-empty result → bars, always, null bucket included. One renderer, one answer for each question.

    1. Home by measurement: the mapping belongs in the shared series layer — packages/core/src/utils/chart-series.ts's single-dimension branch (the located cause), where the refactor(core,react,dashboard,report): the analytics label net's fetch-and-memo glue is written once (#4389) #4404 label-map family (deriveDimensionLabelMaps / dimensionOptionTranslator) already owns category-label resolution. Map the null/undefined category to the bucket label THERE so every consumer (dashboard widgets, standalone ObjectChart) gets the same answer — do not patch it in the recharts wrapper per-chart.
    2. The label goes through the i18n channel per convention (defaults-map rows, the live fix(i18n): the drifted detail row mirrors the en pack, and the three defaults maps get a gate (#4401) #4409 gate) — an en (None) / zh (未指定)-class label; measure what sibling "unlabeled bucket" conventions exist before inventing wording.
    3. Check hasNoCategoryKey in AdvancedChartImpl still means what it says post-fix (key absent vs value null are now both handled — the former by it, the latter by the bucket).

    Red-first (the card's own three cases): partial case is the sharpest — rows [{user_id:null,count:51},{user_id:'Dev Admin',count:2}] pre-fix renders ONE bar (dominant group silently dropped), post-fix TWO bars with the null bucket labeled and counted; all-null case pre-fix axes-without-marks, post-fix one labeled bar. Must-not-change: non-null groups render identically; the genuine empty-result state stays the designed empty state (green both sides).

    Grading: patch @object-ui/core + patch @object-ui/plugin-charts (renders strictly more of the data already fetched); .d.ts-checked both ways, minor only if a public surface grows. Never major.

    Surface (mutual exclusion): packages/core/src/utils/chart-series.ts + packages/plugin-charts/** (renderer semantics + tests) + i18n defaults-map rows + changesets. ⛔ NOT: plugin-dashboard (in-flight #4432 — your fix reaches its widgets through core, no dashboard file edits), plugin-calendar (#4453), console/** (#4474), the DOM-leak ledger. Disjoint from in-flight #4408 (list error state) — if its measured surface unexpectedly names chart files, the LATER dispatch yields; check its claim comment before editing.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions