Skip to content

fix(plugin-report): 报表内嵌图表分桶 null 类目并转发作者声明的图表 chrome (#4877, #4878) - #4908

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4877-4878-report-chart
Aug 17, 2026
Merged

fix(plugin-report): 报表内嵌图表分桶 null 类目并转发作者声明的图表 chrome (#4877, #4878)#4908
yinlianghui merged 2 commits into
mainfrom
claude/issue-4877-4878-report-chart

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4878
Fixes #4877

数据集报表的内嵌图表,是唯一一个两半都没接上的图表面:它只转发六个键 —— chartTypedataheightisAnimationActiveseriesxAxisKey。两卡各修一半,同一条转发线,故合单。


#4878 — DATA 半:改走 buildChartSeries,继承整个 null 家族

前提复核(main 现状与卡述一致):DatasetReportChartrelabelDimensions(state.rows, dimensionLabels) 直通渲染器,链路上没有任何分桶。这正是 #4466 实测「一个 mark 都不画」的输入 —— 代价不是空图,而是静悄悄画错:主导分组消失,而 y 轴刻度仍按它撑开,图看上去完全合法。

仪表盘与图表视图从来没有这个缺陷,因为它们走 buildChartSeries(@object-ui/core),null 家族的修复全在那里。现在报表也走它,于是这些性质是继承来的,而不是在第三个面上重新派生:

性质 出处
null 桶本身 #4466
桶标签取自调用侧 i18n bundle #4500
桶 IDENTITY 与桶标签分离(库里存的 (None) 仍是另一个分组) #4508

承重约束①(#4020 三级显示名必须压过派生的 fields[].label) —— 已保住。buildChartSeries 赋的 labelfields[].label ?? name;#4020 的解析更高一级(作者 chart.series[].labelheaderLabel → measure 名),所以落点处显式覆盖,并钉住了 { en, 'zh-CN' } 记录形式:core 里没有 provider,它自己的取法是「第一个字符串胜出」,那正是 #4020 关掉的缺陷类。

承重约束②(null 桶标签走 i18n bundle) —— tt('chart.nullCategory', '(None)') 在渲染器里解析后作为 nullCategoryLabel 传入,与仪表盘 #4500 的分工一致(core 无 React,拿不到 bundle)。zh 控制台读 (未指定)

量到的转发键前后对照(DATA 半)

之前 之后
data relabelDimensions(rows, …) 直通,null 类目原样到达 buildChartSeries(...).data,null 归入本地化桶
xAxisKey xAxis buildChartSeries(...).xAxisKey(同值,同一处派生)
series 手搓 [{ dataKey, label }] 派生所得,再叠加作者表现键与 #4020 标签

#4877 — PRESENTATION 半:声明即生效

ReportChartSchema 声明、而这条路径全部丢弃的键,现在按 #4229 的 data/presentation 分界下放。

showLegend 是其中最锋利的一个:它不是「被忽略」,而是效果被反转 —— AdvancedChartImpl 算的是 legendVisible = showLegend !== false,键缺席即图例开,于是作者写的 false 照样画出图例。

量到的转发键前后对照(PRESENTATION 半)

之前 之后
showLegend 丢弃(且效果反转) 转发
showDataLabels 丢弃 转发
colors(数组 → 位置调色板) 丢弃 转发为 colors
colors(记录 → 按类目着色) 丢弃 转发为 categoryColors
subtitle / description 丢弃 转发
annotations / interaction 丢弃 转发
height 直读(0 会压塌绘图区) 走白名单(非正值退回 280 默认)
series[]color/stack/type/yAxis/dashArray/opacity/variant 全部丢弃 series[].name 匹配后合并
title 渲染器自绘 h3 不变(见下方偏离清单)
aria 丢弃 仍不转发(见下方偏离清单)

series 的匹配规则是 series[].name → 派生 dataKey,所以成员资格仍归数据集:作者点名一个本图不绘制的 measure,该条目被忽略,不能增删或改指一条 series。

卡面警告的处理:xAxis/yAxis 是裸名字符串

卡里点名的坑 —— 直接把报表 chart 喂给 mergeAuthoredPresentation 会让 axisPresentation 读一个字符串,合成 yAxis: [{}] 这个「只声明自己存在」的空条目,而 y 轴条目的数量正是开启副轴的开关。

这里从结构上规避,而不是加一道守卫:core 额外导出 mergeAuthoredSeries(只做 series 合并),报表调它。轴在这个面上是纯 DATA(它们就是选择集本身),所以那个面根本够不到读轴表现的代码。


共享 helper 上提(卡面给的第二条路)

chartConfigPresentationmergeAuthoredPresentationplugin-dashboard/src/DatasetWidget.tsx 移到 @object-ui/core/src/utils/chart-presentation.ts,紧挨着它们所合并的那个派生 buildChartSeries

理由是 #4389 的先例:两个 plugin 各写一份同样的分析标签网,被当作缺陷立卡并把纯数据的那半上提到 core。ReportChartSchemaChartConfigSchema 声明的是同一套词汇,在报表侧再抄一份就是同一个缺陷再来一次。

  • 两个 helper 全是纯数据变换,无 React、无 i18n,层级与 chart-series.ts 一致;
  • DatasetWidget 以原名 re-export,模块公开面与渲染行为逐字节不变(其既有测试全绿,见下);
  • 教条(两条准入判据、data/presentation 裁定、aria 为何不转发)随代码一起搬到了新模块头部。

有据偏离清单

两处偏离卡面的键清单,均为刻意,并各有一条钉子把它记成决定而非疏漏:

  1. aria 不转发。 它是 ReportChartSchema 声明的键里,这条路径上唯一无人读取的那个:AdvancedChartImpl 没有 aria prop,而本渲染器是把 schema 直接交给组件,并不经过 SchemaRenderer 的扁平 ARIA 注入(所以连仪表盘那条「扁平化」的可能性在这里都不存在)。转发它只是把「声明了却没人读」下移一层,而那正是本卡要消除的失败模式。这沿用仪表盘的既有裁定(其注释将此上报到 objectstack#5175 的收窄那一半;该编号属 objectstack 仓,本 PR 未独立核实)。
  2. title 不转发。 本渲染器自己把它画成图上方的 h3(卡里也确认 title 本就生效),再转发一次会在图表自身的 frame 里画出第二个标题。

另有一处顺带的行为收紧:height: 0 之前直读会压塌绘图区,现在走白名单被丢弃、退回 280 默认 —— 与仪表盘同判据,已钉。


验收证据(按卡分节)

反向验证:两卡各一次变异证负,方向先书面预判再跑

变异前已 commit,还原一律 git checkout 分支名 -- 文件路径(不用 stash,共享栈)。

#4878 —— 把 DATA 半还原成改前那几行(rows 直通、series 手搓),#4877 的转发保持在位:

预判:chartNullCategory.test.tsx 10 例 → 5 红 / 5 绿。红:① 裸 null 被替换、① 度量值挂在桶上、② zh (未指定)、② en (None)、③ #4508 身份。绿:① series 仍绑定、BOUNDARY 无 null 分组、④ 三条标签例。

实测 5 红 / 5 绿,逐例吻合;chartChrome.test.tsx 19/19 全绿,证明变异只切到了 #4878

#4877 —— 去掉 ...chrome spread 并把 series 还原成手搓,#4878 的路由保持在位:

预判:chartChrome.test.tsx 19 例 → 11 红 / 8 绿。红:8 个 chrome 键 + 作者 height + 3 条 series 表现例。绿:非正 height、未声明 chrome、两条 titlearia、成员资格被忽略、越族 type、未声明 series。

实测 11 红 / 8 绿,逐例吻合;chartNullCategory.test.tsx 10/10 全绿。

一处预判与模板预设不符,如实记录(已回写进测试文件头):en 桶标签那条 BOUNDARY 例,我原先按「两侧都绿」写注释。那对 #4500 自己的变异成立(去掉 nullCategoryLabel 选项但保留路由 —— core 的英文兜底与 en 语言包经不同通道产出同样的字节),对 #4878 的变异则不成立:桶本身被移除时,两种语言都没有标签可读,它必然红。两个测试文件现在都把实测计数写在预判旁边,而不是留一个想当然的方向。

测试与门禁

命令 结果
vitest run packages/plugin-report/ packages/plugin-dashboard/ packages/core/ Test Files 162 passed (162) / Tests 2584 passed (2584)
最终一轮 vitest run packages/plugin-report/ packages/plugin-dashboard/ packages/core/src/utils/ Test Files 106 passed (106) / Tests 1343 passed (1343)
仓根 turbo run type-check --concurrency=2 Tasks: 81 successful, 81 total
node scripts/check-control-bytes.mjs OK (scanned 4390 tracked text file(s))
node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs 均通过,2 个 changeset,无 major
eslint(改动文件) 0 errors(仅既有风格 warning)

新增钉子 29 条:DatasetReportRenderer.chartNullCategory.test.tsx(10)与 DatasetReportRenderer.chartChrome.test.tsx(19)。既有的 DatasetReportRenderer.chartMeasureLabel.test.tsx(#4020 的钉子)全程未改动且全绿 —— 那是承重约束①的独立见证。

changeset 两个文件,按卡分开:.changeset/report-chart-null-category-bucket.md(#4878)、.changeset/report-chart-authored-chrome.md(#4877,含 core / plugin-dashboard 的上提)。均为 patch

相邻发现,未扩围

立卡 #4906(finding 标签,未指派):报表图表既不派生维度选项颜色(buildOptionColorMapcategoryColors),也不派生声明的类目顺序(buildCategoryOrder,framework#3588),而仪表盘两者都派生。今天没有东西渲染错 —— 调色板是位置式而非语义式、漏斗按值降序而非按声明管道序 —— 属跨面 drift,故按 observation 类立卡,不在本 PR 修。


本 PR 为 draft,不挂 auto-merge,等 PM 验收。

claude added 2 commits August 17, 2026 03:32
…rt chrome (#4877, #4878)

A dataset-bound report's embedded chart was the one chart surface that shared
neither half of the ruled dataset/author split. It forwarded exactly six keys —
chartType, data, height, isAnimationActive, series, xAxisKey.

#4878 — the DATA half now routes through `buildChartSeries` (@object-ui/core),
where the whole null-category family lives (#4466 / #4497 / #4673 / #4500 /
#4508). The rows used to reach the renderer with a raw null category, which
draws no mark: the group vanishes while the y-axis still accommodates it. The
bucket label is resolved from the locale bundle at the call site, because core
is React-free. #4020's three-level measure display name still outranks the label
the derivation assigns.

#4877 — the PRESENTATION half now routes through `chartConfigPresentation` and
`mergeAuthoredSeries`, so the chrome (showLegend, showDataLabels, colors,
subtitle, description, annotations, interaction, height) and the per-series
presentation keys ReportChartSchema declares stop being inert metadata.
`showLegend: false` was inverted in effect, not merely ignored.

Both helpers moved from plugin-dashboard's DatasetWidget to @object-ui/core
beside `buildChartSeries`, the derivation they merge onto, so the two surfaces
lower one vocabulary once. DatasetWidget re-exports both names unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
…ons (#4877, #4878)

The `en` bucket-label BOUNDARY case was written as "green on both sides". That
is true of objectui#4500's own mutation (drop the `nullCategoryLabel` option,
keep the routing — core's English floor and the `en` pack produce the same
bytes through different channels) and FALSE of objectui#4878's, which removes
the bucket entirely: with nothing bucketed there is no label to read in either
language, and it measured red. Both files now carry the measured counts beside
the predictions rather than a presumed direction.

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-DzXFL076.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) 498.37KB 111.05KB
core (index.js) 4.06KB 1.61KB
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.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 127.85KB 32.73KB
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) 120.43KB 29.04KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 197.59KB 53.01KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.23KB 26.97KB
plugin-map (index.js) 17.91KB 5.72KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.97KB 11.33KB
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(#4877 + #4878 合单,批次 16,PM 会话 session_01GTRjn8xBqp75dk7kFupVRt)

实物核验(已过):8 文件 +1116/−285 逐字对账;changeset 两个、分卡(chrome / null-bucket 各一);标识 0;releases 0;plugin-dashboard 的触碰在认领评论声明的文件面内(helper 上提路线)。

#4878(null 类目分桶):报表行改路由 buildChartSeries,#4466 家族(#4497/#4673/#4500/#4508)整体继承而非重派生;两条承重约束逐一落实 —— #4020 三级显示名回落压过派生 label(未触碰的 chartMeasureLabel 钉全程独立见证),null 桶标签调用侧走 i18n bundle(zh「(未指定)」/en「(None)」双钉)。变异证负预判 5红/5绿,实测逐例命中,且 chrome 钉 19/19 保持绿证明变异作用域单卡隔离。

#4877(作者面 chrome 键):走卡面第二条被许可路线 —— chartConfigPresentation/mergeAuthoredPresentation 从 DatasetWidget 上提到 @object-ui/core/utils/chart-presentation.ts(循 #4389 先例),DatasetWidget 原名 re-export、公开面不变(全套件绿见证);另导出 mergeAuthoredSeries 使卡面警告的 string-axis 陷阱结构上不可达而非靠防御。变异证负预判 11红/8绿,实测逐例命中。

有据偏离(全部接受)

CI(亲读终态):20 项全 completed,18 success + 2 skipped,零失败。

附带产出:新 finding #4906(报表 chart 缺 option 颜色/声明类目序派生,跨面漂移观察卡)。

→ undraft + auto-merge (SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 04:14
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit 5ffcc14 Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4877-4878-report-chart branch August 17, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment