Skip to content

feat(plugin-dashboard): forward the chartConfig presentation keys the chart block delivers - #3934

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os7016-chartconfig-forward
Aug 9, 2026
Merged

feat(plugin-dashboard): forward the chartConfig presentation keys the chart block delivers#3934
yinlianghui merged 1 commit into
mainfrom
claude/issue-os7016-chartconfig-forward

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#5175

Fixes objectstack-ai/objectstack#7016

背景

DashboardWidgetSchema.chartConfig 声明的是完整的 spec ChartConfigSchema(14 个键),但 ADR-0021 dataset 路径只把其中一个键落到渲染器上 —— showLegend(objectui#3135)。其余的键(title / subtitle / description / height / colors / showDataLabels / annotations / interaction)解析为合法元数据、抵达 DatasetWidget、在构造 chart schema 之前被丢掉。底层 chart 块本身全都兑现这些键 —— 惰性纯粹出在 dashboard 这一次转发上。

行号漂移如实报告:issue 正文基于 @91757a7 写的 :595-605 / :632,在本 PR 的 base(c2fd1223a)上实际是 DatasetWidget.tsx:987-997(取键)与 :1024(构造 schema)。

转发判据(两条同时满足才转发)

  1. 底层 chart 块在 dashboard 路径端到端兑现该键。 { type: 'chart' } 经 ComponentRegistry 解析到 ChartRendererAdvancedChartImpl。每一个转发键都有 DOM 级实测钉,⛔ 绝不转发到一个会忽略它的 prop(那只是把 declared 不等于 enforced 下沉一层)。
  2. 不与 dataset 推导冲突。 xAxis / yAxis / series 由 dataset selection 推导(buildChartSeries),作者写的会遮蔽推导出的绑定并把图画白,一律不转发。

逐键判定表

判定 依据(代码位置 / 实测证据)
showLegend 转发(既有,#3135) 本 PR 不改行为;DatasetWidget.showLegend.test.tsx 原有钉不动
title 转发 AdvancedChartImpl.tsx:233-241 ChartFrame 绘制;实测:dashboard 元数据写 title 后 DOM 出现该文本,且 chart 容器不再是 widget 根的直接子节点(frame 包了一层)
subtitle 转发 同上,ChartFrame 的第二行;实测同一条测试
description 转发 AdvancedChartImpl.tsx:299 → 容器 role="img" + aria-label;实测:[data-slot="chart"]roleimgaria-label 为写入值;未声明时两者都为 null(不给无名图形盖 role="img")
height 转发 AdvancedChartImpl.tsx:296-298 → 容器内联 style.height;实测:写 height: 420style.height === '420px',未声明时为空且保留 h-[350px] 默认类
colors(string[]) 转发colors AdvancedChartImpl.tsx:430-432 getPalette();实测:pie 的 path.recharts-sector fill 依次等于调色板
colors({value: color}) 转发categoryColors ChartRenderer 透传 categoryColors,渲染层按分类优先查表;实测:sector fill 取作者 map 的色而非调色板色。两臂经不同 prop 到达渲染器,拆分与 react tier 的 ObjectChart.tsx:769-782 完全一致,作者 map 合并在维度选项色之上(spec 字段注释规定的优先级)
showDataLabels 转发 AdvancedChartImpl.tsx:580-584 LabelList;实测:.recharts-label-list text 依次为 120 / 80;关掉或未声明时为空数组(负向断言前先 await 出图,避免"因为什么都没画所以绿")
annotations 转发 AdvancedChartImpl.tsx:530-568 ReferenceLine / ReferenceArea;实测:line 标注画出 .recharts-reference-lineTarget 标签可见,region 标注画出 .recharts-reference-area;空数组不下发死键
interaction 转发 AdvancedChartImpl.tsx:570-578,tooltips / brush 两个键都被读;实测:brush: true 画出 .recharts-brush,tooltips: false.recharts-tooltip-wrapper 从有到无(带"开"的对照臂)
type 不转发 判据 2:dataset 路径已有 chartType 通道(CHART_TYPE_MAP,DatasetWidget.tsx:944)。负向钉:pie widget 写 chartConfig.type: 'line',chartType 仍是 pie,specType 不出现
xAxis / yAxis 不转发 判据 2:轴由 dataset selection 推导。负向钉:写了之后 schema 上没有 xAxis / yAxis,xAxisKey 仍是推导出的维度
series 不转发 判据 2:一 measure 一 series 由 buildChartSeries 推导。负向钉:写 series: [{ name: …, stack: 'g' }] 后 schema 的 series 仍是推导出的 dataKey 形状、stack 不出现
aria 不转发 —— 收窄候选 判据 1 不成立:AdvancedChartImpl 没有 aria prop,SchemaRenderer 的 ARIA 注入读的是扁平ariaLabel / ariaDescribedBy / role(packages/react/src/SchemaRenderer.tsx:107-118),不读嵌套 aria 对象。实测双向钉:直接把 aria 交给 ChartRenderer 也不改任何属性;dashboard 侧写了同样一个属性都不出现。见下

收窄候选清单(交回 #5175 的收窄半边)

  • aria —— ChartConfigSchema 声明、chart 路径上无人读。存在一个"把 aria.ariaLabel 摊平成 ariaLabel 让 SchemaRenderer 读到"的翻译写法,本 PR 故意不做:(a) react tier 的 ObjectChart 不做这个翻译,dashboard 单独做会造出一个只此一处的方言;(b) description 已经在 chart 容器上占了可访问名(role="img" + aria-label),摊平后的 ariaLabel 会落在 SchemaRenderer 的外层包裹元素上,产生两个嵌套的可访问名 —— 那是 a11y 缺陷不是能力;(c) AriaProps.role 在 chart 上无处安放。到底是给它一个读者还是从 ChartConfigSchema 收窄掉,是契约裁决,归 #5175。
  • xAxis / yAxis / series —— 在一个推导这三者的 surface 上被声明。本 PR 把"写了仍被忽略"钉成现状契约,不代表它应该长期这样声明。

测试

  • packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx —— 判定表本身,在 schema 接缝上(stub 掉 SchemaRenderer,与 .showLegend / .animation 同一个缝)。正向:每个转发键;负向:xAxis / yAxis / series / type / aria 写了仍被忽略;边界:height: 0 / 负数被丢、空 annotations 不下发、未声明 chartConfig 时十个键一个都不出现。
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.dom.test.tsx —— 真链路端到端 DOM,不 stub 任何渲染器:DatasetWidgetSchemaRenderer → registry 的 chartAdvancedChartImpl。覆盖在 Recharts ResponsiveContainer 之外绘制的键(标题 / 副标题 / 可访问名 / 高度)。
  • packages/plugin-charts/src/ChartRenderer.dashboardChartConfig.test.tsx —— plot 内部那一半。marks(bar / LabelList / ReferenceLine / Brush)需要一个被测量到的盒子,ResponsiveContainer 在 headless DOM 里报 0x0、不渲染子节点;而 recharts 只在 plugin-charts 里解析得到,所以把它 mock 成固定尺寸的那半证据只能落在这个包。测试驱动的是 ChartRenderer(registry 对 type: 'chart' 解析到的那个组件),schema 与 DatasetWidget 实际下发的形状逐字节一致。

为什么拆两处而不是一处:在 dashboard 那个 harness 里断言"没有 brush / 没有 reference line"会因为什么都没画而通过 —— 那是假绿。所以 plot 内部的键一律在能真的画出来的地方钉,dashboard 侧只钉容器外确实渲染的元素(aria 的负向钉留在 dashboard 侧是成立的:两个可能承载可访问名的元素 —— chart 容器与 SchemaRenderer 包裹层 —— 都在 ResponsiveContainer 之外,都确实渲染)。

命令与结果

pnpm exec vitest run packages/plugin-dashboard/ --maxWorkers=2
  Test Files  29 passed (29)
        Tests  262 passed (262)

pnpm exec vitest run packages/plugin-charts/ --maxWorkers=2
  Test Files  19 passed (19)
        Tests  152 passed (152)

pnpm exec turbo run type-check --concurrency=2
  Tasks:    78 successful, 78 total

反向验证(方向先判后跑)

先判的方向:把转发那段还原成 base 的 showLegend-only 实现后,正向钉全红、拒收钉全绿(它们断言的是"键不出现",老代码同样满足),plugin-charts 那个文件整体全绿(它直接驱动 ChartRenderer,根本不经过 DatasetWidget)。

实跑与预判一致:

DatasetWidget.chartConfig.test.tsx      (14 tests | 6 failed)
DatasetWidget.chartConfig.dom.test.tsx  (7 tests | 3 failed)
ChartRenderer.dashboardChartConfig.test.tsx  11 passed
  Test Files  2 failed | 1 passed (3)
        Tests  9 failed | 23 passed (32)

红的 9 条正是 9 条正向钉;拒收钉与 showLegend(那是 #3135 的,不是本 PR 的)照常绿。

changeset

.changeset/dataset-widget-chartconfig-presentation-os7016.md,@object-ui/plugin-dashboard: patch,写明「dashboard 元数据的 chartConfig 呈现键首次生效」以及行为开启面:存量 dashboard 写过这些键的,从无效变有效 —— 写过 chartConfig.title 的会在图内多出一行标题(widget 卡片自己的 title 是另一个键),写过 height 的不再填满卡片,写过 colors 的不再用主题调色板,showDataLabels / annotations / interaction.brush 开始绘制。没写 chartConfig、或只写了 showLegend 的 widget 渲染与之前逐字节一致(未声明的键一律不下发,渲染器自己的默认值继续说话)。

附带说明

  • packages/plugin-dashboard/package.json 加了一条 devDependency @object-ui/plugin-charts(workspace):端到端 DOM 测试需要它的注册副作用,声明出来而不是靠 vitest alias 隐式解析。无环 —— plugin-charts 不依赖 plugin-dashboard。
  • ⛔ 未碰 packages/spec(收窄半边归 spec 车道),⛔ 未碰 DashboardRenderer 的内联路径。

Generated by Claude Code

… chart block delivers

`DashboardWidgetSchema.chartConfig` is declared as the full spec
`ChartConfigSchema`, but the ADR-0021 dataset path lowered exactly one key onto
the chart renderer — `showLegend` (#3135). `title`, `subtitle`, `description`,
`height`, `colors`, `showDataLabels`, `annotations` and `interaction` parsed as
valid metadata, reached DatasetWidget, and were dropped before the chart schema
was built, even though the underlying chart block draws every one of them.

DatasetWidget now lowers them, on two mechanical criteria that both have to
hold: the chart block draws the key end to end on this path (each is pinned at
the DOM level, so nothing is forwarded to a prop that ignores it), and the key
does not fight the dataset derivation. `xAxis`/`yAxis`/`series` therefore stay
out (derived from the dataset selection; an authored one would shadow the
derived binding), `type` stays out (the widget's own type picks the family via
CHART_TYPE_MAP), and `aria` stays out because nothing on this path reads it —
AdvancedChartImpl has no `aria` prop and SchemaRenderer's ARIA injection reads
the flat ariaLabel/ariaDescribedBy/role.

`colors` is split the way the react tier splits it: a string[] is the positional
palette, a {value: color} record is a per-category map merged over the category
dimension's own option colours.

Part of objectstack-ai/objectstack#5175
Fixes objectstack-ai/objectstack#7016

Co-Authored-By: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 9, 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 9, 2026 9:47am

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-KDeOrHmy.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
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) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
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) 4.91KB 0.87KB
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) 482.64KB 106.39KB
core (index.js) 3.00KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 231.13KB 56.80KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
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.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.02KB 30.47KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.63KB 59.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 109.67KB 26.55KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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) 20.15KB 6.72KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
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 (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) 2.71KB 1.34KB
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

验收通过(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#7016(#5175 enforce 半边,v17 尾批)

实物核验:base c2fd1223a → head b7502b872,单提交 7 文件;chartConfigPresentation() 白名单实现(非 spread)与判据注释逐条对应。CI 亲读终态:20 检查全 completed、0 失败。

验收要点:

  • 逐键判定表采信(14 键逐条带证据):8 键转发(showLegend 既有 + title/subtitle/description/height/colors/showDataLabels/annotations/interaction),5 键判据 2 拒转(type/xAxis/yAxis/series)且各有负向钉,aria 判据 1 实测不成立拒转 —— 与裁定「预期入围」不同的实测结论,论证三点(react tier 无此翻译、与 description 的可访问名冲突、role 无处安放)成立,采纳。该结论与 xAxis/yAxis/series 一并构成 #5175 收窄半边的裁决输入,已回传。
  • colors 双臂拆分(string[] → 调色板 / record → categoryColors,作者 map 压在维度选项色之上)与 react tier 及 spec 注释的优先级一致。
  • 测试三文件分层的理由(ResponsiveContainer 在 headless 下 0x0 假绿、recharts 仅在 plugin-charts 可解析)扎实,所有负向断言先 await 出图防空转绿。
  • 反向验证 9 正向钉全红、拒收钉与直驱文件全绿,方向先判后跑吻合。
  • finding objectstack#7026(min-size 兜底被 {...props} 覆盖成死代码,但当前顺序恰好让小显式高度生效 —— 优先级未定故标 finding)立单规范,归后续分诊。

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 09:56
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 230ffd8 Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os7016-chartconfig-forward branch August 9, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants