Skip to content

fix(plugin-dashboard): 交叉表(pivot ≥2 维)单元格内叠加 compareTo 对比值 (#3614) - #3757

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3614-pivot-compare-stacking
Aug 8, 2026
Merged

fix(plugin-dashboard): 交叉表(pivot ≥2 维)单元格内叠加 compareTo 对比值 (#3614)#3757
yinlianghui merged 1 commit into
mainfrom
claude/issue-3614-pivot-compare-stacking

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3614

问题

DatasetWidget 的 matrix 分支(type: 'pivot'dimensions.length >= 2)是 PR #3612 唯一留白的渲染路径。它按 桶 × 度量cellCols,执行器返回的对比列(度量名加 __compare 后缀,如 amount__compare)既不进表头、也不进单元格,行/列/总计三处小计同样丢弃 —— 于是一个配了有界日期窗口 + compareTo 的 pivot:查询正确、对比数据真的返回了、界面一个字看不到。这正是 #3337 要终结的「静默无对比」,只是换了 widget 家族。

实现(按 2026-08-07 维护者裁决:形态 2 单元格内叠加)

  • 单元格内叠加:主值在上,对比值与 delta 百分比在下(小字号)。列结构保持 桶 × 度量 不变 —— 给对比单独开列会变成 桶 × 度量 × 窗口,列数翻倍、表头三层,而交叉表的宽度本来就是最紧张的资源。
  • 三处小计同样叠加:rowTotalById / colTotalById / grandTotal 与数据单元格同一套处理。只有 Total 列不显示对比,读起来是「这一行没有对比」—— 那是另一个、且错误的陈述。
  • CSV 导出保持数据形:exportColumns 去掉 isMatrix 例外,交叉表也为每个参与对比的度量导出一列扁平的 度量名__compare,单元格仍是裸数字,表格软件可以继续计算。显示叠加,导出是数据,绝不导出 "$120 $100 20%" 这种「CSV 外衣的截图」。
  • 窗口标签:整表一条 caption 说明对比窗口(如 "vs last year"),取自 KPI / 扁平表格同一套 dashboard.trend.* 词汇;delta 复用同一个 computeMetricDelta,所以同样两个窗口下的 KPI 与交叉表单元格在符号和取整上一致,不各算一套百分比。交叉表没有「按窗口的列表头」可以限定 —— 那恰好是叠加换掉的东西,所以标签落在 caption 与单元格 title 上。

对比是否存在从数据判定(沿用既有的 comparedValues),没有新增配置开关:执行器没送对比的 pivot,渲染结果与改动前完全一致。

测试

扩写本包既有的 DatasetWidget.compareTo.test.tsx:

  • 叠加单元格:升(120/100 → up 20%)、降(30/60 → down 50%)、平(200/200 → neutral 0%)、无行的空格子不凭空长出对比;并断言主值与叠加块是两个节点,而非拼接成一个字符串。
  • 三处小计:行小计 150/160、列小计 320/300 与 30/60、总计 350/360 都带对比。
  • 导出:表头 Status,Priority,Amount,Amount · vs last year,数据行 Open,High,120,100,并断言整份 CSV 里没有 $ 也没有 %(任何显示格式都没漏进数据)。
  • 三条回归钉:无 compareTo(逐格断言 td 文本与改前一致);配了 compareTo 但执行器没返回对比列;结果里带 __compare 而 widget 没要求对比。

逆向验证(先预测后跑,两个方向都对上):只回退渲染改动 → 两条叠加测试红在缺失的 matrix-cell-compare,导出测试与三条回归钉全绿;只回退导出改动 → 只有导出测试红(得到 Status,Priority,Amount,缺对比列)。

pnpm exec vitest run --maxWorkers=2 packages/plugin-dashboard/
Test Files 26 passed (26)
Tests 218 passed (218)
pnpm --filter @object-ui/plugin-dashboard type-check # tsc --noEmit,无输出
pnpm --filter @object-ui/plugin-dashboard lint # 0 errors(221 个既有 any warning)
node scripts/check-control-bytes.mjs # OK(3690 个文本文件)

Changeset:.changeset/pivot-compare-stacking-3614.md(patch,user-visible)。文档:packages/plugin-dashboard/SKILL.mdcompareTo 章节补上 pivot 一条与导出形状说明。


Generated by Claude Code

…-tab (#3614)
The matrix branch of `DatasetWidget` (`type: 'pivot'` with two or more
dimensions) was the one render path PR #3612 left out. It lays out
`bucket x measure` and never admitted the `<measure>__compare` columns the
executor returns, so a pivot with a bounded date window and a `compareTo` ran a
correct comparison query, received correct comparison data, and showed none of
it — headers, cells and all three subtotals silent. That is objectui#3337's
"silently no comparison", relocated to another widget family.
Maintainer ruling 2026-08-07 chose form 2, in-cell stacking:
- The comparison stacks INSIDE the cell — current value on top, comparison
value and delta beneath in smaller type. The column structure stays
`bucket x measure`: a comparison column would make it
`bucket x measure x window`, doubling the width and adding a third header
level on the widget family whose width is already the scarce resource.
- rowTotalById / colTotalById / grandTotal stack it identically. A Total that
alone showed no comparison reads as "this row has none" — a different, false
statement.
- `exportColumns` drops its `isMatrix` exception, so the cross-tab exports a
flat `<measure>__compare` column per compared measure with bare numbers in
the cells. Display stacks; export is data, and a spreadsheet can still
compute on it (never "$120 $100 20%").
- One caption names the window for the whole table, from the same
`dashboard.trend.*` vocabulary the KPI/flat-table paths use, and the delta
comes from the same `computeMetricDelta` — a cross-tab cell and a dataset KPI
over the same two windows agree on sign and rounding rather than each rolling
their own percentage. A cross-tab has no per-window column header to qualify,
which is exactly what stacking traded away.
Presence is detected from the DATA via the existing `comparedValues`, not a new
config flag, so nothing is opted into per path and a pivot the executor sent no
comparison for renders byte-identically to before.
Tests extend the package's own compareTo suite: stacked cells (rise / fall /
flat / empty cell), all three subtotals, the flat CSV export asserted to carry
no display formatting at all, plus three regression pins (no compareTo; a
compareTo the executor answered without comparison columns; stray `__compare`
columns the widget never asked to compare). Reverse verification, predicted
before running: reverting the render change alone turns the two stacking tests
red on the missing `matrix-cell-compare` and leaves the export test and all
three pins green; reverting the export change alone turns only the export test
red ("Status,Priority,Amount" vs the expected comparison column). Both matched.
Fixes#3614
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 8, 2026 12:03pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DZtYqjz8.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
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)480.72KB105.64KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.84KB3.18KB
data-objectstack (index.js)137.51KB35.11KB
fields (index.js)230.90KB56.84KB
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)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
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)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)117.06KB30.24KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)186.61KB49.34KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
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)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
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 (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)2.71KB1.34KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 12:05
@yinlianghui
yinlianghui added this pull request to the merge queueAug 8, 2026
Merged via the queue into main with commit 02eb444Aug 8, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3614-pivot-compare-stacking branch August 8, 2026 12:06
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.

DatasetWidget 的交叉表(pivot ≥2 维)丢弃 __compare 列:配了 compareTo 的 pivot 静默无对比

2 participants

@yinlianghui@claude