Repository navigation
fix(plugin-charts): refuse a numeric axis no row can scale (objectui#7195) - #10388
Conversation
…7195) A cartesian tile whose numeric axis has rows but no value Recharts can build a scale from drew an empty frame and said nothing. It now renders the data-chart-error placeholder with code no-numeric-value, naming the bound key and the row count, keyed on the whole dataset so the mixed boolean tile keeps drawing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb
|
changeset-claim-re-read
|
✅ Console Performance Budget
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. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 83/83 Adopted from an isolated at-tier reviewer by ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Required fixes:
Seat decision: FAIL stands. The three required fixes go back to the dev as patch round 1 on this PR; the new head gets a fresh at-tier review before ACCEPT. The absent-key shape (objectui#8266) is cut as its own card, per the dev's option B, and ⛔ does not widen this PR. Generated by Claude Code |
…(objectui#7195) Contract review patch round 1. anchorsNumericAxis now mirrors Recharts' makeDomain as well as makeNumber: a two-element [lo, hi] array whose ends both pass the scalar rule anchors the axis, so range bars and range areas keep drawing. The series-family check now stays silent unless every bound key is a plain own property of some row: Recharts resolves dotted keys through get(), and this predicate does not re-implement that path grammar. Changeset and source prose corrected to match. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb
✅ Console Performance Budget
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. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 81/81 Adopted from an isolated at-tier reviewer by ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Required fixes:
Seat decision: FAIL stands. Required fixes 1 to 3 go back to the dev as patch round 2. The seat rules on the ③ question, the stacked range with a boolean end (a d3-stack Generated by Claude Code |
…s (objectui#7195) Contract review patch round 2. An array anchors by its first two elements with no length check, and a Number object anchors, as Recharts' makeDomain and isNumber read them. Any array in a stacked series anchors, so a stacked range with a boolean end (painted by d3's stack) stays silent. An axis whose spec declares both a numeric min and max has a scale without data, so it is never refused: scatter reads xAxis / yAxes[0], series read the axis they may bind, over-approximated toward silence. Prose corrected to match. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb
✅ Console Performance Budget
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. 📦 Bundle Size Report
Size Limits
|
…i#7195) The no-new-wrappers directive on the Number-object pin suppressed nothing, and the repo's lint reports an unused directive as an error. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb
✅ Console Performance Budget
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. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 107/107 Adopted from an isolated at-tier reviewer by ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Required fixes:
Seat decision: FAIL stands, on one point. The seat's round-2 ruling ( Generated by Claude Code |
…ctui#7195) Contract review patch round 3. A stacked bar (bar / column / horizontal-bar, or a bar inside a combo) paints every value except null and undefined, including unparseable strings, NaN and objects as full-height d3-stack artefacts, so those stay silent. A stacked area draws a path for booleans but an empty one for unparseable values, so only booleans and arrays join the scalar rule there. A line's stack is inert in the renderer, so it keeps the unstacked rule. The family is resolved the way the renderer resolves it. Prose corrected to match. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb
✅ Console Performance Budget
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. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 76/76 Adopted from an isolated at-tier reviewer by ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Required fixes:
Seat decision: FAIL stands. Four rounds have each found a new way the per-series predicate misses how Recharts actually builds an axis scale. So patch round 4 changes posture rather than modelling one more case. The refusal is scoped to what the predicate can prove. An axis is live, and every series bound to it stays silent, whenever ANY series bound to it carries a bar- or area-mode Generated by Claude Code |
…ctui#7195) Contract review patch round 4, seat ruling. A stacked bar or area gives its axis a scale through d3's stack whatever its values, and an unstacked sibling on that axis is coerced onto it and draws. Instead of modelling that per series, any series with a bar- or area-mode stack now makes the tile live, so it is never refused. A line's stack stays inert. Unstacked census cases still refuse. Prose and pins updated to match. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb
✅ Console Performance Budget
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. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 95/95 Adopted from an isolated at-tier reviewer by ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Required fixes:
Seat decision: FAIL stands. The PR stays draft and is ⛔ not enqueued. Five rounds have each found a new renderer path that builds a scale for an all-boolean axis. This time it is Generated by Claude Code |
…195) Contract review patch round 5. With a stepSize and one declared bound, the renderer's own tick builder reads booleans as 0 / 1 and supplies the other end, so the axis has a scale and booleans draw. declaresFullDomain becomes declaresScale: a finite min and max, or any finite stepSize. The broader rule (stepSize alone included) was chosen because it is the simplest to keep true and errs to silence. Prose and pins updated to match. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb
✅ Console Performance Budget
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. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 123/123 Adopted from an isolated at-tier reviewer by ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Seat decision: ACCEPT. On the question the ruling turns on (a refusal over drawn marks), the head gives 0 across 2993 + 4030 + 101 reviewer tiles and the dev's 640. The one boundary flag (a raw-prop Generated by Claude Code |
Fixes #7195
Clause-②: yes
What changed
A cartesian tile whose numeric axis has rows, but not one row carrying a value that axis can build a scale from, drew an axis frame with zero marks and said nothing. It now renders the existing
data-chart-errorplaceholder (the objectui#4683 channel,ChartRefusal,role="status") with a new codeno-numeric-valueand a literal English sentence keyed on the whole dataset:(
its only row has no numeric value for KEYwhen N is 1; several dead keys are joined with "or".) Ruling 5809002651, letter A. No "reject booleans" predicate and no authoring-time type check. No new i18n pack key, sopackages/i18nis untouched.Only
packages/plugin-charts/src/AdvancedChartImpl.tsx, the rewritten boolean pin inAdvancedChartImpl.degeneratePosition.test.tsx, a newAdvancedChartImpl.noNumericValue.test.tsxbeside them, and.changeset/7195-no-numeric-value-refusal.md.Mechanism, measured before it was coded (the PM hypotheses, each checked)
Number.isFiniteon the raw value would refuse charts that draw. Recharts builds a numeric domain throughmakeDomain/makeNumberin its axis selectors: a number (primitive orNumberobject), a string or aDatewhoseNumber()is finite, or an array read by its first two elements when both pass that rule (range bars and areas;[1, 2, 3]draws). Booleans are rejected. Measured on this component under the package's fixed 480x320 harness: numeric strings ('3') draw on every family,''draws at zero on line / area / scatter, andDatevalues draw on scatter. The predicate (anchorsNumericAxis) mirrors those readers and does not useNumber.isFinitealone. An axis whose spec declares both a numericminandmaxalways has a scale, so it is never refused.Number(value)) whatever its values, and any unstacked series on that axis is placed on it (an all-boolean series beside a stacked all-nullone draws). Rather than model that per series, any series with a bar- or area-mode stack keeps the whole tile silent (seat ruling, round 4,5824094683). A line spreads nostackId, so its stack is inert and it follows the unstacked rule.hasNoNumericSeriesValue) sits in the outer wrapper beside its siblings, in the ordermissing-category-key, thenno-plottable-series, thenno-numeric-value. It needs a category axis and a bound series before its sentence can be true, so both siblings are the more basic answer. Scatter's check sits in the scatter arm, afterscatter-multi-series(a binding fault) andno-plottable-points(no row has a pair that can be placed), and before theunplotted-pointsfootnote. Its pins keep their own codes (see the seam tests).no-plottable-points("no row carries a PAIR") and notno-plottable-series("nothing bound"): the binding exists, every row was read, and the axis still has no scale. A code shared with another arm is what the objectui#4695 review flagged as impossible for a machine to tell apart. Scatter and the series families share this ONE code because the diagnosis and the remedy are the same (bind a numeric column), and the family is already on the tile.The narrowed accept-set (for the contract review)
Now refused (each drew 0 marks before, measured on the base commit with the same probe):
[true, null],[true, 'n/a'],[true, NaN]): before this change these drew 0 marks under anunplotted-pointsfootnote reading "1 of 2 rows ... not drawn", which implied one point WAS drawn;nullor an unparseable string); two series that are both all-boolean; a "stacked" line is judged the same way; an unstacked range with a boolean end.minormaxalone,logarithmicalone and annotations do not build a scale, so those all-boolean tiles are still refused (0 marks on base). Any axis that declares astepSizecounts as having a scale: with one declared bound the renderer's tick builder (ticksFor) reads booleans as 0 / 1 and supplies the other end, so booleans draw. AstepSizealone is left silent too, erring toward silence (round 5).Still draws, unchanged, with no refusal and no note:
:226reverse control, kept green;Numberobjects;Datevalues; a boolean beside a numeric string;''(drawn at zero on line / area / scatter; on bar / horizontal-bar / combo it gives the silent all-zero picture it gave before, unchanged);[1, 3],[1, 2, 3]) on bar / area / horizontal-bar / combo, stacked too;nullbar, a stacked area of unparseable strings) also stay silent, as before this PR;minandmax(scatterxAxis/yAxes[0]; the series'yAxesentries): booleans are placed on the declared scale;stepSizetogether with a singleminormax(seriesyAxesentries; scatterxAxis/yAxes[0]): booleans are placed on the tick-built scale;dataKey(for examplea.b, which Recharts resolves throughget) beside an all-boolean series;The
:226pina BOOLEAN coordinate is measured-and-DECLINEDis rewritten, not deleted, asa BOOLEAN coordinate: the all-boolean tile is refused (objectui#7195), the mixed tile still draws. The mixed half keeps its three assertions (3 marks, no note, nono-plottable-points) and adds "nono-numeric-value". The all-boolean half now asserts 0 marks, no plot, nono-plottable-points, and theno-numeric-valuerefusal with the text "none of the 2 rows has a numeric value for xm". The twoAdvancedChartImpl.tsxcomments that described the boolean case as deliberately left silent now point to the new refusal.Evidence
Current head
1e13495e2(patch round 5 after the round-5 at-tier review5824416651FAILED on0e5bb6e8b; full numbers in the round-5 report5824848065):eslint .): exit 0, 0 errors. Consumers: 126 files / 2015 tests. Type-check exit 0.stepSizeclause fromdeclaresScaleturns exactly the 22stepSizepins red and no other. Restore proven.cd7b728b9vs head, 640 tiles (a 255-tile condensed rounds 0–4 population, a 300-tile matrix ofstepSizewith and without a bound, and 85 review-reconstruction tiles): drew-on-base 341, drawn tiles changed 0, new refusals over base marks 0.Patch round 4, at
0e5bb6e8b:eslint .): exit 0, 0 errors. Consumers: 126 files / 2015 tests (the round-4 review re-derived the same figure). Type-check exit 0.cd7b728b9vs head, 782 tiles, including 302 mixed stacked + unstacked tiles on one axis and the 28 round-4 review tiles: drew-on-base 432, drawn tiles changed 0, new refusals over base marks 0.Patch round 3, at
604f051c7:eslint .): exit 0, 0 errors. Consumers: 126 files / 2015 tests (vitest JSON reporter; the per-path breakdown and the exact command are in the report). Type-check exit 0.cd7b728b9vs head, 694 tiles (426 stacked shapes across six families, plus the round-2 population): drew-on-base 322, drawn tiles changed 0, new refusals over base marks 0.Patch round 2, at
89c43b447:eslint .): exit 0, 0 errors. Consumers: 126 files / 2015 tests. Type-check exit 0.Numberobjects 2, stacked arrays 1, declared domain 9). Restore proven.cd7b728b9vs head, 394 tiles (declared domains full / min-only / max-only / logarithmic / stepSize, annotations, dual-axis binding, length-1 and length-3 arrays, stacked ranges,Numberobjects): drew-on-base 134, drawn tiles changed 0, newno-numeric-value196, new refusals over base marks 0.Patch round 1, at
18f03f4d6:pnpm exec vitest run packages/plugin-charts/: Test Files 68 passed (68), Tests 704 passed (704). Consumers: 126 files / 2015 tests passed. Type-check exit 0. The same gate set exits 0.cd7b728b9vs head, 161 tiles: drew-on-base 83, drawn tiles changed 0, newno-numeric-value49, new refusals over base marks 0.History, at the first head
774582568:pnpm exec vitest run packages/plugin-charts/from the repo root: Test Files 68 passed (68), Tests 698 passed (698).plugin-dashboard/src/__tests__/, the app-shellDatasetPreviewandwidget-dom-leak-sweepfiles, theplugin-reportchart-locale file,examples/schema-cataloggallery render): Test Files 126 passed (126), Tests 2015 passed (2015).pnpm --filter @object-ui/plugin-charts type-checkafter building the@object-ui/plugin-charts^...closure: exit 0. Thetsconfig.test.json--listFilesincludes both touched test files (2 hits).git checkout HEAD --: an earlyreturn trueinjected intoanchorsNumericAxis(markergrep -c1 after the mutation, 0 after the restore). The two files went 43 failed / 93 passed of 136. That is exactly the 43 refusal assertions (scatter 6, series families 36, the rewritten:226pin 1), and every drawing and seam test stayed green. Restore proven bygit hash-objectequal to the HEAD blob02440cbc57dcand an emptygit diff HEAD. No build step was involved: the tests import./AdvancedChartImplfrom source.package.jsonand.github/workflows/for the touched paths, each with exit 0:check-control-bytes,check-new-cross-file-line-citations(0 new),check-changeset-presence,check-changeset-no-major,check-changeset-overwrite,check-changeset-claims,check-vi-mock-specifiers,check-vi-mock-inherit,check-vi-mock-override-shape,check-test-path-roots.eslint --format jsonover the three touched files from the package directory (the sameeslint .config the package'slintscript uses): 3 files, 0 errors, 64 warnings (no-explicit-anyin the test harness style, plus existing ones in the source file). The config has no type-aware parser options, so the diff cannot change the verdict for any file it did not touch. The fullpnpm lintrun belongs to CI.check-changeset-claimspointed at.changeset/7546-chart-series-keys-declared.mdbecause it namesAdvancedChartImpl.tsx. I read that paragraph:stackstill becomesstackIdandyAxisstill binds the secondary axis, so it is still true.Acceptance notes
valueover rows that carrycountdraws 0 marks, and the ruling's predicate literally holds for it. But the render pins inObjectChart.countSeriesKeyRender-8266.test.tsxandObjectChart.categoryAxisKeyRender-8269.test.tsxkeep that shape silent. Their header asks for that to be "re-decided rather than a silent behaviour change nobody notices", and both files are outside this card's file surface. A first draft that covered the shape turned exactly those two pins red, so the refusal fires only when every bound series key is a plain own property of some row. A dotted path (Recharts resolves it throughget) or a column no row carries keeps the tile silent: an unresolved key cannot be known not to draw. The absent-key shape is filed as its own card, objectui#10396.stackon a line series has no effect in this renderer (the line arms spread nostackId), and the predicate reads the line family the same way, so a "stacked" all-boolean or all-unparseable line is refused (0 marks on base).barwith every value''draws 0 rectangles, because recharts reads''as zero and the domain is[0, 0]. That is the same picture as an all-zero bar, which is finite data and not this defect. It is not refused.Card relations are declared here only; the commit carries no card trailer.
Implemented by
session_01D486C1axmXnrkJMNUfz2eb(https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb), seatdomain:ui#3.Generated by Claude Code