Repository navigation
plugin-charts: a scatter's extreme points are half-clipped by the plot edge — the numeric axes get no domain padding #7396
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 分诊 —
domain:ui/priority:p3/pm:queue/bug锚定 (anchoring):
packages/plugin-charts/src/AdvancedChartImpl.tsx⇒domain:ui。在
origin/maina472b07上复核 —— 缺失属实散点的两条轴,逐字:
packages/plugin-charts/src/AdvancedChartImpl.tsx:1777 <ScatterChart> packages/plugin-charts/src/AdvancedChartImpl.tsx:1779 <XAxis packages/plugin-charts/src/AdvancedChartImpl.tsx:1780 type="number" packages/plugin-charts/src/AdvancedChartImpl.tsx:1781 dataKey={xAxisKey} packages/plugin-charts/src/AdvancedChartImpl.tsx:1785 minTickGap={isMobile ? 32 : 48} packages/plugin-charts/src/AdvancedChartImpl.tsx:1787 <YAxis packages/plugin-charts/src/AdvancedChartImpl.tsx:1788 type="number" packages/plugin-charts/src/AdvancedChartImpl.tsx:1794 width={48}两条轴都没有
domain属性。 正控制:domain这个词在本文件出现 13 行,所以这不是"整文件没这个词所以我搜错了",而是这两条轴上确实没有。⭐ 顺带解开一个悬案:
:1785那个minTickGap就是我在 #7386 的路由评注里标为"memo 之外的第二处硬编码"的那一处 —— 它服务的是散点的数值 x 轴。这对 #7386 是好消息:那张卡讲的是分类轴不该收时间序列的预算,而散点的 x 是数值轴,minTickGap在那里是正当的。⛔ #7386 的接手者不要顺手放宽:1785。定级理由
priority:p3:- 真实浏览器实测,数字很干净:绘图区 x 53..505,标记 cx 依次 53 / 256.4 / 301.6 / 414.6 / 459.8 / 505 —— 首末两个标记精确落在左右边界上,符号半径约 5px,所以各有约一半画在绘图区外。这是可复现的几何事实,不是印象。
- 但后果是纯视觉:数据没错,位置没错,只是边缘的点被裁了一半。读者仍能看到它、仍能 hover 到它。
- 无数据面、无静默错答、无安全面。
不上 p2:不影响任何判断的正确性;而且卡片自己也把它与 #7248(真正的误读来源是图例色块)区分开了 —— 那张才是"读者以为看到一个数据点"的误导,这张只是"点被切了一半"。
定型:Bug,不踩 manual floor —— 但影响面比看起来宽
卡片自己点了这一条,值得放大:
adding padding changes the appearance of every scatter, so it wants its own justification and its own pins rather than riding along.
加 domain padding 不改任何数据、不改授权面 ⇒ Bug/tidy。但它改变每一张散点图的外观(轴刻度范围会变),所以:
- 需要 before/after 截图论证;
⚠️ 任何钉住散点轴刻度值的既有测试都会红,那是预期的,要刻意更新。
⛔ 给执行席的边界
- 两条候选修法不等价,卡片给的顺序也不是随口的:
- 给数值 domain 加约一个符号半径的余量 —— 改的是数据到像素的映射,会让轴上出现"不整"的刻度端点(例如 x 轴从 -0.3 起)。
- 在图框里预留那条边距 —— 不动 domain,只把绘图区往内缩。⭐ 本席倾向这条更符合"Expected"那句话("A mark is drawn wholly inside the plot area"),且不会污染刻度。⛔ 但这是执行席的判断。
- 符号半径不是常数:
:1796有<ZAxis type="number" range={[60, 400]} />—— 气泡大小是可变的(面积 60..400 ⇒ 半径约 4.4..11.3px)。⚠️ 按固定 5px 加余量,在大气泡上仍会被裁。⛔ 这是本卡最容易做半吊子的地方,必须显式处理:要么按range上界取余量,要么说明为什么按典型值就够。 - ⛔ 不要顺手动
:1785的minTickGap—— 见上,那是 plugin-charts: a categorical x axis above 5 buckets still loses labels — minTickGap 48 is a time-series budget charged to band axes #7386 的邻居但不是它的目标。 - 钉子要断言"标记完全在绘图区内",⛔ 不要断言某个具体的 domain 数值 —— 那会把一次实现选择钉死。
⚠️ 并注意 plugin-charts: a categorical x axis above 5 buckets still loses labels — minTickGap 48 is a time-series budget charged to band axes #7386 记录的同一个陷阱:happy-dom 报告零文本度量,任何依赖测量的断言在本仓 DOM 环境里会因为错误的原因通过;这条也要走真实浏览器验证。
⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
Claim: objectui#7396 —
domain:uiexecution seat, sessionsession_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T10:29Z.
Seat: domain:ui#1
Branch: claude/issue-7396-scatter-extreme-mark-clipping
Clause-②: noState written in one call:
pm:queue→pm:dispatched, assigneeos-justin. Read backMATCHES:bug,priority:p3,domain:ui,pm:dispatched.Re-verified on
mainat15b33aeb4— the premise holds, with correctionsTriage read this on
a472b07twelve days ago. Both readings below are mine, taken in this act.probe triage ( a472b07)now ( 15b33aeb4)the scatter block :1777:1829its XAxis :1779–:1785:1831–:1837its YAxis :1787–:1794:1839–:1847either axis carries a domainpropno no ⚠️ Triage's lit control does not hold as written, and the correction matters. It reads "domain这个词在本文件出现 13 行,所以这不是「整文件没这个词所以我搜错了」". The bare word does appear on 13 lines — but as the JSX attributedomain=it appears 0 times in this file and 0 times anywhere underpackages/plugin-charts/src/. So "the word appears elsewhere" does not prove the attribute spelling can match. The honest control is different, and stronger:domainreaches an axis in this file through a helper, not through JSX.yAxisSpecProps(:1264) returns...(domain ? { domain } : {})fromdomainFor(axis), and it is spread at five sites —:1918,:1919,:2016,:2035,:2045. That is the control: the mechanism exists, is used five times, and the scatter is not one of them.⛔ The adjacent defect is FILED and is OUT OF SCOPE — objectui#9675
That same reading turned up something bigger than this card, and it is not yours to fix here: because the scatter bypasses
yAxisSpecProps, its y-axis silently drops a specmin,max,logarithmic,ticksandtitle. Filed as objectui#9675.⛔ Do not fix objectui#9675 in this PR. This card is p3 and purely visual; that one is a silent wrong answer and wants its own grading and its own pins.
⭐ But the two collide on one prop, which is why you are being told: an explicit
domainfor padding and a spec-deriveddomainare the same recharts prop. If you pad by settingdomain, you write the prop objectui#9675 is about, and whichever lands second will shadow the first.⇒ Prefer the card's second route — reserve the margin rather than move the domain. Recharts'
XAxis/YAxistakes apaddingprop — an object carryingleft/righton the x-axis andtop/bottomon the y-axis — which does this without touchingdomain.⚠️ The literal JSX spelling is not written out here: this repository'spost-stampedREFUSES any doubled-brace sequence it does not recognise as a stamp token, and JSX double braces collide with that namespace. Read the spelling off recharts' own types, not off this comment. Measured:padding=occurs 0 times in this file, so there is no precedent either way and you must justify the spelling you pick. If you conclude padding cannot do it and an explicitdomainis required, stop and report — that decision belongs on objectui#9675, not here.Hard constraints
- ⛔
:1837'sminTickGap={isMobile ? 32 : 48}must not be touched. Triage flagged it: it is the second hard-coded value it marked on objectui#7386, and it is legitimate here because scatter's x is a numeric axis, not the categorical axis that card is about. Relaxing it would silently take a bite out of objectui#7386. - ⛔
allowDataOverflow::1272records that it is what makes an explicit domain actually clip rather than be silently widened. Do not set it on these axes. - ⛔ Clause-②: no — and it stays
noonly if you add no new authorable key. If your fix wants a schema-level padding key, clause ② flips toyes, the card needs an at-tier contract review, and you must stop and report instead. Re-measure clause ② at delivery and return it as aclause_2field. - ⛔ Scope is the scatter branch of
AdvancedChartImpl.tsx. Do not change the other fouryAxisSpecPropssites, the grid, the legend, orZAxis.
⭐ How to verify — the DOM harness cannot measure this
happy-dom and jsdom report
clientWidth0 and therefore never fire container-size effects, and they apply CSS@mediarules regardless ofinnerWidth. Chart geometry is unmeasurable there — not unreliable, unmeasurable. Use the preinstalled Chromium at/opt/pw-browsers; ⛔ never runplaywright install.The card's own measurement is your before-control, and you must retake it yourself rather than quote it: Showcase Chart Gallery, Estimate vs Progress, viewport 1440, widget svg 510x350 — plot area x 53..505, marks at cx 53, 256.4, 301.6, 414.6, 459.8, 505, symbol radius ~5px. After: every mark's cx ± r lies strictly inside the plot area. Report both sets of numbers and the command that produced them.
⛔ A pin that asserts the fix must fail on
mainand pass on your branch — verify both directions before you call it a pin.Return
A clean stop on a falsified premise, with no PR and no code, is a full success. If the padding route cannot be justified, say so with the measurement and stop; do not widen this p3 into objectui#9675.
Generated by Claude Code
- ⛔
Claim amended — the
Thread-read:line my claim above omittedClaim: objectui#7396 —
domain:uiexecution seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T10:36Z. Amends comment5712863720; ⛔ nothing else about that claim changes and the dispatch already in flight is unaffected.
Thread-read: 5548586258
Branch: claude/issue-7396-scatter-extreme-mark-clipping
Clause-②: noThe charter's claim shape (SKILL.md:810) carries
Thread-read: <id of the newest comment on the card at the moment this claim is written>, and my claim above has no such line. The half-state patrol reports that omission as H50, and its reason is exactly the failure this card's dispatch avoided: the field cannot be filled without walking the thread to its last page, where triage's grading and prior rulings live — the content that OVERRIDES the body.⭐ The field is filled truthfully rather than retrofitted:
5548586258is quoted in the claim above, so the read it attests to is on the record in the same artefact.⚠️ Found by auditing my own claims after the patrol flagged an earlier one of mine on a different card. All three claims this seat posted today carried the same omission and all three are being amended.
Generated by Claude Code
os-dev-report
{
"issue": 7396,
"status": "done",
"branch": "claude/issue-7396-scatter-extreme-mark-clipping",
"pr": "#9680",
"session": "session_012EpHzwH4wTy5sd7ibkD2yq",
"premise_still_valid": true,
"clause_2": "no",
"summary": "Took the padding route, as directed: the scatter's XAxis and YAxis now carry recharts' axispaddingprop, reserving a margin at both ends of each numeric axis so an extreme mark is drawn wholly inside the plot area. Nodomainis written and noallowDataOverflowis set, so the prop objectui#9675 needs stays free;minTickGapis untouched; no authorable schema key was added, so clause 2 re-measures asno. The padding value is derived in code as ceil(sqrt(area / PI)) over the symbol-area envelope the branch's ZAxis declares, i.e. the largest radius that declaration admits, and the two padding objects are module constants because recharts memoises each axis's settings on its props and re-registers the axis when that object changes. Three premises of the dispatch order came out false on measurement; they are infalsified_premisesbelow, and none of them changes the ruled route. Draft PR 9680 opened, body first linePart of #7396, changeset added (real patch bump: this moves published plugin-charts source). Issue assignee was already set by PM (os-justin); I did not write it.",
"tests": "BEFORE/AFTER in real Chromium (preinstalled at /opt/pw-browsers, never installed):PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers node measure.mjsagainst a vite-served harness rendering AdvancedChartImpl scatter with the Chart Gallery 'Estimate vs Progress' rows in a 510px box, viewport 1440, svg 510x350. Plot area identical in both runs: x 53..505, y 5..296; symbol radius 4.514px. BEFORE (tree at 15b33ae, clean): cx 53, 256.4, 301.6, 414.6, 459.8, 505 and the y-max row at cy 5 -- worst overhang +4.514px OUTSIDE the plot area (first and last on the x boundary, y-max on the top boundary). The card's cx list is reproduced digit for digit. AFTER: cx 65, 257.6, 300.4, 407.4, 450.2, 493 and the y-max row at cy 17 -- worst case 7.486px INSIDE, every mark's centre +/- radius strictly inside on BOTH axes. PIN: packages/plugin-charts/src/AdvancedChartImpl.scatterEdgeClipping-7396.test.tsx, 5 tests, asserting only the invariant (mark vs the plot rect read from the SAME render) and never a coordinate or a domain value. ABLATION, run from the committed fix with a trap-guarded restore: on-disk mutation proven by anchor count (padding=occurrences 2 -> 0) before anything was read, mutated blob hash differs from the HEAD blob, andgit diff origin/mainshows no axis-prop difference -- i.e. the mutated axes are byte-identical to main's. Under it: 1 file failed, 5 of 5 tests failed, e.g. 'mark at cx 53 pokes past the left edge: expected 4.513516668382053 to be less than 0' and 'mark at cy 5 pokes past the top edge'. Restored withgit checkout HEAD -- PATH; blob hash back to the HEAD value andgit diff HEADempty (verified by state, not by exit code); re-run on the restored tree: 5 passed. No rebuild/dist leg applies -- the ablation target is the source file the vitest run resolves directly, not a dist artifact. PACKAGE:pnpm exec vitest run packages/plugin-charts/61 files / 548 tests passed (includes the existing objectui#7248 scatter pin);pnpm --filter @object-ui/plugin-charts type-checkexit 0;pnpm --filter @object-ui/plugin-charts lint0 errors (343 pre-existing warnings). CLOSURE:pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-charts^...' buildexit 0 under the shared verify lock (VERDICT command-exit 0). GATES: check-changeset-presence exit 0 (1 changeset declared); check:new-line-citations 0 new citations; check:control-bytes OK; check:vi-mock-specifiers / -inherit / -override-shape OK; check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:unreferenced-sources all exit 0; check-governed-queue-guard --test on the three changed paths: NOT GOVERNED. Control-byte self-scan over the three changed files: no matches. NOT MEASURED: the repo-widepnpm lintsweep (CI's run, not narrowed and not reproduced here); remote CI convergence (report is delivered at the end of local verification, as contracted). NOT MEASURED: the--pair PR-NUMBERcontract-review instrument -- no script in objectui accepts that flag, so its exit code has no reading here. PR 9680 carries labels plugin, plugin: charts, tests (all written by the repo's own labeler workflow) and does NOT carry needs:contract-review; I neither applied nor removed any label.",
"falsified_premises": [
"The DOM harness CAN measure this, sopremise_still_validis true for the DEFECT but the verification premise is false. The order states chart geometry is 'unmeasurable, not unreliable' in happy-dom. The reason given is real -- clientWidth 0, no container-size effect, @media applied regardless of innerWidth -- but it is exactly what the existing objectui#7248 pin already works around: with ResponsiveContainer mocked to a fixed box, recharts computes the plot rect, every centre and the symbol radius ARITHMETICALLY, with no DOM measurement in the path. Measured: the harness's x coordinates agree with the real-Chromium run to the digit (cx 65, 257.6, 300.4, 407.4, 450.2, 493 both places). What is NOT portable is any ABSOLUTE coordinate that depends on text metrics -- the legend is shorter there, so the plot rect comes out 310px tall against Chromium's 291 -- so the pin compares each mark against the plot rect read from the SAME render and pins no coordinate. This is why an in-repo regression pin exists at all instead of the browser run being the only evidence. The real-Chromium before/after was still taken as directed, and it is the number reported.",
"It is BOTH axes, not just x. The card and the order measure the x axis only. In the same before run the y-max row sat at cy 5 against a plot area starting at y 5 -- centred on the TOP boundary, overhanging by a full radius, the identical defect. Padding is therefore reserved on the y axis too and pinned separately.",
"The mark radius is a CONSTANT here, and not the value triage predicted. Triage warned the symbol is variable (area 60..400 from the branch's ZAxis, radius about 4.4..11.3px) and called this the easiest place to do a half job. Measured: every mark is 4.514px = sqrt(64 / PI) -- recharts' own IMPLICIT default area, not the declared 60. recharts' selectZAxisWithScale returns nothing for a ZAxis carrying nodataKey, so the declaration never reaches the mark. The half-job hazard triage named is therefore absent today; the margin is still sized to the declared envelope's upper bound rather than to the 4.514px observed, so it does not depend on either a recharts default or that declaration staying inert."
],
"mcp_calls": "0 -- no MCP GitHub tool was called, read or write; every GitHub read and write went through the REST proxy with curl",
"api_writes": "3 -- POST /repos/objectstack-ai/objectui/pulls (draft PR 9680); POST /repos//issues/7396/comments (this report); PATCH /repos/objectstack-ai/objectui/issues/comments/5713143502 (one correction to that same report comment, adding thefalsified_premisesits own summary pointed at -- one logical write delivered in two calls, declared rather than left silent). No labels written (the repo's labeler workflow owns PR labels here), no PATCH of any issue or PR body. Plus 2 git pushes to the branch (empty-branch routing probe, then the commit).",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the scatter branch'sZAxisdeclaration is INERT. recharts' selectZAxisWithScale returns nothing for a ZAxis carrying nodataKey, so the declared symbol-area range never reaches the mark and every mark is painted at recharts' implicit default area -- measured radius 4.514px, which is sqrt(64 / PI), the implicit default, not sqrt(60 / PI) from the declaration. This is a dead prop, not a wrong answer: nothing is mis-rendered and no metadata is dropped, so it is not class (a), (b) or (c). Successor: whoever gives that ZAxis adataKey, most likely the same file alongside objectui#9675. The margin landed here is already sized to the declared envelope's upper bound, so it does not break when they do.",
"noted, not filed: triage's lit control for thedomainabsence was already corrected by the dispatch order and I confirm the correction holds --padding=occurred 0 times in this file before this change, so there was no precedent for the spelling either; it was read off recharts' own XAxis/YAxis type declarations (padding?: XAxisPadding, an object of left/right; YAxisPadding, top/bottom) and off the range selector that applies it. Successor: none needed -- the dispatch order already carries it."
]
}
Generated by Claude Code
✅ ACCEPT — PR objectui#9680. ⛔ Not landing yet; CI re-read at the flip.
domain:ui@ objectui seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T10:57Z. Verified against the tree at head2463b7f1d, ⛔ not against the report.The route held, and the constraints held with it
constraint my reading of the diff ⛔ no domain, noallowDataOverflowadded lines matching either: 0 — the prop objectui#9675 needs is still free ⛔ minTickGapuntoucheddiff lines touching it: 0 — objectui#7386 protected ⛔ no new authorable key · Clause-② no^\+exportin the diff: 0the fix itself padding={SCATTER_X_AXIS_PADDING}andpadding={SCATTER_Y_AXIS_PADDING}, two lines⭐ The derivation and the observation agree, which is the part I checked hardest
SCATTER_AXIS_EDGE_PADDING = Math.ceil(Math.sqrt(400 / Math.PI)). Evaluated: 12.
Measured shift in the real-Chromium run: left edge53 → 65, right edge505 → 493. 12 and 12.A derived constant that lands exactly on the observed displacement is a much stronger signal than either number alone — the arithmetic and the browser are two independent instruments agreeing.
The pin is an invariant, not a photograph
Every assertion compares a mark against the plot rect read from the same render (
o.left < 0,o.right < 0, …). The only coordinates in the file are in its docblock. ⭐ Three things it gets right that this shift has seen go wrong:- Non-vacuity guards first: no plot rect, no marks, or a zero radius each fail with their own message — "symbol has no radius, so this measures nothing". That is the ablation-Leg-A class I caught on objectui#9648 today, closed here before it could open.
- Clearance is derived from the mark:
toBeLessThanOrEqual(-o.mark.r)— at least a full radius, sized by the symbol being measured, not by a literal. - ⭐ The tick values are pinned:
expect(ticks('bottom'), 'the x domain moved').toEqual(['0','25','50','75','100']). That is the discriminating control for the whole route choice — if a later change reserves the margin by moving the domain instead, this is what turns red. The route is now enforced, not merely documented.
Ablation: mutation proven on disk before anything was read (
padding=occurrences 2 → 0, mutated blob hash differs from HEAD's, andgit diff origin/mainshows the mutated axes byte-identical to main's) — then 5 of 5 failed, with named messages naming the real coordinates. Restored, and restoration proven by state (blob hash back to HEAD's,git diff HEADempty) rather than by an exit code. 5 passed on the restored tree.⛔ Three premises of mine came out false. The third one is the good news.
① "Chart geometry is unmeasurable in the DOM harness — not unreliable, unmeasurable." False as I applied it. The underlying fact is real (
clientWidth0, no container-size effect,@mediaapplied regardless ofinnerWidth), but it describes geometry driven by container measurement. WithResponsiveContainermocked to a fixed box, recharts computes the plot rect, every centre and the radius arithmetically, with no DOM read in the path — which is exactly what the existing objectui#7248 scatter pin already does. Measured: the harness's x coordinates agree with real Chromium to the digit (cx 65, 257.6, 300.4, 407.4, 450.2, 493 in both). What is genuinely not portable is any absolute coordinate depending on text metrics — the shorter legend makes the harness plot rect 310px tall against Chromium's 291 — which is precisely why the pin measures against the rect from its own render.⭐ This one is a new sub-class of my own error. The other four premises a lane has falsified today were my own greps over-read. This one I inherited verbatim from a predecessor's handover post (objectui#7233 §3) and passed on without testing it. Prose from a seat I trust is still an assertion about an artefact I did not measure.
② It is BOTH axes. The card and my order measured x only. In the same before-run the y-max row sat at
cy 5against a plot area starting aty 5— centred on the top boundary, overhanging by a full radius. Identical defect, and the diff reserves margin on both axes with its own assertions for each.③ The mark radius is a CONSTANT, and the hazard triage named does not exist — filed as objectui#9681. Every mark measures 4.514px =
sqrt(64 / PI), recharts' implicit default, becauseselectZAxisWithScalereturns nothing for aZAxiswith nodataKey: the branch'srange={[60, 400]}is inert. Triage's warning that "the bubble size is variable (area 60..400 ⇒ radius ~4.4..11.3px)" and that this is "the easiest place on this card to do a half job" is false in every clause.⭐ And the fix is right anyway, for a better reason than the one it was asked for. Triage's instruction — size the margin to the
rangeupper bound — was the correct engineering call derived from a false premise, and the lane followed it while measuring that the premise was false. So the margin is 11.28px-worth rather than 4.514px-worth, which means it neither depends on a third-party default nor reopens the defect the day someone gives that ZAxis adataKey. Sizing to what is painted today would have done both.Recorded, not required
The lane notes no script in objectui accepts
--pair PR-NUMBER, so that instrument has no reading here — correct, and correctly reported as no reading rather than as a pass. Labels on the PR were written by the repo's own labeler workflow; the lane neither applied nor removed one. Changeset: a realpatchbump, since this moves publishedplugin-chartssource.⛔ Not landing on this comment
I re-read CI at the moment of the flip and ⛔ never inherit a CI reading from a comment — including this one.
Generated by Claude Code
✅ LANDED — PR objectui#9680 squash-merged to
main(e859ad07c), 2026-09-17T11:44:50Zdomain:ui@ objectui seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T11:48Z. Verified by content on mergedmain, ⛔ not from the PR page and ⛔ not from the dev report.probe reading on merged mainSUBJECT — the x axis reserves its margin padding={SCATTER_X_AXIS_PADDING}at:1901SUBJECT — the y axis too padding={SCATTER_Y_AXIS_PADDING}at:1911CONTROL — the prop objectui#9675 needs is still free domain=across the whole file: 0CONTROL — no clipping semantics were introduced allowDataOverflowon the scatter axes: 0CONTROL — the constant is derived, not guessed ceil(sqrt(400 / PI))= 12, and the measured edges moved53 → 65and505 → 493— 12 and 12That last row is the one I trust most: a constant computed from the declared symbol envelope landing exactly on the displacement a real browser measured is two independent instruments agreeing.
Two things this card was wrong about, and one it was right about for the wrong reason
⛔ It is both axes. The card and my order measured x only; the y-max row sat at
cy 5against a plot area starting aty 5, overhanging the top boundary by a full radius.⛔ My verification premise was false. I wrote that chart geometry is "unmeasurable, not unreliable" in the DOM harness. With
ResponsiveContainermocked to a fixed box recharts computes the plot rect, every centre and the radius arithmetically — measured, the harness agrees with real Chromium to the digit. ⭐ I inherited that sentence verbatim from a predecessor's handover and passed it on without testing it; prose from a seat I trust is still an assertion I have not measured.⭐ Triage's instruction was right for a reason that is false. It warned the symbol is variable (area 60..400, radius ~4.4..11.3px) and called this the easiest place to do a half job. Measured: every mark is 4.514px =
sqrt(64 / PI), recharts' implicit default, becauseselectZAxisWithScalereturns nothing for aZAxiswith nodataKey— the declaration is inert (filed as objectui#9681). Its prescription — size the margin to therangeupper bound — was nevertheless the correct call, and following it is exactly why the landed margin neither depends on a third-party default nor reopens this defect the day someone gives that ZAxis adataKey.Residue: none on this card
The adjacent defects are filed and were kept out deliberately: objectui#9675 (the scatter bypasses
yAxisSpecProps, so a specmin/max/logarithmic/ticks/titleon its y-axis is silently dropped) and objectui#9681 (the inertZAxis). The landed pin enforces the route:expect(ticks('bottom'), 'the x domain moved')turns red if a later change reserves the margin by moving the domain instead.⇒ Closing, with the
pm:*state stripped in the same write.
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Found while fixing objectui#7248 (that card's stray dot turned out to be the legend swatch; this is a separate, real instance of the same reader-facing complaint, and is NOT addressed by that PR).
What happens
The scatter branch of
AdvancedChartImplrenders its two numeric axes with nodomain, so recharts fits the domain to[dataMin, dataMax]on x. Rows sitting at either extreme land exactly ON the plot boundary, and since a mark has a radius, roughly half of each extreme symbol renders outside the plot area.Measured
Showcase Chart Gallery, Estimate vs Progress, real Chromium, viewport 1440, widget svg 510x350:
The first and last marks sit precisely on the left and right boundary. The symbol radius is about 5px, so about half of each is painted outside the plot area. Visible in the screenshots on objectui#7248 as clipped half-dots hugging both edges.
Why it is filed separately
Different mechanism from objectui#7248 (axis domain fitting, not legend label resolution) and a different blast radius: adding padding changes the appearance of every scatter, so it wants its own justification and its own pins rather than riding along.
Expected
A mark is drawn wholly inside the plot area — either by padding the numeric domain by roughly one symbol radius, or by reserving that margin in the chart box.
Generated by Claude Code