Skip to content

fix(plugin-dashboard): draw an authored combo as a combo on the dataset path (#4229) - #4298

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4229-combo-dataset-path
Aug 11, 2026
Merged

fix(plugin-dashboard): draw an authored combo as a combo on the dataset path (#4229)#4298
yinlianghui merged 1 commit into
mainfrom
claude/issue-4229-combo-dataset-path

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4229

Both halves of the card land here, as it asked: fixing either one alone leaves a worse state than fixing neither (a chart that is nominally a combo with no line and one axis, or a forwarded series array colliding with the dataset derivation).

The premise, re-verified at origin/main (ee66e2ebf)

Both halves still hold, unchanged:

  1. CHART_TYPE_MAP had no combo entry, so CHART_TYPE_MAP[widgetType] ?? 'bar' sent every combo widget to the bar family.
  2. chartConfigPresentation forwarded no series / xAxis / yAxis at all, under a comment stating they are "DERIVED from the dataset selection".

The ruling this implements

Quoted verbatim from the claim comment on #4229 (#4229#issuecomment-5252539356):

数据归 dataset,呈现归作者(merge-forward)

  1. "derived from the dataset selection" 不变量只正确地覆盖数据形状(哪些字段成为 series、行与桶),不覆盖呈现属性。per-series typeseries[].yAxis: 'left'|'right'yAxis[] 轴定义是作者意图,ObjectChart:兑现 ChartConfig 作者形状(type / xAxis.field / yAxis[].field / series[].name)与轴表现属性(framework#3701 对齐) #2880 已裁 ObjectChart 尊重它们;dataset 路径从未接续,正是本缺陷。
  2. 执行形状:dataset 路径继续推导 series 成员列表;作者 chartConfig 中按 name/key 匹配到推导 series 的呈现属性合并其上(显式绑定优先,ObjectChart:兑现 ChartConfig 作者形状(type / xAxis.field / yAxis[].field / series[].name)与轴表现属性(framework#3701 对齐) #2880 S2 延伸到 dataset 路径)。作者条目点名 dataset 选择之外的度量 ⇒ 忽略(成员归 dataset);推导出而作者未配的 series ⇒ 保持家族默认。

This is #2880's S2 — dual axes as yAxis[].position plus series[].yAxis, explicit binding taking priority over the implicit per-series-type guess — which PR #2883 landed in ObjectChart and which the dataset path never carried over.

Where the split falls

The line runs through the two binding keys, not around whole objects:

OwnerKeys
Datadataset (derived, never forwarded)series membership; ChartSeries.name; ChartAxis.field; xAxisKey
Presentationauthor (merged forward)series[].type, series[].yAxis, label, color, stack, variant, dashArray, opacity; axis title / format / min / max / stepSize / showGridLines / position / logarithmic

mergeAuthoredPresentation is the one place that merge happens — a single function carrying the match-by-name rule, not a spread per attribute. An authored entry naming a measure outside the dataset selection matches nothing and is dropped with its presentation; a derived series with no authored entry keeps the family default.

Dropping ChartAxis.field on the way through is what makes forwarding the axes safe structurally rather than by a guard: it is the one key by which an authored axis could name a series, because normalizeChartSchema synthesises series out of yAxis[].field when a chart declares none. The count of axis entries still travels, since that is what declares the secondary axis.

CHART_TYPE_MAP.combo maps to combo rather than to a base family: the renderer draws it distinctly (mixed marks on a ComposedChart, both y-axes), it has been a ChartTypeSchema member since spec 17.0.0-rc.1, and widgetDispatch already resolves a combo widget to chartType: 'combo' — so the two surfaces now agree.

Two consequences worth flagging in review

  • A non-combo widget can now declare one type: 'line' series and get the combo the renderer already derived from disagreeing series types (Vocabulary consolidation (#2901): what shipped, and why Track C items 1–3 are not planned #2945). The widget's own family is unchanged; the renderer derives.
  • A compareTo overlay now inherits its own measure's mark and axis. Without this, my change would have introduced a new wrong state: once a chart becomes a combo, the overlay of a bar/left measure fell to the renderer's positional guess and drew as a line on the right axis. It inherits mark and axis only — never stack, which would add the two periods together.

Tests

New DatasetWidget.comboPresentation.test.tsx (12 cases) drives the card's exact fixture — task_count bar/left, avg_progress line/right, two yAxis entries — and asserts against the shape the renderer receives, running the emitted schema through normalizeChartSchema (the one translation layer ChartRenderer puts in front of AdvancedChartImpl), imported read-only.

Honest scope note on the DOM half. These do not count recharts marks. That needs ResponsiveContainer mocked to a measured box, and recharts resolves inside plugin-charts alone — a vi.mock('recharts') in plugin-dashboard cannot even resolve the specifier, which is the same constraint DatasetWidget.chartConfig.dom.test.tsx already records. The mark half is already pinned there against exactly this shape: AdvancedChartImpl.comboFromSeries.test.tsx draws a chartType: 'line' series as a line and binds yAxis: 'right' to the right axis, and the combo branch renders both y-axes unconditionally. The card's measured table is therefore reconciled across the two files rather than re-measured here.

Reverse verification, fix removed via git checkout origin/main -- DatasetWidget.tsx, direction predicted first: 8 red, 4 green. The 4 that stay green are exactly the controls and the membership pins — an authored series outside the selection ignored, a non-cartesian per-series type dropped, a no-chartConfig widget emitting a byte-identical derived series, a chrome-only chartConfig leaving bindings alone. Green in both states is what makes them controls: they guard the invariant rather than testing the new code.

× resolves the `combo` family instead of falling through to bar
× merges the authored per-series mark onto the derived series
× declares two y-axes and binds each series to the authored one
× leaves a derived series with no authored entry untouched
× does not let an authored axis `field` reach the renderer
× keeps an axis slot that declared only its `field`
× lets a bar widget declare one line series (a derived combo)
× gives each overlay the mark and axis of the series it overlays
Tests 8 failed | 4 passed (12)

DatasetWidget.chartConfig.test.tsx pinned the old belief and is updated: its refusal section now pins the data half (ChartAxis.field, ChartSeries.name) as hard as it pinned the whole keys before. type and aria stay refused, untouched.

Green locally: plugin-dashboard 36 files / 313 tests (including #4273's relabelDimensions/displayRows and #4275's pivotDimensionValue regions, which this diff does not touch), plugin-charts 22 files / 170 tests, root type-check 78/78 tasks, type-check:coverage, type-check:vitest-setup, check:control-bytes, check-changeset-presence, check-changeset-no-major, and eslint on the changed files (0 errors).

Out of scope

The kpi / gauge / solid-gauge / bullet caveat was measured, not fixed, per ruling item 4 — filed as #4295. The answer: one shape, and deliberate, not #4229's shape. All four are routed to the single-value path by three coordinated places with no per-type differentiation anywhere, and the spec's own ChartTypeSchema calls them "honest single-value variants pending a real dial/target renderer". The gap is one missing capability (no dial / arc / bullet-target renderer), not four defects.


Generated by Claude Code

…et path (#4229)
A widget authoring the spec's combo shape — `series[].type` plus
`series[].yAxis` and two `yAxis` entries — rendered as two bar series on one
shared axis (measured: 2 bars / 0 lines / 1 axis where 1 bar + 1 line + 2 axes
were authored). Two halves, both here, and fixing either alone is worse than
neither: `CHART_TYPE_MAP` had no `combo` entry so the widget fell through its
`?? 'bar'` default, and `chartConfigPresentation` forwarded no `series` /
`xAxis` / `yAxis` at all.
The ruled split: the dataset owns DATA (series membership, and the column each
binding reads — `ChartSeries.name`, `ChartAxis.field`), the author owns
PRESENTATION (per-series mark and axis binding, label/colour/stack, axis
scale and chrome). Presentation merges onto the derived bindings by name/key
with the explicit binding winning — one merge function, not a spread per
attribute. This is #2880's S2 rule, landed in ObjectChart by #2883 and never
carried to the dataset path.
Dropping `ChartAxis.field` on the way through is what makes forwarding the axes
safe structurally: it is the one key by which an authored axis could name a
series, since the renderer synthesises series from `yAxis[].field` when a chart
declares none.
Also: a `compareTo` overlay now inherits its own measure's mark and axis, so a
bar-on-the-left measure's comparison no longer draws as a line on the right
once the chart becomes a combo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 12:01pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)29.4 KB350 KB
Entry fileindex-BwCPcgO5.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.27KB3.23KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)488.62KB108.26KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)150.04KB39.79KB
fields (index.js)228.45KB56.62KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)16.38KB5.47KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.73KB17.54KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)121.07KB31.39KB
plugin-designer (index.js)210.91KB42.67KB
plugin-detail (index.js)238.98KB59.76KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)109.18KB26.48KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.60KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

combo dashboard widgets render as grouped bars — DatasetWidget has no combo family and refuses to forward series/xAxis/yAxis on the dataset path

2 participants

@yinlianghui@claude