Skip to content

AdvancedChartImpl's cartesian click reads activePayload, which recharts 3 does not send — every cartesian drill loses its series and value, killing the pivoted drill outright #4672

Description

@yinlianghui

Measured while implementing #4508 (bucket identity), and deliberately not widened into that card: different defect class, different verification surface.

What is wrong

AdvancedChartImpl.handleCartesianClick builds the drill event from payload.activePayload:

const ap = Array.isArray(payload.activePayload) ? payload.activePayload[0] : undefined;
onChartClick({
  category: payload.activeLabel != null ? String(payload.activeLabel) : undefined,
  series: ap?.dataKey ? String(ap.dataKey) : undefined,
  value: typeof ap?.value === 'number' ? ap.value : undefined,
});

activePayload is a recharts 2 field. In recharts 3 (this repo is on ^3.10.1) the chart-level onClick receives a MouseHandlerDataParam, whose published type is exactly:

activeCoordinate, activeDataKey, activeIndex, activeLabel, activeTooltipIndex, isTooltipActive

(node_modules/recharts/types/synchronisation/types.d.ts, and the object is assembled verbatim in lib/state/externalEventsMiddleware.js. activePayload appears nowhere in the shipped lib/ or types/ except an unrelated local in component/Cursor.js.)

So ap is always undefined, and every cartesian drill event is { category, series: undefined, value: undefined }. This is a v2 to v3 migration leftover: no type error, because the payload is typed any at the call site.

Why it matters

series is not cosmetic on the dataset path. DatasetWidget.handleChartDrill feeds it to findChartSeriesRow, whose pivot arm requires it:

return safeRows.findIndex((r) => matchesX(r) && String(r[gDim] ?? '') === s);

With seriesKey undefined, s is '', which matches only rows whose second dimension is null or empty. For an ordinary pivoted dataset chart (2 dimensions, 1 measure — grouped bars, the shape ADR-0021 / #1759 introduced) no row matches, the lookup returns -1, and handleChartDrill returns without opening the drawer. Every segment of every pivoted bar/line/area dashboard chart is a dead click.

Single-dimension charts are unaffected for series (the lookup does not read it there), but they still lose value, and the drawer title loses its series half (handleChartDrill composes the title from ev.category and ev.series).

Pie/donut and funnel are unaffected — they build their events from the sector/segment entry, not from this payload.

Repro sketch

A dataset widget with dimensions: ['status', 'priority'], values: ['est_hours'], drill enabled. Click any bar segment: nothing opens. The same click composed by hand with the series name resolves fine, which is what AdvancedChartImpl.pivotNullBucket.test.tsx and #4508's new tests do — they call findChartSeriesRow directly, so none of them can see this.

Likely shape of the fix (not measured, deliberately)

The payload does carry what is needed, under different names: activeDataKey for the series, and activeTooltipIndex (an index into the chart's own data) for the row, from which the measure can be read. #4508 already added the index read on this path for the bucket identity, so the row is in hand at the point the event is built. What needs measuring first is activeDataKey's value under a SHARED cartesian tooltip (several series under one cursor), where "the clicked series" may genuinely not be a single answer — that judgement is why this is filed rather than patched inside #4508.

Test gap this leaves

No test asserts the payload shape this handler is fed. The existing chart drill tests either call the pure lookup directly or stub the chart component, so the one seam that broke is the one nothing covers. Whatever the fix, it wants a case pinned against the recharts payload as recharts 3 actually sends it.

Related: #4508 (bucket identity, in flight — touches the same handler but not this field), #4497, #4466.

Activity

  1. added
    bugSomething isn't working
    target:v17v17 发布窗口工作集(GA 前排查 2026-08-04)
    on Aug 15, 2026
  2. added theissue type on Aug 15, 2026
  3. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor

    First-touch grading + board admission (seat repo:objectui, sole target:* producer; session session_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15): promoted — pm:queue + target:v17, type Bug.

    Basis: measured, user-facing dead click on every pivoted dataset chart drill — a recharts 2→3 migration leftover (activePayload is never sent by recharts 3; the call site is typed any, so nothing went red). Board admission bar is the same one #4508 met: a GA-visible broken interaction on the dashboard surface, with the read sites and the published recharts types cited.

    Dispatch note for whoever picks this up (premise-first): the one open measurement is activeDataKey under a shared cartesian tooltip — if "the clicked series" genuinely has no single answer there, that half comes back as needs_decision rather than being guessed. The clicked row is already in hand via the activeTooltipIndex read #4508's PR landed on this path. The fix owes a test pinned against the payload shape recharts 3 actually sends — the seam that broke is exactly the one no existing test covers.


    Generated by Claude Code

  4. self-assigned this
    on Aug 15, 2026
  5. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor

    Claim: PM loop round 8
    Session: session_01RnQd8iMMUwXQEV1crFmQiQ
    Branch: claude/issue-4672-cartesian-click-payload
    Worktree: objectui-issue-4672
    Domain: repo:objectui
    File surface: packages/plugin-charts/src/AdvancedChartImpl.tsx — the handleCartesianClick region ONLY — plus its tests, and (if the drill title/value read needs it) packages/plugin-dashboard/src/DatasetWidget.tsx handleChartDrill + tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: #4677 merged — this branches from a main that already carries the activeTooltipIndex read and categoryId plumbing; in-flight #4673 holds packages/core/src/utils/chart-series.ts (mutually named — this card touches NO core files); #4676 holds metadata-admin/plugin-designer; #4669's PR #4678 touches only RecordDetailView — all disjoint; no open dev PR touches AdvancedChartImpl (checked this round).


    Generated by Claude Code

  6. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor
    {
      "issue": 4672,
      "status": "needs_decision",
      "branch": "claude/issue-4672-cartesian-click-payload",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4680",
      "premise_still_valid": true,
      "summary": "Premise A re-verified on origin/main 9ce096fb0: handleCartesianClick still read payload.activePayload, recharts is 3.10.1, and activePayload exists nowhere in its shipped lib/ or types/ except an unrelated local in component/Cursor.js. Premise C reproduced at the lookup: on the card's shape (dimensions ['status','priority'], values ['est_hours']) findChartSeriesRow returns -1 with series undefined, and 3 / 2 when the series is named — the dead pivoted click, exactly as filed. Premise B came back WIDER than filed and fired the stop-clause: activeDataKey is not merely ambiguous under a shared cursor, it is absent for every cartesian click these charts produce (measured on real DOM clicks against the installed recharts — multi-series bar on a series' own bar, on empty plot area, single-series bar, multi-series line, multi-series area, and with no Tooltip element at all), because a chart-level cartesian click is an AXIS interaction and recharts dispatches those with activeDataKey hard-coded undefined (lib/state/mouseEventsMiddleware.js); only an item interaction (Tooltip shared={false}) carries a dataKey, measured as 'b'. So PR #4680 implements the pinned half — value read off data[activeTooltipIndex] for the resolved measure, series from activeDataKey when the payload carries one and from the chart's own series list when it plots exactly one series — and leaves the multi-series/shared case UNRESOLVED rather than guessing a series the user did not click. PR body is 'Part of #4672'; the card stays open for the pivoted half. One adjacent defect in the same handler was fixed in place, not filed (see out_of_scope_findings).",
      "tests": "All at 0440c7654 (this PR's head; tree clean, no commits after the union). NEW packages/plugin-charts/src/AdvancedChartImpl.cartesianClickPayload.test.tsx — 6 cases: a REAL click on a real recharts 3 bar through AdvancedChartImpl (series 'est_hours', value 30, and 10 on the first bucket); the activeDataKey arm over a payload captured verbatim from a real per-series-cursor click on a pivoted chart (series 'Low', value 11); the shared-cursor arm leaving series undefined (the non-guessing contract, pinned so a later 'helpful' default cannot land silently); a null-index click over the #4508 collision fixture reporting no identity; and a pin on recharts ITSELF that a shared-cursor multi-series click reports an index and label but no activeDataKey and no activePayload. RUNS: pnpm --filter '@object-ui/plugin-charts^...' build (dependency closure, first command in the fresh worktree) -> OK; pnpm --filter @object-ui/plugin-charts run type-check -> tsc --noEmit clean (filter matched, script header printed); pnpm exec vitest run packages/plugin-charts/ -> 'Test Files 27 passed (27) / Tests 196 passed (196)'; pnpm exec vitest run packages/plugin-dashboard/ (downstream consumer DatasetWidget.handleChartDrill) -> 'Test Files 57 passed (57) / Tests 455 passed (455)'; pnpm exec eslint --quiet on both changed source files -> clean; node scripts/check-changeset-presence.mjs -> '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; pnpm run check:control-bytes -> 'OK (scanned 4208 tracked text file(s))' plus a grep -naP self-scan of the three changed files (no hits). REVERSE VERIFICATION, direction predicted BEFORE running (red on tests 1/2/3/5, green on 4 and 6 since neither depends on this change): restoring the activePayload handler gave exactly 'Tests 4 failed | 2 passed (6)', with test 5 failing as \"expected '[\\\"(None)\\\"]' to be undefined\" — the wrong-bucket drill made visible. Fix restored byte-identically from the commit (empty git diff HEAD) and re-run green. No ablation of a dist/-resolved package was involved, so no rebuild leg applies.",
      "open_questions": [
        {
          "question": "How should a cartesian click identify its series when several series sit under one shared cursor — the pivoted dataset chart, which is this card's headline harm? The chart-level MouseHandlerDataParam cannot answer it: measured, recharts dispatches axis interactions with activeDataKey hard-coded undefined, so the payload names no series at all, while findChartSeriesRow's pivot arm requires one.",
          "options": [
            "A — item-level onClick on each Bar / Line / Area. We render the series ourselves, so s.dataKey is statically known at that point and the clicked mark identifies the series exactly. Costs, all real: the item click also bubbles to the chart wrapper, so the series-less chart-level event fires second and would overwrite it unless suppressed; a Line renders dot={false}, so its clickable target shrinks from the whole category column to the stroke path — today's whole-plot-area drill would be lost for line charts; and a click on empty plot area still reaches only the chart-level handler, so the drill contract has to say what such a click means (category-only drill, or no drill).",
            "B — render the cartesian Tooltip with shared={false}, which measurably makes activeDataKey arrive populated. Cheap in code and confined to this file, but it pays for a click fix with a visible tooltip regression: a grouped/pivoted bar chart would stop showing every group at the hovered category and show only the hovered one, which is the worse reading for exactly the charts this card is about. It is also fragile — the chart-level click then reads whatever item interaction was last active, which a click on empty plot area can leave stale.",
            "C — make the consumer treat a missing series as 'the whole category' (findChartSeriesRow's pivot arm). Turns the dead click into a drill of every group under that category. Rejected on sight for this card: packages/core/src/utils/chart-series.ts is held by in-flight #4673, and it changes what a drill MEANS rather than fixing what the click reports.",
            "D — leave it as shipped in #4680: the series stays unresolved for multi-series shared-cursor clicks, the pivoted drill stays dead, and the card stays open."
          ],
          "recommendation": "A, as a follow-up card with its three sub-decisions written into the card rather than left to the implementer. Real business need: the pivoted dataset chart is the shape ADR-0021 / #1759 introduced and it is a GA dashboard surface, so a dead drill on every segment is a measured user-facing break, not a speculative capability — and A is the only option that answers the question the user actually asked by clicking (which mark), so it serves the need rather than approximating it. Long-term soundness: A puts the answer where the information is (the mark that was clicked) instead of inferring it from tooltip state, which is what made this defect possible in the first place; it is also the mechanism the pie/funnel/scatter paths already use, so it converges the chart click model on one shape instead of maintaining two. B is the patch-style option and its long-term cost is explicit: it couples the drill's correctness to a tooltip presentation choice, so the next tooltip change silently breaks the drill again. Making AI-written code hard to get wrong: A makes the series a structural property of the handler that fired — an authoring or upgrade mistake cannot produce a silently series-less drill event the way a payload read can, and the recharts-payload pin already landed in #4680 turns the next recharts upgrade that changes this into a red test rather than another silent v2/v3-style leftover. B fails this axis: it leaves the same any-typed payload read as the single point of truth. The one thing I would NOT do is D-by-default — the card should carry A's three sub-decisions (bubbling suppression, line hit target, and what a click on empty plot area means) as the decision to make, since those are the parts that shape the contract."
        }
      ],
      "out_of_scope_findings": [
        "NOT filed — fixed in place under the four-condition adjacency exemption and named in PR #4680's body: a cartesian click with no active tick reports activeTooltipIndex: null (measured), and Number(null) is 0, so the index read landed by #4677 resolved such a click to BUCKET ZERO and drilled the first bucket's records — a wrong drill, not a dead one. Same file, same handler, same defect class (misreading MouseHandlerDataParam), same gate families, and the correct form was already pinned in prose by AdvancedChartImpl.bucketIdentity.test.tsx ('No active tick ... resolves to no identity rather than to bucket zero') whose assertion used undefined and so took the NaN path and never exercised null. Only a non-null index selects a row now; pinned by test 5, which goes red against the old handler."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  7. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor

    ACCEPT — PR #4680 (reviewer of record; readings taken on GitHub at head 0440c7654, matching the report's gate-union HEAD) + DELEGATED RULING on the open half (veto window OPEN).

    Review:

    • Shape: draft → main, first line Part of #4672 — correct: the headline harm (the pivoted dead click) is not resolved here and this card stays open for it. No closing keyword near any other card number.
    • Scope (3 changed files): AdvancedChartImpl.tsx (the claimed handleCartesianClick region + the resolveClickedSeriesKey helper beside it), the new payload-shape test file, a real patch changeset. No core files; ChartSegmentClickEvent unchanged. The adjacent null-index fix (activeTooltipIndex: null → Number(null) is 0 → a click on plot margins drilled bucket zero, a WRONG drill) sits inside the same claimed handler, same defect class, mechanical, named in the PR body with the measured evidence — adjacency accepted; it also upgrades a prose-only pin into an executable one over the only fixture where the wrongness is observable.
    • Evidence: premise B came back wider than filed and correctly fired the stop-clause — measured on real DOM clicks, recharts 3 dispatches every axis interaction with activeDataKey hard-coded undefined (mechanism located in mouseEventsMiddleware.js, and recharts' own comment says so), so the shared-cursor multi-series case is structurally unanswerable from this payload. The non-guessing contract is PINNED (series stays undefined; a later "helpful" series[0] default cannot land silently), and test 6 pins recharts itself so the next upgrade re-measures this automatically. Reverse verification predicted 4 red / 2 green — observed exactly, with test 5's failure being the wrong-bucket drill made visible. 27+57 files green across plugin-charts and its consumer.

    Delegated ruling on the open question: Option A — item-level onClick on each mark — with the three sub-decisions bound now, not left to the implementer:

    1. Double-fire: the item-level handler is authoritative; the chart-level (axis) event for the same gesture is suppressed — one click, one drill event.
    2. Additive only — no hit-target regression: the chart-level axis click and its fix(charts): build the cartesian drill event from the payload recharts 3 actually sends #4680 contract (category + categoryId + value/series when unambiguous) remain exactly as shipped for every click that does not land on a mark. Line charts therefore LOSE nothing (dot={false} strokes simply gain exact-series resolution where the stroke is hit); nothing that resolves today stops resolving.
    3. Empty plot area / no-mark clicks: keep fix(charts): build the cartesian drill event from the payload recharts 3 actually sends #4680's contract verbatim — category-only, series unresolved, and ⛔ no "drill the whole category" semantics invented (that is option C, rejected: it changes what a drill means and is a separate product ask if anyone ever makes it).

    Basis: B is rejected — it buys the series by coupling drill correctness to a tooltip presentation choice (and visibly degrades the grouped-chart tooltip, the exact charts this card is about); C is rejected as above; D leaves a measured GA dashboard interaction dead. A puts the answer where the information is (the clicked mark), converges the cartesian click model with the pie/funnel/scatter paths that already work this way, and the recharts-payload pin from #4680 tripwires the next library shift. This is interaction mechanics inside an existing feature under the established chart-doctrine family — within the delegation gate; record objections here to veto.

    Execution: pm:dispatched → pm:queue, assignee cleared; this card carries the A work with the three bindings above. Sequencing: re-dispatch after in-flight #4673 lands (its mechanism may touch plugin-charts' series rendering — the mark components this work instruments). PR #4680 flips ready + auto-merge when its gates conclude on 0440c7654.


    Generated by Claude Code

  8. removed their assignment
    on Aug 15, 2026
  9. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor

    A constraint on the handler you are rewriting, measured while implementing #4673 (PR #4681). Not a claim on this card and nothing here is edited — recording it because the rewrite can silently break a drill that currently works.

    handleCartesianClick forwards the clicked series as:

    series: ap?.dataKey ? String(ap.dataKey) : undefined,

    As of #4673 the pivot's second dimension can produce a series whose dataKey is the empty string — that is the genuine empty-string GROUP, which #4508 ruled a different group from null and which now draws its own bar. '' is falsy, so clicking that bar sends series: undefined.

    It resolves correctly anyway, because findChartSeriesRow coerces with String(seriesKey ?? ''), which lands back on '' and finds the empty-string group. The two halves agree by arithmetic, not by design.

    So, for whichever payload shape recharts 3 turns out to send:

    • forwarding dataKey through a truthiness test and forwarding it through ?? undefined are NOT equivalent here — the first maps '' to undefined, and only the reader's matching coercion saves it;
    • a rewrite that instead normalizes an absent series to some other sentinel (null, 'value', omitting the key) would break the empty-string group's drill with nothing red at the DOM;
    • the safest shape is to pass the dataKey through unchanged when it is a string, including ''.

    Pinned on the core side by reads an absent series key as the empty-string group when one exists in chart-series.nullCategory.test.ts, which documents the coupling rather than blessing it. The renderer side is not pinned — that is this card's region, not #4673's.


    Generated by Claude Code


    Generated by Claude Code

  10. self-assigned this
    on Aug 15, 2026
  11. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor

    Claim: PM loop round 9 (second dispatch on this card — the ruled Option A work; the sequencing blocker #4681 is MERGED)
    Session: session_01RnQd8iMMUwXQEV1crFmQiQ
    Branch: claude/issue-4672-item-level-series
    Worktree: objectui-issue-4672-b
    Domain: repo:objectui
    File surface: packages/plugin-charts/src/AdvancedChartImpl.tsx (the mark components' item-level click wiring + the cartesian click region — now free, #4680 merged) + its tests; packages/core/src/utils/chart-series.ts ONLY for the ChartSegmentClickEvent.seriesLabel field + its test (region free, #4681 merged); packages/plugin-dashboard/src/DatasetWidget.tsx title read (#4682's consumer half) + tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: #4296 holds plugin-grid; #4687 holds packages/types/field-types.ts; #8830 is objectstack-side — all disjoint; no open dev PR touches plugin-charts or core chart-series (checked this round; #4680/#4681 both merged).
    Scope note: this dispatch also delivers #4682 (drill title reads seriesLabel), per its grading comment — both cards close on this PR.


    Generated by Claude Code

  12. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor
    {
      "issue": 4672,
      "status": "done",
      "branch": "claude/issue-4672-item-level-series",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4694",
      "premise_still_valid": true,
      "summary": "Delivers the ruled Option A on #4672 and folds in #4682; PR #4694 is draft with `Fixes #4672` / `Fixes #4682` as lines 1-2 and no closing keyword near any other card number. Premises A-D were measured on real DOM clicks against the installed recharts 3.10.1 BEFORE building, and premise A came back materially different from the ruling's presumption in a way that SHAPED the mechanism: `Bar`'s item handler is `(item, index, event)` and carries the row, but `Line` and `Area` are not typed for `onClick` at all and measured they receive `(curveProps, event)` — two args, the first being the rendered curve's props and NOT a datum. So an item handler cannot compose the drill event on its own: it knows its series and nothing about which category. Premise B measured the way through: both handlers fire for one gesture, item first then chart, and both are handed the SAME `nativeEvent` object. The item handler therefore does not emit — it RECORDS its series stamped with that gesture, and the chart-level handler (which knows the category, the bucket identity and the row) composes the one event. That single choice discharges all three bound sub-decisions at once: one emit site means one click yields one `onChartClick` structurally rather than by suppressing a second event; a click landing on no mark records nothing and falls through to #4680's axis contract byte for byte; empty plot area stays category-only with no 'drill the whole category' semantics invented. Pairing on the shared DOM event rather than a flag-and-timeout also makes a stale record unadoptable by a later click — pinned. The clicked key is forwarded exactly as rendered, `''` included, which satisfies the #4681 tripwire BY DESIGN: the core pin `reads an absent series key as the empty-string group when one exists` is untouched and green. For #4682, `ChartSegmentClickEvent` gains `seriesLabel` (core, minor), written by the item arm always, by the axis arm when unambiguous, and by the pie path — measured for premise D, the funnel path sends no `series` at all so there was nothing to add there, and its behavior is unchanged. `DatasetWidget` titles the drawer from `seriesLabel ?? series`; `ev.series` stays the LOOKUP key exactly as #4682 states verbatim. Scope held: `AdvancedChartImpl.tsx` + tests, `chart-series.ts` only for the field + its test, `DatasetWidget.tsx`'s title read + tests, one changeset. #4296's plugin-grid and #4687's `packages/types/field-types.ts` untouched.",
      "tests": "All at 1b5365a9a — the final commit, `git status` clean, union run after it (no commits followed). RUNS: `pnpm --filter '@object-ui/plugin-charts^...' build` then `'@object-ui/plugin-dashboard^...' build` (dependency closures, DOWNSTREAM-prefix direction not needed here — these are the upstream closures a fresh worktree needs before its own tests) -> OK; type-check for the three packages -> `Scope: 3 of 47 workspace projects`, core / plugin-charts / plugin-dashboard each `Done` (filters matched — stated because a zero-match filter passes silently); `pnpm exec vitest run packages/core/ packages/plugin-charts/ packages/plugin-dashboard/ --maxWorkers=2` -> `Test Files 174 passed (174)` / `Tests 2524 passed (2524)`; `eslint --quiet` on all 7 changed files -> clean; `node scripts/check-changeset-presence.mjs` -> `7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)`; `pnpm run check:control-bytes` -> `OK (scanned 4215 tracked text file(s); skipped 85 binary)` plus a `grep -naP` self-scan of the changed files (no hits). Re-derived the gate set against the actual changed paths rather than trusting the dispatch list: objectui has no `scripts/pm/dispatch-gates.mjs` (that is objectstack-side), so I enumerated `check:*` from package.json — `check:phantom-deps` was implicated and ADDED to the dispatch's list, run green (`Every in-scope import is declared`); `i18n-drift`, `i18n-keys`, `spec-symbols`, `action-forward-parity`, `skills-paths` are not touched by these paths. NEW: `AdvancedChartImpl.itemSeriesClick.test.tsx` (11 cases, real DOM clicks with the #4680 rAF idiom) — the card's headline case END TO END (a real click on a pivoted multi-series bar's segment, carried through `findChartSeriesRow` to that group's own raw row: index 3, `{status:'Done', priority:'Low', est_hours:11}` — the dead click alive); all four segments each resolving to their OWN row [0,1,2,3]; one-click-one-event; the line `dot={false}` stroke and the area mark; the three additive pins; the `''` group; and #4682's key-vs-label split. Plus `DatasetWidget.drillTitleLabel.test.tsx` (7 cases) and three core cases on the new field. REVERSE VERIFICATION — FOUR legs, each direction predicted BEFORE running, all observed as predicted: (1) remove ONLY the item-handler wiring -> predicted 8 red / 3 green, observed `Tests 8 failed | 9 passed (17)`, exactly the 8 named; (2) the naive Option A where the item handler emits directly -> predicted the one-click-one-event pin red, observed `expected [ { series: 'Low' }, ...(1) ] to have a length of 1 but got 2`; (3) revert the title to `ev.series` -> predicted 2 red / 5 green, observed exactly that with the card's harm verbatim, `expected 'Backlog / [null]' to be 'Backlog / (None)'`; (4) drop `seriesLabel` from the core interface -> predicted a TYPECHECK error rather than a runtime red, observed `error TS2353` + `TS2339` at 5 sites. Each leg restored from the commit and proved byte-identical (`git diff HEAD` empty) before the next. No `dist/`-resolved dogfood ablation was involved, so no rebuild leg applies. NOTE ON LEG 1's GREEN THREE, stated because the template presumes everything goes red: the one-click-one-event and two additive pins are green on BOTH sides of that ablation by design — they guard against a regression the ablation cannot produce — which is precisely why leg 2 exists, and leg 2 is what gives the one-event pin its teeth.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #4692 (unassigned, `finding` label, no `pm:queue`): a combo chart has no drill-through at all — `ComposedChart` is the one cartesian branch that never receives `cartesianClickProps`, while every mark it renders is the same Bar/Line/Area the drillable branch uses. Searched first (keyword + file-path over open issues) and found nothing. NOT fixed in place and deliberately not folded under the adjacency exemption: the prop's own doc declares combo a no-op, so this is not a contract breach but a recorded gap, and wiring a family up would add a verification surface this card does not carry. What makes it worth a card is that the family is DERIVED — `effectiveChartFamily` resolves a chart to combo when its series declare different families, so adding `type: 'line'` to one series of a drillable bar chart silently turns that chart's drill OFF with nothing in the authored spec saying drill was touched. The same doc line is also already drifting in the other direction: it lists scatter / treemap / sankey as no-ops when all three have wired handlers."
      ]
    }

    On the #4680 pin the dispatch asked me to flip consciously — the honest result is not the shape the instruction presumed, so stating it plainly rather than reporting a flip that did not happen.

    leaves the series unresolved when the shared cursor names none did not go red, and its assertion did not need to change. Re-measured: that case drives the chart-level handler directly with a synthesized payload, which is exactly the shape of a click that reached no mark — so it now pins sub-decision 3 verbatim (empty plot area / no-mark clicks keep #4680's contract) instead of standing in for all multi-series clicks. What changed is its scope, not its verdict.

    So it was restated, not deleted: renamed to leaves the series unresolved when a click reached no mark — it does not guess one, re-documented with why it is narrower and still load-bearing, and given a seriesLabel assertion. The complementary half the dispatch wanted — "a MARK click resolves its series" — is the new file's headline case. The file's closing recharts-behaviour pin also gained a note that it is now the tripwire for the fallback arm becoming reachable again on a future recharts upgrade.


    Generated by Claude Code


    Generated by Claude Code

  13. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    CollaboratorAuthor

    ACCEPT — PR #4694 (reviewer of record; readings taken on GitHub at head 1b5365a9a, matching the report's gate-union HEAD; delivers this card AND #4682 as intended, Fixes both).

    • Shape: draft → main, lines 1–2 are the two intended closing keywords and no others. Scope (8 files) held exactly: AdvancedChartImpl + tests, core chart-series.ts only for seriesLabel + test, DatasetWidget title read + tests, one changeset across the three released packages; in-flight siblings untouched.
    • Premise A reshaped the mechanism, and the reshape is better than the ruled naive form: measured, Line/Area item handlers receive (curveProps, event) — no datum — so "item handler emits" is unimplementable for two of three mark families. The record-and-compose design (item arm RECORDS its series stamped with the shared nativeEvent; the chart-level arm is the ONE emit site) discharges all three bound sub-decisions structurally: one click one event by construction, no-mark clicks fall through to fix(charts): build the cartesian drill event from the payload recharts 3 actually sends #4680's axis contract byte-for-byte, empty plot area unchanged. Gesture-pairing on the shared DOM event kills the stale-record class a flag-and-timeout would have had — pinned directly. This is the ruling's intent delivered through the mechanism the measurement forced; publicly recorded.
    • The fix(charts): build the cartesian drill event from the payload recharts 3 actually sends #4680 pin outcome is the honest one: re-measured, that test drives the chart-level handler directly — exactly a no-mark click — so its assertion narrowed in scope without flipping. Renamed/re-documented with the complementary mark-click case added, assertion untouched. The dispatch presumed a red flip; the report says what actually happened instead. Correct.
    • The fix(core): draw a null second-dimension group instead of orphaning its measure #4681 tripwire satisfied by design: '' forwarded exactly as rendered; the core pin untouched and green.
    • A pivoted chart's drill title shows the raw series key, so an identity-keyed group's drawer reads [null] instead of its label #4682 delivered per its own constraint: seriesLabel written wherever a series resolves (item always, axis when unambiguous, pie measured and included; funnel measured — nothing to add), title reads seriesLabel ?? series, ev.series stays the lookup key. The two-field argument (label unresolvable, key unshowable) is pinned on both sides.
    • Evidence: headline case green END-TO-END (real DOM click on a pivoted segment → findChartSeriesRow → that group's own raw row — the dead click alive); 174 files / 2524 tests across the three packages; four ablation legs all predicted-exact, with leg 2 (the naive design) existing precisely to give the one-event pin its teeth — and the report names which greens are green-by-design instead of claiming a clean sweep. Gate set re-derived from the changed paths with check:phantom-deps added.
    • Out-of-scope finding: A combo chart silently has no drill-through: ComposedChart is the one cartesian branch that never receives the click props #4692 (combo charts have no drill at all, and the family is DERIVED — adding one type: 'line' series silently turns a chart's drill off; plus the same doc line drifting about scatter/treemap/sankey) — correctly searched-first, filed, not folded; graded on its own card.

    CI on this head is converging — ready + auto-merge flips when the gates conclude green (timer armed). Both cards close on merge; pm:dispatched comes off then. This also closes the last open item the target:v17 board carried for the chart-drill family.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingtarget:v17v17 发布窗口工作集(GA 前排查 2026-08-04)

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions