Skip to content
43 changes: 43 additions & 0 deletions .changeset/7195-no-numeric-value-refusal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@object-ui/plugin-charts': patch
---

A cartesian chart whose numeric axis has rows but nothing to build a scale from now says so
instead of drawing an empty frame (objectui#7195). The tile renders the existing
`data-chart-error` placeholder with the new code `no-numeric-value` and a sentence naming the
bound key and the row count: "none of the N rows has a numeric value for KEY".

The answer is keyed on the whole dataset, not on the value's type. A boolean beside one real
number still draws (Recharts builds the scale from the number and places the booleans on it), so
that mixed chart keeps drawing with no note. What now refuses, each of which drew zero marks
before:

- scatter, when every row's x (or y, or both) is a boolean, or a boolean mixed only with values
that cannot be placed. That last shape used to carry a footnote implying one point was drawn;
- bar / column / horizontal-bar / line / area / combo with no stacked bar or area series, when
no row gives any bound series a value the axis can scale: every value boolean, `null` or an
unparseable string. A line's `stack` has no effect in the renderer, so a "stacked" line is
judged the same way.

A chart with ANY stacked bar or stacked area series is never refused, whatever its values: the
stack gives its axis a scale on its own, and every series on that axis is placed on it (an
all-boolean series beside a stacked all-`null` one draws). Some of those charts draw nothing
(a stacked all-`null` bar, a stacked area of unparseable strings); they stay silent, exactly as
before, rather than risk a sentence over marks that are on screen.

What keeps drawing, unchanged: numeric strings, `Number` objects, `Date` values, range values
(an array read by its first two elements, both numbers, so `[1, 3]` and `[1, 2, 3]` alike: range
bars and range areas), every chart with a STACKED bar or area series (the stack paints booleans
as numbers, and unparseable strings, `NaN`, objects and arrays as full-height bars), a series that
is all boolean beside a numeric one, a dual-axis chart with one live axis, and any axis whose
spec declares both a numeric `min` and a numeric `max` (the chart builds that scale from the spec
and places booleans on it), and any axis whose spec declares a `stepSize` (with one declared
bound, the chart's tick builder reads booleans as 0 and 1 and supplies the other end; a `stepSize`
alone is left silent too, erring toward silence). A `min` or `max` alone, `logarithmic` alone
or an annotation does not build a scale, so those tiles are still refused. `''` counts as a value at zero: a line, area or scatter draws it, and a bar,
horizontal-bar or combo paints the same zero-height picture as all-zero data, silently, exactly
as before. A bound series key that is not a plain property of any row (a dotted path such as
`a.b`, which Recharts resolves into nested rows, or a column no row carries) keeps the chart
silent: the refusal fires only when every bound key is read from the rows directly. Rows with no placeable pair still get scatter's
`no-plottable-points`, a multi-series scatter still gets `scatter-multi-series`, and
`missing-category-key` / `no-plottable-series` still take precedence over this one.
Original file line number Diff line number Diff line change
Expand Up @@ -223,32 +223,41 @@ describe('objectui#7171 — a POSITIONAL chart legitimately plots zero and negat
expect(noteOf(container)).toBeNull();
});

it('a BOOLEAN coordinate is measured-and-DECLINED, because whether it places depends on its neighbours', () => {
it('a BOOLEAN coordinate: the all-boolean tile is refused (objectui#7195), the mixed tile still draws', () => {
// The trap this card was fenced against, hit in a new place. An all-boolean
// x drew 0 of 2 marks in the browser sweep and looked exactly like a
// sibling of `null` — and pinning it as unplaceable turned RED here, which
// is how the mixed reading was found: Recharts needs one real number to
// build the scale and then coerces the booleans onto it.
//
// The REVERSE CONTROL, kept green: a boolean beside a number DOES place, so
// `isPlottableCoord` still accepts it and neither this card's note nor
// objectui#7195's refusal may fire — a sentence over three visible points
// saying they are not there is false about the picture.
const { container: mixed } = renderScatter([
{ xm: 10, ym: 40 },
{ xm: true, ym: 25 },
{ xm: false, ym: 60 },
]);
expect(marksOf(mixed), 'a boolean beside a number DOES place').toBe(3);
// So the predicate must accept it: a footnote reading "2 of 3 rows are not
// drawn" over three visible points is a false sentence about the picture,
// which is worse than the silence this card is about.
expect(noteOf(mixed)).toBeNull();
expect(refusalOf(mixed)).toBeNull();
expect(mixed.querySelector('[data-chart-error="no-numeric-value"]')).toBeNull();
cleanup();

// The cost of that choice, stated rather than hidden: with EVERY row
// boolean there is no scale to coerce onto, nothing draws, and this card's
// answer stays silent — exactly as it is today. A narrow hole, not a
// regression, and its real answer belongs upstream.
// With EVERY row boolean there is no scale to coerce onto and nothing
// draws. objectui#7195 (ruled A) answers that on the WHOLE dataset — no
// row carries a numeric value for `xm` — which is true here and cannot be
// true of the mixed tile above. Its own code, not this card's: the rows
// DO pass `isPlottableCoord`, so `no-plottable-points` stays silent.
const { container: allBool } = renderScatter([{ xm: true, ym: 40 }, { xm: false, ym: 25 }]);
expect(marksOf(allBool)).toBe(0);
expect(refusalOf(allBool)).toBeNull();
expect(noteOf(allBool)).toBeNull();
expect(plotOf(allBool)).toBeNull();
const refusal = allBool.querySelector('[data-chart-error="no-numeric-value"]');
expect(refusal).not.toBeNull();
expect(refusal!.textContent).toContain('none of the 2 rows has a numeric value for xm');
});

it('a constant x with a varying y still draws every mark', () => {
Expand Down
Loading
Loading