Skip to content

feat(charts): combo 图从 mark 下钻,axis/surface 保持静默 (#4692) - #4870

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4692-combo-mark-clicks
Aug 16, 2026
Merged

feat(charts): combo 图从 mark 下钻,axis/surface 保持静默 (#4692)#4870
yinlianghui merged 1 commit into
mainfrom
claude/issue-4692-combo-mark-clicks

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4692

按 2026-08-16 维护者裁定(评论 5306033961,方案 B)实施本卡余下的实施半 —— doc-truth 半已由 PR #4705 落地。

机制

AdvancedChartImpl 只把 cartesianClickProps 施加在最后一个 cartesian ChartComponent 上;combo 分支从它自己的 ComposedChart 提前 return,渲染时只带 data,不带任何点击 props。于是 combo 图的 onChartClick 永不触发 —— mark 不触发,axis 也不触发,尽管它画的正是可下钻分支同样的 Bar / Line / Area

值得修而不是只记录的原因是图族是派生的:effectiveChartFamily 在各 series 声明的族不一致时把图解析成 combo(#2945)。给一张可下钻柱图的某个 series 加上 type: 'line',整张图的下钻就静默失效 —— 作者写的 spec 里没有一个字提到下钻被动过,也没有任何报错。

改动

combo 的 Bar / Line / Area mark 现在发 { category, categoryId, series, value },语义与平面 cartesian 分支完全一致,复用 #4672 / #4682 的 item 级 series-identity 机制:mark handler 记录自己被渲染时的 series,chart 级 handler 组装并发出唯一一个事件,所以一次手势仍然只产生一次 onChartClick

两个分支共用同一个组装器 emitCartesianClick,只在一条规则上分岔 —— requireMark:

  • 平面分支照旧对每一次点击发事件,落空时回退到 axis 级答案;
  • combo 在手势落在 mark 上时发。

axis / surface 点击保持静默(裁定 guard 1):一张 combo 同时画多个 measure,surface 点击没有单一 series 可报,回退答案只能靠猜 —— 与 pivoted 案裁定同理。combo 也因此不带全图 pointer 光标,affordance 落在真正会应答的 mark 上。

⛔ 未做 Option C 的 designer hint(裁定 guard 2,另卡另裁)。

钉的翻转

AdvancedChartImpl.comboClickNoop.test.tsx 是 PR #4705 钉住 combo 静默的文件,本改动按裁定 guard 3 就地翻转它,并随内容改名为 AdvancedChartImpl.comboMarkClick.test.tsx(文件名里的 "Noop" 已与断言相反;git 记录为 rename,仓内无任何引用)。9 个用例:

  • 5 个发射钉:显式 combo 的 bar mark、line mark、一次手势一个事件;派生 combo 的端到端下钻(经 findChartSeriesRow 落到具体行)与其 line series;
  • 2 个 axis / surface 负钉(守 guard 1),显式与派生各一;
  • 2 个正控制:平面柱图的 mark 点击照常发(PR docs(charts): correct onChartClick wiring doc, pin combo's no-click state #4705 留下的那个),以及同样形状的 surface 点击在平面柱图上确实会发 —— 没有这一条,上面两个负钉会因为「什么都没产生」而空绿。

反向验证(双臂,方向先判后跑)

A 臂 —— 摘掉 combo 的点击接线:预判 5 个发射钉变红且指名,2 个负钉保持绿(空绿:未接线的 combo 本来就处处静默,这两个钉对「接没接线」并不敏感),控制保持绿。实测完全一致:

× a bar mark emits { category, series, value } — was silent before the ruling
× a line mark emits ITS OWN series, not the bar it shares the plot with
× emits exactly ONE event per gesture — item records, chart emits
× resolves the drill end to end after ONE series changed its mark
× the line series that DERIVED the combo is itself drillable
AssertionError: expected [] to have a length of 1 but got +0
Tests  5 failed | 4 passed (9)

B 臂 —— 把 combo 的 surface 过度接线(comboClickProps 换成 cartesianClickProps,即 guard 1 禁止的那种「顺手改进」):预判反过来 —— 2 个负钉变红,发射钉全绿。实测一致,且红的内容是真事件而非空数组,证明负钉不是空绿:

× NEGATIVE PIN (guard 1): a click on the surface / axis stays silent
  AssertionError: expected [ { category: 'Jan', …(4) } ] to have a length of +0 but got 1
× NEGATIVE PIN (guard 1): the derived combo’s surface is silent too
  AssertionError: expected [ { category: 'Open', …(4) } ] to have a length of +0 but got 1
Tests  2 failed | 7 passed (9)

A 臂单独看不足以证明 guard 1 被守住(负钉在那一臂里是空绿),B 臂才是守 guard 1 的那一臂 —— 两臂合起来才覆盖裁定的两个方向。

验证

同步更新的 doc comment

PR #4705onChartClick 的注释改成「combo 是 no-op」,本改动使其失真,已按裁定 guard 3 一并更新:现在写明 combo 已接线、唯一的刻意例外是 axis / surface 静默、以及「派生不再吃掉一个交互」这层因果;radar 是 L1 里剩下的唯一 no-op。


Generated by Claude Code

`AdvancedChartImpl`'s combo branch returned from its own `ComposedChart`
before `cartesianClickProps` was applied to anything, so a combo chart
fired `onChartClick` never — not on a mark, not on the axis — even though
its marks are the same `Bar`/`Line`/`Area` components the drillable
cartesian branch renders.

The family is derived, not only authored: `effectiveChartFamily` resolves
a chart to `combo` whenever its series declare different families, so
adding `type: 'line'` to one series of a drillable bar chart silently
turned that chart's drill off, with nothing in the authored spec saying
drill was touched.

Combo's marks now emit `{ category, categoryId, series, value }` with the
same semantics the plain cartesian branch gives, reusing the item-level
series-identity machinery from #4672/#4682 — the mark records its series,
the chart-level handler composes the one event.

Only the marks drill: a click on a combo's surface or axis stays silent,
because a plot carrying several measures has no single series to report
there. Shared with the plain branch through one composer that differs on
that rule alone.

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-BL7Xfwx4.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) 496.22KB 110.42KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 157.05KB 43.28KB
fields (index.js) 231.73KB 57.60KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
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.86KB 10.83KB
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) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
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.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.14KB 32.58KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 239.81KB 59.97KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 115.34KB 27.74KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 192.23KB 51.30KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.23KB 26.97KB
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.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
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

Copy link
Copy Markdown
Collaborator Author

【PM 验收 · ACCEPT】objectui 分片 PM(session_01GTRjn8xBqp75dk7kFupVRt)对 #4692 裁定(方案 B,评论 5306033961)实施半的验收:

实物核验:4 文件对账相符(AdvancedChartImpl +84、钉子文件 git rename 翻转 3→9 例、changeset patch);零越包、模型标识 0、releases 0。三条 scope guards 逐条兑现:①axis/surface 静默 —— requireMark 一条规则分岔,平面分支行为逐字不变;②未做 designer hint;③钉翻转保正控制 + 新增 axis/surface 负钉,doc comment 同步更新。显式与派生 combo 双形态覆盖,派生形态的端到端下钻读数(经 findChartSeriesRow 落到 PIVOT_RAW 行)正中本卡 AI-author 静默陷阱。

反向验证(双臂,方法论示范级):A 臂摘接线证 5 发射钉活(且诚实记录负钉在此臂是空绿);B 臂用 guard 1 明令禁止的「顺手改进」当变异(cartesianClickProps 直换),2 负钉红且红文是真事件 —— 单跑任何一臂都覆盖不了裁定的两个方向,B 臂正是负钉不空绿的证明。CI 亲读:19 项全 completed(17 success + 2 skipped),零失败。测试 rename(Noop→MarkClick)名实相符、仓内零引用,核准;mark 级 pointer 光标(不上全图)与 guard 1 自洽,取舍已记录。零 finding。

处置:undraft + auto-merge(SQUASH)。本卡两半(PR4705 doc-truth + 本 PR 实施)至此全部落地。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 16, 2026 20:01
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit f95434b Aug 16, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4692-combo-mark-clicks branch August 16, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A combo chart silently has no drill-through: ComposedChart is the one cartesian branch that never receives the click props

2 participants