` with nothing in it. Anything that
+ // paints a plot here would be a sankey drawn from links that do not exist.
+ expect(container.querySelector('svg')).toBeNull();
+ });
+
+ it('leaves the no-rows case alone — that is the empty-result question, answered upstream', () => {
+ const { container } = renderSankey([]);
+
+ expect(refusalOf(container), 'no refusal without rows to refuse over').toBeNull();
+ expect(container.querySelector('svg')).toBeNull();
+ // Byte-for-byte what this arm returned before objectui#7140: an empty div,
+ // carrying only the className it was handed.
+ expect(container.textContent).toBe('');
+ });
+
+ it('still draws when ONE row is positive among zeros', () => {
+ const { container } = renderSankey([
+ { stage: 'Prospecting', amount: 0 },
+ { stage: 'Proposal', amount: 7 },
+ { stage: 'Won', amount: 0 },
+ ]);
+
+ expect(refusalOf(container), 'a drawable sankey is never replaced by prose').toBeNull();
+ expect(container.querySelector('svg')).not.toBeNull();
+ });
+
+ it('CONTROL — an all-positive sankey draws, and carries no refusal', () => {
+ const { container } = renderSankey([
+ { stage: 'Prospecting', amount: 40 },
+ { stage: 'Proposal', amount: 25 },
+ { stage: 'Won', amount: 12 },
+ ]);
+
+ expect(refusalOf(container)).toBeNull();
+ expect(container.querySelector('svg')).not.toBeNull();
+ });
+
+ it('does not fire for other chart families handed the same all-zero rows', () => {
+ // The guard lives inside the sankey arm and reads the sankey filter's own
+ // result, so it cannot reach a family that has no such filter. Pinned
+ // because a hoisted copy of the predicate is the obvious refactor and would
+ // blank four working charts: bar/pie/funnel/treemap all render an all-zero
+ // dataset today (measured in Chromium — axes, labels and legend).
+ for (const chartType of ['bar', 'pie', 'funnel', 'treemap'] as const) {
+ const { container } = render(
+
,
+ );
+ expect(refusalOf(container), `${chartType} must be untouched`).toBeNull();
+ cleanup();
+ }
+ });
+});
diff --git a/packages/plugin-charts/src/AdvancedChartImpl.tsx b/packages/plugin-charts/src/AdvancedChartImpl.tsx
index baf47ce1ee..3105af33af 100644
--- a/packages/plugin-charts/src/AdvancedChartImpl.tsx
+++ b/packages/plugin-charts/src/AdvancedChartImpl.tsx
@@ -1048,7 +1048,45 @@ function AdvancedChartImplInner({
const nodes = [{ name: rootName }, ...rows.map((r) => ({ name: String(r?.[xAxisKey] ?? '') }))];
const links = rows.map((r, i) => ({ source: 0, target: i + 1, value: Number(r?.[dataKey]) || 0 }));
if (links.length === 0) {
- return
;
+ // Rows ARRIVED and the filter above kept none of them, so there is no
+ // flow to draw. This used to return a bare `
` — objectui#7140.
+ //
+ // Measured in Chromium before it was changed, against a populated
+ // control that drew 1 `
` / 7 ``: the all-zero, all-null,
+ // all-negative and unparseable-measure tiles each rendered ONE element
+ // and nothing else (`descendantCount: 1`, `svg: 0`, `textContent: ''`),
+ // and their screenshots were byte-identical to each other. No marks, no
+ // text, no `role` — the one path in this file that put nothing at all on
+ // the page, and pixel-identical to a render that crashed. A reader could
+ // not tell a genuinely all-zero flow from a broken widget, which is the
+ // distinction every other refusal here exists to make.
+ //
+ // Gated on rows being present for the same reason `hasNoCategoryKey` and
+ // `hasNoPlottableSeries` are: handed NO rows the sentence below would be
+ // false — there is no row whose measure could be anything. That is the
+ // empty-RESULT question (objectui#7130), answered upstream in
+ // `ObjectChart` where the query outcome is known, so this arm leaves the
+ // no-rows case byte-for-byte as it was.
+ //
+ // ONE code and ONE sentence, for the reason `hasNoPlottableSeries`'
+ // docstring gives: three causes reach here — a genuinely all-zero flow,
+ // values a flow cannot represent because they are negative, and
+ // unparseable measures that `Number(…) || 0` folds to zero — and naming
+ // any ONE of them is a sentence that is false for the other two. The
+ // predicate the filter actually applies is true for all three, so the
+ // copy names THAT. No console warning either, unlike the two refusals
+ // below: those carry a diagnostic pair that does not fit on screen,
+ // whereas this message already names the key and the exact test it
+ // failed.
+ if (data.length === 0) {
+ return
;
+ }
+ return (
+
+ This chart has no flow to draw: no row's{' '}
+ {dataKey} is above zero.
+
+ );
}
return (