Skip to content

fix(charts,core,dashboard): resolve a cartesian drill from the clicked mark, and title it with the series label - #4694

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4672-item-level-series
Aug 15, 2026
Merged

fix(charts,core,dashboard): resolve a cartesian drill from the clicked mark, and title it with the series label#4694
yinlianghui merged 1 commit into
mainfrom
claude/issue-4672-item-level-series

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4672
Fixes #4682

Delivers the ruled Option A on #4672 (delegated ruling, comment 5301329590, veto window unvetoed) with its three sub-decisions bound, and folds in #4682 as the same PR's title half.

The dead pivoted drill (#4672)

PR #4680 fixed what a cartesian click can read out of recharts 3's MouseHandlerDataParam, and measured the wall it could not get past: a chart-level cartesian click is an axis interaction, and recharts dispatches those with activeDataKey hard-coded undefined — the shared cursor spans every series at that tick, so the payload names no single one. A pivoted dataset chart (2 dimensions, 1 measure — the shape ADR-0021 introduced) needs the series to resolve its drill row, so every segment of every such dashboard chart stayed a dead click.

The mark knows. This renderer draws the Bar / Line / Area, so an item-level onClick closes over the very dataKey it was rendered with — statically known, not inferred from tooltip state.

Premises measured first (section 2 of the dispatch)

measurement, on the installed recharts 3.10.1, via real DOM clicks
A Bar's item handler is (item, index, event) and the item carries the row and value. Line / Area are not typed for onClick at all, and measured they receive (curveProps, event)two args, the first being the rendered curve's props, not a datum.
B Both handlers fire for one gesture, item first, chart second, and both are handed the same nativeEvent object.
C A dot={false} line renders .recharts-line-curve and nothing else, so the stroke is its only mark; the item handler fires on it.
D The funnel path sends no series at all — nothing to add. The pie path sends only the key, so it gained seriesLabel too.

What premise A forced

Because line and area item handlers get no datum, an item handler cannot compose the event on its own — it knows its series and nothing about which category. So the item handler does not emit. It records its series, stamped with the gesture, and the chart-level handler (which does know the category, the bucket identity and the row) composes the one event.

That single choice discharges all three bound sub-decisions:

  1. Double-fire — one click yields one onChartClick because there is exactly one emit site, rather than a second event suppressed after the fact. Pinned by a test that goes red against the naive design (item emits directly): expected [ ... ] to have a length of 1 but got 2.
  2. Additive only — a click landing on no mark records nothing and falls through to fix(charts): build the cartesian drill event from the payload recharts 3 actually sends #4680's axis contract byte for byte: category + categoryId + the series only where one series is plotted. A line's dot={false} stroke simply gains the exact series where the stroke is hit; nothing that resolved before stops resolving.
  3. Empty plot areafix(charts): build the cartesian drill event from the payload recharts 3 actually sends #4680's contract verbatim: category-only, series unresolved. No "drill the whole category" semantics invented.

Pairing on the shared DOM event rather than on a flag-and-timeout also means a record left by one gesture can never be adopted by a later click — pinned directly.

The '' group (the #4681 tripwire)

The clicked key is forwarded exactly as rendered, '' included. The empty-string second-dimension group draws its own bar since #4673 and '' is falsy, so a truthiness test on the way out would send no series at all and leave that bar's drill standing on the reader's String(seriesKey ?? '') coercion instead of on what was clicked. Satisfied by design, not by editing the core pin — chart-series.nullCategory.test.ts's reads an absent series key as the empty-string group when one exists is untouched and green.

The opaque drill title (#4682)

ChartSegmentClickEvent gains seriesLabel, written wherever a handler resolves a series (item-level always, axis-level when unambiguous, pie too), and DatasetWidget.handleChartDrill titles the drawer from seriesLabel ?? series.

ev.series stays the lookup keyfindChartSeriesRow resolves it through the same assignment buildChartSeries made — and only the title reads the label, exactly as the card requires.

Neither string can do the other's job, which is why this is a second field rather than a change to the first: the label is not resolvable (it is precisely what the colliding groups share — pinned in core: resolving by the shared label returns -1) and the key is not showable.

The #4680 pin, flipped consciously

leaves the series unresolved when the shared cursor names none was written when no cartesian click could resolve a series, and it stood in for all of them. Re-measured rather than assumed: it drives the chart-level handler directly, which is exactly the shape of a click that reached no mark — so its assertion is unchanged and still green, and what it pins is now narrower and still load-bearing (sub-decision 3 verbatim). It is renamed and re-documented to say so, with the complementary mark-click case added in the new file. The pin was not deleted and its assertion did not need to move; the honest result is a scope change, not a red flip.

Tests

New: AdvancedChartImpl.itemSeriesClick.test.tsx (11 cases, real DOM clicks) — the headline case resolved end to end (a real click on a pivoted segment, through findChartSeriesRow, to that group's row); every segment to its own group; one-click-one-event; line stroke and area mark; the three additive pins; the '' group; and #4682's key-vs-label split. Plus the #4682 consumer tests in DatasetWidget.drillTitleLabel.test.tsx and the field's own core tests.

Reverse verification — four legs, each direction predicted before running, all observed as predicted:

leg predicted observed
remove only the item-handler wiring 8 red / 3 green (the additive + one-event pins green by design — the ablation cannot make them fail) Tests 8 failed | 9 passed (17) — exactly the 8 named
naive design (item emits directly) one-click-one-event red red: to have a length of 1 but got 2
title reverts to ev.series 2 red / 5 green red with the card's harm verbatim: expected 'Backlog / [null]' to be 'Backlog / (None)'
drop seriesLabel from core a typecheck error, not a runtime red error TS2353/TS2339 at 5 sites

Each leg restored byte-identically from the commit (git diff HEAD empty).

Gate union — all at 1b5365a9a, the final commit, tree clean

  • pnpm --filter '@object-ui/plugin-charts^...' build and '@object-ui/plugin-dashboard^...' build — dependency closures, first in the fresh worktree
  • type-check for core / plugin-charts / plugin-dashboard — Scope: 3 of 47 workspace projects, all Done (filters matched; no silent zero-match)
  • pnpm exec vitest run packages/core/ packages/plugin-charts/ packages/plugin-dashboard/Test Files 174 passed (174) / Tests 2524 passed (2524)
  • eslint --quiet on all 7 changed files — clean
  • node scripts/check-changeset-presence.mjs7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)
  • pnpm run check:control-bytesOK (scanned 4215 tracked text file(s)), plus a grep -naP self-scan of the changed files (no hits)
  • re-derived against the actual changed paths: check:phantom-depsEvery in-scope import is declared. The remaining gates (i18n-*, spec-symbols, action-forward-parity, skills-paths) are not implicated by these paths.

Scope

AdvancedChartImpl.tsx + tests; chart-series.ts only for the seriesLabel field + its test; DatasetWidget.tsx's title read + tests; one changeset. The in-flight siblings' files (plugin-grid, packages/types/field-types.ts) are untouched. Combo charts were left alone deliberately — ComposedChart is the one cartesian branch that never receives the click props, which is declared behavior and a different defect class; recorded as #4692, unassigned.


Generated by Claude Code

…d mark, and title it with the series label

objectui#4672's ruled half and objectui#4682.

A chart-level cartesian click is an AXIS interaction, and recharts 3 dispatches
those with `activeDataKey` hard-coded undefined — the shared cursor spans every
series at that tick. A pivoted chart's drill lookup requires the series, so
every segment of every pivoted dashboard chart was a dead click.

The mark knows. This renderer draws the Bar / Line / Area, so an item-level
onClick closes over the dataKey it was rendered with. Both handlers fire for one
gesture (item first, chart second, sharing one nativeEvent), so the item handler
records its series stamped with that gesture and the chart-level handler emits
the single event: one click, one drill event, and a click landing on no mark
falls through to objectui#4680's axis contract byte for byte.

The clicked key is forwarded exactly as rendered, '' included — the empty-string
second-dimension group draws its own bar since objectui#4673 and '' is falsy.

ChartSegmentClickEvent gains `seriesLabel`, and the drill drawer titles itself
from `seriesLabel ?? series`. `ev.series` stays the LOOKUP key; only the title
reads the label, so an identity-keyed group stops announcing `Backlog / [null]`
over a segment the user saw labelled `(None)`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnQd8iMMUwXQEV1crFmQiQ
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 15, 2026 9:41am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-16qNSrGU.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.91KB 108.67KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 63.32KB 18.06KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 121.90KB 31.76KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.72KB 27.70KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 192.10KB 51.24KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 111.82KB 27.23KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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