Skip to content

The chart null-category bucket has two identity collisions: it merges a null group with an empty-string group, and collides with a stored value spelling the label #4508

Description

@yinlianghui

Measured while implementing #4497 (PR #4506) and pinned there as limits; filed unassigned rather than widened into that card. Both are properties of the #4466 doctrine itself, not of the pivot extension — the pivot inherits them.

1. A null group and an empty-string group merge into one bar (pivot branch)

buildChartSeries' pivot branch keys buckets by String(xRaw ?? ''), so a stored null and a stored '' land in the same bucket and draw one bar. The bar takes its label from whichever row created the bucket, so with a null row first it renders (None) — and the drill for the segment sourced from the '' row then finds nothing:

buildChartSeries(
  [{ status: null, priority: 'Low', n: 3 },
   { status: '',   priority: 'High', n: 5 }],
  ['status', 'priority'], ['n'],
).data
// [{ status: '(None)', Low: 3, High: 5 }]   ← one bar, two sources

findChartSeriesRow(rows, ['status','priority'], ['n'], '(None)', 'Low')   // 0
findChartSeriesRow(rows, ['status','priority'], ['n'], '(None)', 'High')  // -1  ← dead

handleChartDrill returns on -1, so this is a no-op click, never a drill into the wrong records — and pre-#4497 the whole bar was invisible, so it is strictly more affordance than before, not a regression. It is still an inconsistency: a bar labelled "no value" carries a segment belonging to a group whose value is the empty string.

Widening findChartSeriesRow to match '' rows against the null label was considered and rejected in #4497: it would make a bar labelled (None) drill to a raw row whose stored value is '', i.e. filter records by a value the label does not name — trading a dead click for a wrong one.

The pivot table answers the same question differently, which is worth deciding together: buildPivot (plugin-dashboard) gives a null dimension value its own bucket id, distinct from the literal placeholder character, and labels it with an em-dash. See DatasetWidget.test.tsx, "buildPivot keeps a null COLUMN-dimension value apart from the literal placeholder".

2. A stored value spelling the label collides with the bucket (both branches)

A row whose stored category is the label string keeps its own bucket (its key is '(None)', not ''), so two bars carry the same axis text and the click resolves to the first:

buildChartSeries(
  [{ status: '(None)', priority: 'High', n: 1 },
   { status: null,     priority: 'High', n: 2 }],
  ['status', 'priority'], ['n'],
).data
// [{ status: '(None)', High: 1 }, { status: '(None)', High: 2 }]

findChartSeriesRow(rows, ['status','priority'], ['n'], '(None)', 'High')  // 0 — the literal row

This one can produce a wrong drill: clicking the null bucket's bar resolves to the literal-valued row and filters on it. It is inherited unchanged from the single-dimension branch, where #4466 (PR #4498) shipped exactly this trade, and it applies to every localized label too ((未指定) and the other nine packs).

Dormant in practice: it needs a stored first-dimension value byte-equal to the active locale's bucket label.

Why they are one issue

Both are the same missing property — the bucket has no identity distinct from the display string — and any fix touches the same two functions as a pair (buildChartSeries writes the identity, findChartSeriesRow reads it back). A sentinel bucket identity carried alongside the label, rather than the label serving as its own key, would answer both; that is a doctrine-level change to #4466's shape and wants its own ruling.

Both are pinned as measured limits (not as correct) in packages/core/src/utils/chart-series.nullCategory.test.ts, describe "findChartSeriesRow — the measured limits of the bucket label (objectui#4497)", so a future change to either has to face them.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions