Skip to content

Commit b3ac2d4

Browse files
os-teslaclaude
andauthored
refactor(charts): delete the scatter's inert ZAxis declaration (#9815)
Fixes #9681 Clause-②: no The scatter branch of `packages/plugin-charts/src/AdvancedChartImpl.tsx` declared a symbol-area envelope — a `ZAxis` element carrying `type="number" range={[60, 400]}` and **no `dataKey`**. It was inert. This deletes it, repairs the prose that derived a number from it, and leaves behind a test that states the painted mark size as a **reading** rather than as a declaration. ## The fork on the card is not reopened here The card left one open — delete the declaration, or give it a `dataKey`. The dispatching seat ruled **DELETE**, by the floor and not by preference: variable-area marks do not exist in this product today, so wiring a `dataKey` **adds a capability** and is the maintainer's to take. ⛔ Nothing here prepares for that arm, and nothing here forecloses it either — a promise the tree does not keep is removed, that is all. ## Re-derived, not inherited — the declaration really was inert ⭐ This is the entire basis for calling the delete behaviour-neutral, so it was measured at this head rather than carried over from the card. **Mechanism** (recharts 3.10.1, `es6/state/selectors/axisSelectors.js` + `es6/cartesian/Scatter.js`). The declared `range` is bypassed **twice over**: 1. `selectZAxisWithScale` returns `undefined` unless both the axis settings **and** a configured scale resolve. With no `dataKey` there is no domain, so no scale, so the registered axis never leaves the selector. 2. `computeScatterPoints` then takes `defaultRangeZ = zAxis ? zAxis.range : implicitZAxis.range`. With `zAxis` undefined it reads recharts' own `implicitZAxis.range`, which is `[64, 64]` — ⛔ **not** the declared `[60, 400]`. A point with no z value is painted at that range's lower bound, as a circle of `sqrt(area / PI)`. **The discriminating number.** If any part of the declaration had reached a mark — even only as `defaultRangeZ[0]` — the radius would be `sqrt(60 / PI)` = **4.370193722368317**. Measured with the declaration still present, every mark's radius is **4.51351666838205** = `sqrt(64 / PI)`. The declared envelope contributes nothing. **Where that measurement comes from.** Rendered through the same mechanism objectui#7396's edge-clipping suite documents: `ResponsiveContainer` replaced by an explicit 510x350 box, after which recharts computes the plot rect and every symbol's geometry arithmetically, with no DOM measurement in the path. ⚠️ I did **not** drive a real browser this round — the card's own Chromium reading stands on its own, and this run corroborates it: the six `cx` values measured here (65, 257.6, 300.4, 407.4, 450.2, 493) and the y-max row at `cy` 17 agree digit-for-digit with the post-fix real-Chromium row already recorded in that suite's docblock. ## Behaviour-neutral, proven by ablation One ablation, both legs run from the **committed** implementation, restore pinned to `HEAD` and verified by hash rather than by an exit code: | leg | file on disk | `ZAxis` element | result | |:--|:--|:--|:--| | A — after | `1787741c` (this commit) | 0 | 6 marks, all r = 4.51351666838205 | | mutate | `cf72af1b` (the base blob, byte-identical to `origin/main`) | 1 | hash checked on disk before the run | | B — before | `cf72af1b` | 1 | 6 marks, all r = 4.51351666838205 | | restore | back to `1787741c` | 0 | `git diff HEAD` empty for the file | ⭐ The result is stronger than equal radii: **the whole rendered SVG is byte-identical between the two legs** — same length, same string, every `cx`/`cy`/`r` equal. Removing the declaration moves no pixel of this chart. ⚠️ Worth stating plainly, because the usual ablation expectation is inverted here: the new pin stays **green** under the mutation. That is not a pin that fails to measure — the claim being proved is *behaviour-neutrality*, so "the mutation changes nothing" **is** the result. The pin is on the painted size, ⛔ not on the declaration's absence. ## What was repaired alongside the delete The one thing this delete could break was prose, and it was checked before the delete rather than after. objectui#7396's edge margin is sized to `sqrt(400 / PI)` — the declared envelope's **upper** bound, deliberately — so the margin already survives the day mark area becomes live. Deleting the declaration orphaned the justification, not the number: - `SCATTER_SYMBOL_MAX_AREA`'s docblock said the scatter branch *declares* that envelope. It now names it as a **headroom budget** — the envelope a variable-area scatter would have to stay inside — and points at the instrument that reads what is actually painted. - The `SCATTER_AXIS_EDGE_PADDING` "Why this size" section no longer derives its reasoning from a `dataKey`-less `ZAxis`. - Two comments in `AdvancedChartImpl.scatterEdgeClipping-7396.test.tsx` that leaned on "the declared symbol envelope" / "the ZAxis range". ⛔ The margin **value** is unchanged, on purpose. Lowering it to what is painted today would tie the fix to a third-party default and re-open objectui#7396 the day mark area becomes variable. A repo-wide sweep found no other carrier deriving anything from this declaration: outside the four sites above, no file in the tree mentions `ZAxis`, `60, 400`, `sqrt(400`, or a variable/bubble scatter size. No published `CHANGELOG` paragraph is touched. ## What is left behind, and why a pin rather than only a comment The card's value is the class, not the two-character delete. Triage put the lesson on it because **the triage seat was itself the victim** — objectui#7396's triage comment read this live-looking declaration, reasoned correctly from it, and produced a bubble-size prediction every clause of which was false: > 一条声明不是一条读数 … 惰性的声明和生效的声明,在源码里长得一模一样。 A comment alone would have been ceremony: it is the same kind of artifact that misled the last reader — prose asserting a fact, re-derived by nothing. So the delete ships with **both**, and the comment's job is only to point at the instrument: - `AdvancedChartImpl.scatterSymbolSize-9681.test.tsx` — three assertions, each chosen for what its red would mean. Marks are **one size** across a 5x value spread (the clause the triage comment got wrong, stated as an assertion); the size is **identical across two fixtures four orders of magnitude apart** (so it is a property of the renderer, not of the data); and it is recharts' implicit default area (so a version bump that moves that default says so, instead of the margin's justification going quietly wrong). - A comment at the scatter branch recording that mark area is recharts' own default and not configurable here — naming the test, ⛔ not restating its answer. ⚠️ The honest limit: this pin would **not** catch someone re-adding an inert declaration, because an inert declaration by definition changes nothing it can see. It catches the opposite and more useful case — the day the size stops being a constant, whoever did it must come to this file, restate the size, and re-check `SCATTER_SYMBOL_MAX_AREA`. ## Gates Exit codes captured to a file before being read. Run at `7e82932`. | gate | verdict | |:--|:--| | `vitest run packages/plugin-charts/` (whole package) | ✅ exit 0 — 64 files, 564 tests passed | | ↳ objectui#7396 edge-clipping + default-tick-array pins | ✅ green, inside that run | | ↳ objectui#9675 spec-axes pin | ✅ green, inside that run | | `pnpm --filter @object-ui/plugin-charts type-check` | ✅ exit 0 (`tsc --noEmit` **and** `tsc -p tsconfig.test.json`, which is what covers the new test) | | `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-charts^...' build` | ✅ exit 0 (dependency closure, needed before type-check resolves `@object-ui/*` through built `.d.ts`) | | `check-changeset-presence` | ✅ exit 0 — empty frontmatter accepted as the explicit "releases nothing" declaration | | `check-changeset-fixed` · `check-changeset-no-major` | ✅ exit 0 | | `check-control-bytes` | ✅ exit 0 (plus a direct control-byte scan of the three touched files) | | `check-test-path-roots` | ✅ exit 0 | | `check-vi-mock-override-shape` · `-specifiers` · `-inherit` | ✅ exit 0 — the new test declares a `vi.mock('recharts', ...)` | | `check-unreferenced-sources` | ✅ exit 0 | | `check-new-cross-file-line-citations` | ✅ exit 0 — 0 new citations added by this branch | | `check-comment-mask-corpus` | ✅ exit 0 — within the residue objectui#7882 holds open; this branch adds none | | `check-pending-changeset-literals` | ✅ exit 0 | | `check-changeset-claims` (report-only) | ✅ exit 0, with one body to re-read — answered in Acceptance notes | | `check-governed-queue-guard --test` on this diff's paths | ✅ NOT GOVERNED — ordinary review and merge-queue route | | `eslint` over the three touched files | ✅ exit 0 — 0 errors (warnings are the file family's pre-existing `no-explicit-any`) | **The eslint run is a declared narrowing, and here is what makes it a measurement rather than a skip.** ① The population is CI's `turbo run lint`, i.e. every package's own `eslint .` — read from the root and package `lint` scripts, not guessed. ② Three files linted, counted from `--format json` output, not from my list. ③ Invariance for everything untouched: `eslint.config.js` sets no `parserOptions.project`, no `projectService` and no `tsconfigRootDir`, so no rule in this tree is type-aware; and no rule in `eslint-rules/` reads another file from disk (no `readFileSync` / `readdirSync` / `globSync` in any of them). ⇒ this diff cannot move the verdict on a file it does not contain. ⚠️ `check-lint-coverage` is **not** a second opinion on that and is not read as one — it validates that every package *has* a lint script; it runs no linter. ⚠️ `NOT MEASURED` locally, left to CI: the repo-wide `pnpm test` shards, `pnpm test:dist`, `test:e2e`, `pnpm check` (the CLI self-check), and the remainder of the `check:*` farm whose inputs this diff does not touch. ## Acceptance notes ⛔ Nothing below is filed, and nothing below is repaired here. - **The bounded census the card asked for (is anything else inert for the same reason?) came back empty, within a stated bound.** `packages/plugin-charts` contains exactly one `ZAxis` — the one deleted — and every recharts symbol imported by `AdvancedChartImpl.tsx` has a real JSX use site, so there is no dead import of the same family. The one other "declared but conditionally ignored" shape I went looking for, `minTickGap` beside an `interval={0}`, does not occur on the same element. ⚠️ The bound, stated so it is not read as more than it is: this enumerated declarations at their use sites: it did **not** prove every recharts prop in the file live, and no instrument in this repo does. - **`check-changeset-claims` asked for one paragraph to be re-read: `.changeset/7546-chart-series-keys-declared.md`, which names `AdvancedChartImpl.tsx`.** Read. It claims six series keys (`label`, `variant`, `opacity`, `dashArray`, `stack`, `yAxis`) do real work in this file. This diff moves none of them — it deletes a z-axis prop and edits comments — and each of the six still has live references in both `AdvancedChartImpl.tsx` and `normalizeChartSchema.ts`. Nothing in that body goes false because of this change. **Carrier for anything further: none — this change does not touch it.** - **That same pending changeset body carries a cross-file line address (`normalizeChartSchema.ts:242-255`), the form AGENTS.md #11 bans outright.** ⛔ Noted, **not filed**, and deliberately not repaired: the repo has already ruled this population — existing citations are repaired *opportunistically when you are in the file anyway* and ⛔ never swept, with `census:cross-file-line-citations` reporting the tree-wide picture by design. Filing a card for one instance would act against that standing ruling. ⚠️ The one thing worth recording, since nothing else does: a *pending changeset* is the carrier where "opportunistically" has a deadline, because the body publishes verbatim into a CHANGELOG and AGENTS.md then forbids re-addressing it. **Carrier: the next seat that edits that changeset, or the release that publishes it.** - **If variable-area scatter marks are wanted, that is a feature card with its own appetite** — it needs a `dataKey` and the schema key to feed it. ⛔ Not this repair, and nothing here blocks it: the edge margin is already sized to the envelope such a feature would live inside. --- _Generated by [Claude Code](https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 78a0582 commit b3ac2d4

4 files changed

Lines changed: 222 additions & 15 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
---
3+
4+
Delete the scatter branch's inert `<ZAxis type="number" range={[60, 400]} />`
5+
declaration (objectui#9681). Recharts' `selectZAxisWithScale` drops a z axis
6+
carrying no `dataKey` before it reaches a mark, so the declared area envelope
7+
never applied and every mark was painted at recharts' own implicit default
8+
size. ⛔ No package is released by this change: nothing rendered moves, and no
9+
authored value was ever read through the deleted prop.
10+
11+
The declaration's only effect was on readers, and it had already cost one — the
12+
triage comment on objectui#7396 derived a variable-bubble-size prediction from
13+
it whose every clause was false. So the delete is paired with a test that states
14+
the painted mark size as a reading taken off a render
15+
(`AdvancedChartImpl.scatterSymbolSize-9681.test.tsx`), and the prose that sized
16+
objectui#7396's edge margin to the envelope's upper bound is repaired to name
17+
that number as a headroom budget rather than as something the tree declares.
18+
The margin itself is unchanged, deliberately: it is the envelope a variable-area
19+
scatter would have to stay inside, so it already survives the day mark area
20+
becomes configurable.

‎packages/plugin-charts/src/AdvancedChartImpl.scatterEdgeClipping-7396.test.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ const plotAreaOf = (c: HTMLElement) => {
106106

107107
/** Each mark's painted centre and radius, straight off the symbol path: recharts
108108
* writes the symbol's bounding box onto it, so `width / 2` IS the drawn radius
109-
* rather than a value assumed from the ZAxis range. */
109+
* rather than a value assumed from a declaration (objectui#9681 — the scatter
110+
* branch's `ZAxis range` was such a declaration, and it was inert). */
110111
const marksOf = (c: HTMLElement) =>
111112
[...c.querySelectorAll('path.recharts-symbols')].map((p) => {
112113
const cx = Number(p.getAttribute('cx'));
@@ -166,7 +167,8 @@ describe('objectui#7396 — every scatter mark is drawn wholly inside the plot a
166167
it('reserves at least a full mark radius, so the WHOLE symbol clears the edge', () => {
167168
// Stated as the reader sees it — "half a dot is showing" is the complaint,
168169
// and clearing the edge by a hair would still leave it half-clipped once a
169-
// symbol grows. The margin is sized to the declared symbol envelope, so
170+
// symbol grows. The margin is sized to `SCATTER_SYMBOL_MAX_AREA`, the
171+
// headroom budget rather than the size painted today (objectui#9681), so
170172
// every clearance is a full radius or better.
171173
const { container } = renderScatter(FOUR_EDGES);
172174
for (const o of overhangs(container)) {
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* objectui#9681 — what size the scatter paints its marks, stated as a MEASURED
3+
* fact rather than as a declaration.
4+
*
5+
* ## Why this file exists at all
6+
*
7+
* The scatter branch used to carry `<ZAxis type="number" range={[60, 400]} />`.
8+
* It was INERT: recharts' `selectZAxisWithScale` drops a z axis carrying no
9+
* `dataKey` before it reaches a mark, so the declared envelope never applied
10+
* and every mark was painted at recharts' own implicit default area. The
11+
* declaration's only effect was on readers — and it did mislead one, in
12+
* writing, on this repository: the triage comment on objectui#7396 read it and
13+
* derived "the symbol radius is not constant … bubble size is variable (area
14+
* 60..400 ⇒ radius about 4.4..11.3px)", every clause of which was false at that
15+
* head.
16+
*
17+
* ⭐ The lesson that card left behind: **a declaration is not a reading.** An
18+
* inert declaration and a live one look identical in the source, so the only
19+
* defensible answer to "how big are the marks?" is one taken off a render. That
20+
* is what this file is. Deleting the dead prop removed the wrong answer; this
21+
* file supplies a right one that cannot go stale unnoticed.
22+
*
23+
* ## What would turn these red, and why each red is worth having
24+
*
25+
* - Mark area becomes variable (a `ZAxis` gains a `dataKey`, or a symbol size
26+
* is bound to data another way). That is a NEW capability, not a repair, and
27+
* whoever adds it must come here, restate the size, and re-check
28+
* `SCATTER_SYMBOL_MAX_AREA` — the edge margin objectui#7396 reserved is sized
29+
* to that budget precisely so it survives that day.
30+
* - Recharts changes the implicit default area under a version bump. The margin
31+
* prose in `AdvancedChartImpl.tsx` says the painted size is a third-party
32+
* default; if that default moves, a reader should be told rather than left
33+
* with prose that quietly stopped being true.
34+
*
35+
* ## Why this is measurable in this environment
36+
*
37+
* Same mechanism the objectui#7396 edge-clipping suite documents: given an
38+
* explicit width/height in place of `ResponsiveContainer`, recharts computes
39+
* the plot rect and every symbol's geometry arithmetically, with no DOM
40+
* measurement in the path. Nothing here pins an absolute coordinate — only the
41+
* symbol's own width, which is not a function of text metrics.
42+
*/
43+
import { describe, it, expect, vi, afterEach } from 'vitest';
44+
import React from 'react';
45+
import { render, cleanup } from '@testing-library/react';
46+
47+
vi.mock('recharts', async () => {
48+
const actual = await vi.importActual<any>('recharts');
49+
return {
50+
...actual,
51+
// The widget's real box on the showcase Chart Gallery.
52+
ResponsiveContainer: ({ children }: any) =>
53+
React.cloneElement(children, { width: 510, height: 350 }),
54+
};
55+
});
56+
57+
import AdvancedChartImpl from './AdvancedChartImpl';
58+
59+
afterEach(cleanup);
60+
61+
type Row = Record<string, unknown>;
62+
63+
/**
64+
* Recharts' OWN implicit symbol area, in px² — its `implicitZAxis.range` is
65+
* `[64, 64]` and a scatter point with no z value is painted at that range's
66+
* lower bound, as a circle of `sqrt(area / PI)`.
67+
*
68+
* ⚠️ A THIRD-PARTY figure this repo does not choose and cannot set from the
69+
* outside today. It is spelled out here, rather than left implicit, so that the
70+
* day it moves this file says so instead of the margin prose in
71+
* `AdvancedChartImpl.tsx` going quietly wrong.
72+
*/
73+
const RECHARTS_IMPLICIT_SYMBOL_AREA = 64;
74+
75+
/** The six aggregate rows the Chart Gallery scatter plots — the same fixture
76+
* the objectui#7396 suite uses, and the one the false bubble-size prediction
77+
* was written about. Its y values span 12..60, a 5x spread. */
78+
const GALLERY: Row[] = [
79+
{ progress: 0, avg_estimate: 16.5 },
80+
{ progress: 45, avg_estimate: 40 },
81+
{ progress: 55, avg_estimate: 60 },
82+
{ progress: 80, avg_estimate: 24 },
83+
{ progress: 90, avg_estimate: 30 },
84+
{ progress: 100, avg_estimate: 12 },
85+
];
86+
87+
/** Deliberately a different order of magnitude from GALLERY, and a far wider
88+
* spread inside itself: if anything sized a mark from the data, these two
89+
* fixtures could not come out identical. */
90+
const WIDE_SPREAD: Row[] = [
91+
{ progress: 1, avg_estimate: 1 },
92+
{ progress: 50, avg_estimate: 5000 },
93+
{ progress: 99, avg_estimate: 250000 },
94+
];
95+
96+
const CONFIG = { progress: { label: 'Progress' }, avg_estimate: { label: 'Avg Estimate' } };
97+
98+
const renderScatter = (data: Row[]) =>
99+
render(
100+
<AdvancedChartImpl
101+
chartType="scatter"
102+
xAxisKey="progress"
103+
series={[{ dataKey: 'avg_estimate', label: 'Avg Estimate' }] as any}
104+
config={CONFIG as any}
105+
data={data as any}
106+
isAnimationActive={false}
107+
/>,
108+
);
109+
110+
/** Every mark's painted radius, straight off the symbol path: recharts writes
111+
* the symbol's bounding box onto it, so `width / 2` IS the drawn radius. */
112+
const radiiOf = (c: HTMLElement) => {
113+
const marks = [...c.querySelectorAll('path.recharts-symbols')];
114+
expect(marks.length, 'no marks were drawn, so this measures nothing').toBeGreaterThan(0);
115+
return marks.map((p) => {
116+
const r = Number(p.getAttribute('width')) / 2;
117+
expect(Number.isFinite(r), 'symbol carries no width, so this measures nothing').toBe(true);
118+
expect(r, 'symbol has no radius, so this measures nothing').toBeGreaterThan(0);
119+
return r;
120+
});
121+
};
122+
123+
describe('objectui#9681 — the scatter paints ONE mark size, and it is measured here', () => {
124+
it('paints every mark of a chart at the same size', () => {
125+
// The clause the objectui#7396 triage comment got wrong, stated as an
126+
// assertion: across a 5x spread of values, the marks are one size.
127+
const radii = radiiOf(renderScatter(GALLERY).container);
128+
expect(radii.length, 'the gallery fixture lost rows').toBe(GALLERY.length);
129+
expect(
130+
new Set(radii.map((r) => r.toFixed(6))).size,
131+
`mark size is NOT constant — radii ${JSON.stringify(radii)}. If that is intended, ` +
132+
'variable-area marks have arrived and SCATTER_SYMBOL_MAX_AREA needs re-checking.',
133+
).toBe(1);
134+
});
135+
136+
it('paints the same size for a completely different dataset', () => {
137+
// Uniformity within one chart could also come from rows that happen to
138+
// agree. This is the stronger statement: the size is a property of the
139+
// renderer, not of the data — two fixtures four orders of magnitude apart
140+
// come out identical.
141+
const [gallery] = radiiOf(renderScatter(GALLERY).container);
142+
cleanup();
143+
const [wide] = radiiOf(renderScatter(WIDE_SPREAD).container);
144+
expect(wide, 'mark size moved with the data').toBeCloseTo(gallery, 6);
145+
});
146+
147+
it("draws marks at recharts' implicit default area — nothing in this tree sets it", () => {
148+
// The reading itself, computed on the line below rather than written out.
149+
// A red here means
150+
// either recharts moved its default or this branch started declaring a
151+
// symbol size again — both are things a reader must be told about, because
152+
// the edge margin's justification names this as a third-party default.
153+
const expected = Math.sqrt(RECHARTS_IMPLICIT_SYMBOL_AREA / Math.PI);
154+
for (const r of radiiOf(renderScatter(GALLERY).container)) {
155+
expect(r, 'the painted symbol area is no longer recharts’ implicit default').toBeCloseTo(
156+
expected,
157+
6,
158+
);
159+
}
160+
});
161+
});

‎packages/plugin-charts/src/AdvancedChartImpl.tsx‎

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
PolarRadiusAxis,
2525
Scatter,
2626
ScatterChart,
27-
ZAxis,
2827
Cell,
2928
XAxis,
3029
YAxis,
@@ -395,12 +394,20 @@ const X_AXIS_ALL_LABELS_MAX_BUCKETS = 5;
395394
const ROTATED_X_LABEL_MAX_CHARS = 12;
396395

397396
/**
398-
* Symbol AREA envelope the scatter branch declares for its marks, in px².
397+
* Symbol AREA budget the scatter's edge margin below is sized against, in px².
398+
*
399+
* ⚠️ A HEADROOM figure, ⛔ NOT a reading of what the chart paints. Recharts
400+
* paints a scatter mark as a circle of `sqrt(area / PI)` and chooses that area
401+
* itself: nothing in this file and no authored metadata sets it, so every mark
402+
* is drawn at recharts' own implicit default and the budget here is
403+
* deliberately well above it. The size actually painted is measured off the
404+
* rendered symbol by `AdvancedChartImpl.scatterSymbolSize-9681.test.tsx` —
405+
* read that, never this number, for what the marks are.
399406
*
400407
* Kept as a named number because the padding below is derived from it rather
401-
* than guessed: the scatter's `<ZAxis type="number" range={[60, 400]} />`
402-
* declares that envelope, recharts paints a scatter mark as a circle of
403-
* `sqrt(area / PI)`, so this is the upper bound of the radius it can draw.
408+
* than guessed, and kept at THIS value rather than lowered to what is painted:
409+
* it is the envelope a variable-area scatter would have to stay inside, so the
410+
* margin already survives the day mark area becomes variable (objectui#9681).
404411
*/
405412
const SCATTER_SYMBOL_MAX_AREA = 400;
406413

@@ -434,13 +441,14 @@ const SCATTER_SYMBOL_MAX_AREA = 400;
434441
*
435442
* ## Why this size
436443
*
437-
* `sqrt(SCATTER_SYMBOL_MAX_AREA / PI)`, rounded up — the largest radius the
438-
* declared envelope admits. It is deliberately the envelope's upper bound and
439-
* not the radius observed today: with no `dataKey` on that ZAxis recharts
440-
* ignores the declaration entirely and paints every mark at its own implicit
441-
* default area (measured: r = 4.514px, i.e. area 64), so sizing this to what is
442-
* painted would tie the fix to a third-party default AND reopen the defect the
443-
* day a `dataKey` makes the declared envelope live.
444+
* `sqrt(SCATTER_SYMBOL_MAX_AREA / PI)`, rounded up — the largest radius that
445+
* budget admits. It is deliberately the budget and ⛔ not the radius observed
446+
* today: recharts chooses the symbol area itself and nothing here or in
447+
* authored metadata moves it, so sizing this margin to what is painted would
448+
* tie the fix to a third-party default AND reopen the defect the day mark area
449+
* becomes variable. That the painted radius still fits inside this margin is
450+
* asserted, not assumed — `AdvancedChartImpl.scatterSymbolSize-9681.test.tsx`
451+
* reads both off the same render, so the two cannot drift apart in silence.
444452
*
445453
* ## Why these are module constants and not inline objects
446454
*
@@ -1973,7 +1981,23 @@ function AdvancedChartImplInner({
19731981
padding={SCATTER_Y_AXIS_PADDING}
19741982
{...yAxisSpecProps(primaryY)}
19751983
/>
1976-
<ZAxis type="number" range={[60, 400]} />
1984+
{/* ⛔ No `<ZAxis>` here, deliberately — scatter mark AREA is recharts'
1985+
own implicit default and is not configurable in this product
1986+
today. This branch used to declare `<ZAxis type="number"
1987+
range={[60, 400]} />`, which painted NOTHING: recharts'
1988+
`selectZAxisWithScale` drops a z axis that carries no `dataKey`
1989+
before it reaches a mark, so the declared envelope never applied
1990+
and every mark came out at the implicit default size. Its only
1991+
effect was on readers — and it did mislead one, in writing, on
1992+
this repository: the triage comment on objectui#7396 derived a
1993+
variable-bubble-size prediction from it whose every clause was
1994+
false. ⭐ A DECLARATION IS NOT A READING: an inert declaration and
1995+
a live one look identical in the source, so what this branch
1996+
paints is stated by a test that reads the rendered symbol
1997+
(`AdvancedChartImpl.scatterSymbolSize-9681.test.tsx`), never by a
1998+
prop sitting here. Variable-area marks remain possible — they need
1999+
a `dataKey` and a schema key to feed it, which is a new capability
2000+
and its own card, not a prop restored here (objectui#9681). */}
19772001
<ChartTooltip content={<ChartTooltipContent />} />
19782002
{/* `nameKey` is REQUIRED here, for a reason unique to scatter
19792003
(objectui#7248). `ChartLegendContent` resolves a label as

0 commit comments

Comments
 (0)