…ot area
Both scatter axes are numeric and carry no explicit domain, so recharts fits the
domain to `[dataMin, dataMax]` and maps it across the whole plot box. A row at
either extreme is therefore CENTRED on the boundary, and since a mark has a
radius, about half of each extreme symbol paints outside the plot area.
Measured on the Chart Gallery scatter ("Estimate vs Progress") in real Chromium
— viewport 1440, widget svg 510x350, plot area x 53..505 / y 5..296:
before: cx 53, 256.4, 301.6, 414.6, 459.8, 505 and the y-max row at cy 5,
radius 4.514px — first and last ON the x boundary, y-max ON the top
boundary, each overhanging its edge by a full radius.
after: cx 65, 257.6, 300.4, 407.4, 450.2, 493 and the y-max row at cy 17 —
worst clearance 7.486px inside the plot area, on both axes.
The card reported the x axis only; the y axis clipped the same way.
The margin is reserved as recharts' axis `padding`, which insets the pixel range
the scale maps into and leaves the domain alone, so every tick VALUE is
unchanged and only the mapping moves. Moving the domain instead would invent
unround tick endpoints and would write the same recharts prop a spec-declared
`min`/`max` needs, where whichever landed second would shadow the other.
Its size is the largest radius the scatter's declared symbol-area envelope
admits, not the radius drawn today: with no `dataKey` on that ZAxis recharts
ignores the declaration and paints every mark at its own implicit default area,
so sizing to what is painted would tie the fix to a third-party default and
would reopen the defect the day the declared envelope goes live.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq
Part of #7396
The defect
Both axes of the scatter branch in
AdvancedChartImplare numeric and carry no explicitdomain, so recharts fits the domain to
[dataMin, dataMax]and maps it across the wholeplot box. A row at either extreme is therefore centred on the boundary, and since a mark
has a radius, about half of each extreme symbol paints outside the plot area.
Before / after, measured in real Chromium
The DOM harness cannot fire a container-size effect (
clientWidth0), so the card's readingwas reproduced rather than quoted: the chart is rendered at the widget's real box (svg
510x350) in the preinstalled Chromium at viewport 1440, and the geometry is read off the live
SVG — the plot area from the chart's clip rect, each mark's centre and radius from its own
symbol path.
Plot area is identical in both runs: x 53..505, y 5..296. Symbol radius 4.514px.
main15b33ae)Before, the first and last marks sit exactly on the x boundary and the y-max row exactly on
the top boundary, each overhanging by a full radius. After, every mark's centre ± radius is
strictly inside the plot area on both axes.
Two readings differ from the card, both reported rather than papered over:
plot's top edge in the same run. Fixed with it, and pinned.
warned the symbol is variable (area 60..400 from the branch's
ZAxis, radius 4.4..11.3px).Measured: every mark is 4.514px, which is
sqrt(64 / PI)— recharts' own implicit defaultarea. Its
selectZAxisWithScalereturns nothing for aZAxiswith nodataKey, so thedeclared
range={[60, 400]}never reaches the mark and the scatter falls back toimplicitZAxis.range. That inert declaration is noted below, not touched.What changed
Two props, on the scatter's own XAxis and YAxis, plus the constants they read.
The margin is reserved as recharts' axis
padding, not as adomain. Padding insets thepixel range the scale maps into and leaves the domain alone, so every tick value is
unchanged — the axes still read 0/25/50/75/100 and 0/15/30/45/60 — and only the mapping moves.
Moving the domain would invent unround tick endpoints, and it would write the same recharts
prop a spec-declared
min/maxneeds, where whichever landed second would shadow the other.objectui#9675 is not addressed here, and reserving the margin leaves that prop free for it.
The padding value is
ceil(sqrt(area / PI))over the symbol-area envelope the branch declares,i.e. the largest radius that declaration admits — deliberately the envelope's upper bound and
not the 4.514px drawn today, so neither a change in recharts' implicit default size nor a
dataKeyarriving on thatZAxiscan reopen the defect. The two padding objects are moduleconstants because recharts memoises each axis's settings on its props and re-registers the axis
when that object changes; a fresh literal per render would defeat that memo.
minTickGapis untouched,allowDataOverflowis not set on either axis, and no authorableschema key was added.
The pin, verified in both directions
AdvancedChartImpl.scatterEdgeClipping-7396.test.tsxasserts the invariant triage asked for —every mark wholly inside the plot area — and never a coordinate or a domain value: each mark is
compared against the plot rect read from the same render.
It measures something here for a reason worth stating: with
ResponsiveContainermocked to afixed box (the shape the existing objectui#7248 pin already uses), recharts computes the plot
rect, every centre and the symbol radius arithmetically, with no DOM measurement in the path.
The x coordinates agree with the Chromium run to the digit. What is not portable is any
absolute coordinate that depends on text metrics — the legend is shorter there, so the plot
rect is taller — which is why nothing absolute is pinned.
Ablation, run from the committed fix: removing the two
paddingprops leaves the two axesbyte-identical to
main's and turns the pin red.Restored with
git checkout HEAD -- PATH; the file's blob hash is back to itsHEADvalueand
git diff HEADis empty. Re-run on the restored tree: 5 passed.Verification
pnpm exec vitest run packages/plugin-charts/pnpm --filter @object-ui/plugin-charts type-checkpnpm --filter @object-ui/plugin-charts lintnode scripts/check-changeset-presence.mjspnpm check:new-line-citationspnpm check:control-bytespnpm check:vi-mock-specifiers/-inherit/-override-shapepnpm check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:unreferenced-sourcesnode scripts/check-governed-queue-guard.mjs --test THE-THREE-PATHSThe repo-wide
pnpm lintsweep is CI's run, not reproduced locally.Acceptance notes
Noted while measuring, not filed and not touched here:
ZAxisdeclaration is inert. With nodataKeyon it, recharts drops the axisbefore the mark ever reads it, so the declared symbol-area range has no effect and every mark
is drawn at recharts' implicit default size. It is a dead prop, not a wrong answer: nothing is
mis-rendered and no metadata is dropped. Whoever gives that
ZAxisadataKey— the samefile, most likely alongside objectui#9675 — will meet it; the margin here is already sized for
that case, so nothing breaks when they do.
Generated by Claude Code