Repository navigation
Commit d7625ea
fix(plugin-charts): a two-dimension pie, donut, funnel or treemap draws every group or refuses, never the first group alone (#27736)
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgP6XkfpjCfyzzG2KwQPUZfy)_
Fixes #27563
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:仪表板的矩形树图拿到两个维度(例如 阶段 × 负责人,金额求和)时,按「阶段 →
负责人」两级嵌套画出每一格;饼图、环形图、漏斗图拿到两个维度时不再只画第一个负责人的数,而是在磁贴上显示拒绝说明,并点名每一个负责人分组。
-
为什么改:第二个维度被透视成多条序列,这四个图表分支只读第一条,其余分组静默丢掉,看起来像一张完整的图。裁决是「每组都画,或者拒绝,绝不出半张图」:树图原生支持嵌套,所以画全;饼图一圈只能表达一个整体的份额、漏斗每段只能是一个总数,没有放第二条序列的位置,所以拒绝。
-
风险与回滚:只动这四个图表分支和仪表板组件给这几类图传的序列;单维度图表画法不变,对比叠加(compareTo)不算分组,没有数据时仍是原来的空图。仪表板外带多个度量的饼图/漏斗(例如手写多条
yAxis)现在也会拒绝、树图会按度量嵌套,见 Acceptance notes 第一条,请席位确认;回滚即 revert 本 PR。
- 先看:`ui/packages/plugin-charts/src/AdvancedChartImpl.tsx` 的
`buildTreemapGroups`、`OneSeriesFamilyRefusal` 和 pie/funnel/treemap
分支开头;`ui/packages/plugin-dashboard/src/DatasetWidget.tsx` 的
`chartSeries`。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/plugin-charts/src/AdvancedChartImpl.twoDimensionSingleSeries-27563.test.tsx
packages/plugin-dashboard/src/__tests__/DatasetWidget.twoDimensionSingleSeries-27563.test.tsx`,预期
32 条全绿。
- 需要你做的:无,席位落地。
## What changes
A dashboard widget (or dataset-bound chart) of type `pie`, `donut`,
`funnel` or `treemap` with `dimensions: ['stage', 'owner']` and one
measure reaches the renderer pivoted by `buildChartSeries`: one row per
stage, one series per owner. These four arms read `series[0]` alone, so
they drew the first owner's amounts as if they were the whole chart.
Triage's rule (the same as #27460): draw every group, or refuse a shape
the chart cannot draw; never a partial chart. Per family:
- **Treemap: draws every group.** Nesting is native to Recharts'
`Treemap`, so two or more groups are drawn as two levels
(`buildTreemapGroups`, the treemap counterpart of #27560's
`buildSankeyCellFlows`): each stage is a parent, each owner it has
records for a leaf inside it, sized by the measure. A leaf is named
`stage / owner` (the title the dashboard's drill gives the same cell; a
parent's own name is painted over by its leaves) and takes its stage's
colour. A combination the dataset did not answer is no leaf and no
omission. A carried cell that cannot be sized is left out and the
existing `unsized-rows` note is printed once per owner that lost one; no
sizable cell at all is the existing `no-positive-magnitude` refusal, now
naming every owner it tested. Clicking a leaf drills to its cell (`{
category, series }`, as the two-level sankey's flow click does).
- **Pie, donut, funnel: refuse on the tile.** A pie's angle is one share
of one whole and a funnel stage one total, so a second series has no
place on either; a second ring or a stacked funnel would be a new visual
with its own legend and colour contract, which this card does not
decide. Handed two or more groups with rows to draw, these arms now
return `OneSeriesFamilyRefusal` in the existing `ChartRefusal` shell
(`role="status"`, `data-chart-error="one-series-family"`): "A pie draws
one series, and this chart carries 3: `Ann`, `Bo`, `Cy`. Drawing only
the first would hide the others, so none is drawn." Every group is
named, and the sentence stays true if the series are measures rather
than groups.
- **Which series are groups.** `groupSeries` (the sankey's
`sankeyFlowSeries`, renamed and shared) drops a comparison overlay, so a
`compareTo` treemap stays one level.
- **The dashboard hands these families one measure.** `DatasetWidget`
already handed a sankey only the measure it renders (#27560); the same
filter now covers every `SINGLE_SERIES_CHART_FAMILIES` family. A stored
widget of these types that declares several measures with one dimension
(refused at publish by the spec's single-series arm since 17.7.0)
therefore still draws its first measure, and the dropped-measure console
warning stays true, instead of its second measure being read as a group.
A pivot carries one measure, so nothing is dropped from it.
Unchanged: every one-dimension chart of the four types (regression pins
below), the empty-rows path, the sankey.
## Tests
-
`ui/packages/plugin-charts/src/AdvancedChartImpl.twoDimensionSingleSeries-27563.test.tsx`
(20): the fixture is a real `buildChartSeries` pivot (stage × owner, sum
amount). Treemap: one leaf per answered cell with its size, shared stage
colour, the per-owner note for a cell at -5, the magnitude refusal
naming every owner, the leaf click's drill event; pie/donut/funnel: no
`Pie`/`Funnel` element rendered and the refusal names every owner;
regression pins per family for one dimension, the compared treemap, and
no rows. Marks are read from the props each Recharts element was
rendered with, not jsdom pixels.
-
`ui/packages/plugin-charts/src/ChartRenderer.yAxisUndrawn-10691.test.tsx`:
`draw` settles on the plot or the renderer's refusal, and the pie
control asserts the one-series refusal (see Acceptance notes).
-
`ui/packages/plugin-dashboard/src/__tests__/DatasetWidget.twoDimensionSingleSeries-27563.test.tsx`
(12): the card's widgets through `DatasetWidget` → `SchemaRenderer` →
`AdvancedChartImpl`: the treemap draws 5 depth-2 leaves,
pie/donut/funnel refuse naming every owner, a one-dimension widget of
each type draws, and a stored one-dimension widget with two measures
still draws (no group refusal, no nested treemap).
## Acceptance notes
- **Mechanism measured, premise holds.** On `origin/main` (`525ea3632`)
the renderer file swapped in: the treemap drew 3 one-level leaves with
no children, and pie, donut and funnel each rendered one `Pie`/`Funnel`
element bound to the first owner's column (15 of the 32 new tests red,
every control green).
- **Refusal on the tile, not at publish.** Triage's text says "refuse at
publish". A publish-time refusal of a second dimension on these types is
a spec door (`packages/spec`, outside this card); the tile refusal meets
"never a partial chart" today and names every group. Whether the spec
should also refuse the shape at publish is the seat's call.
- **⚠ Seat to confirm: charts outside the dashboard that carry several
MEASURES.** `AdvancedChartImpl` cannot tell a pivoted group from a
second measure; it reads two or more non-comparison series as groups, as
#27560 made the sankey do. The dashboard hands these families one
measure (above). Two other shapes reach the arms with several measures
and change too: a chart block that authors a `yAxis` list on a pie,
donut or funnel, and a page chart bound to a dataset with ONE dimension
and TWO measures (`checkDashboardWidgetChartMeasureArity` is attached to
the dashboard widget schema only, so no spec door refuses either there).
Both used to draw their first measure silently, a partial chart; a pie,
donut or funnel now shows the one-series refusal naming every measure,
and a treemap nests the measures under each category. Measured:
`ChartRenderer.yAxisUndrawn-10691.test.tsx`'s control "(control) pie,
three entries: no note" settled on a drawn `svg`, which the pie no
longer draws; its subject (no y-axis note on a pie) still holds, so the
helper now settles on the plot or the refusal and the control also
asserts the refusal. If the seat wants those multi-measure charts kept
drawing their first measure, the alternative is to mark pivot series in
`buildChartSeries` (`@object-ui/core`, a published type: `Clause-②: yes
(widening)`) and refuse only on that marker.
- **Copy is English**, matching every sibling refusal in this file
(`no-positive-magnitude`, `no-positive-flow`, `missing-series-key`); no
locale keys added.
- A third dimension still collapses into the (d0, d1) cell, as the card
notes; untouched.
- **File surface:** `DatasetWidget.tsx` is touched for the measure
filter (the line #27560 added for the sankey, widened) and two comments,
not for a refusal note; the Claim lists the file conditionally.
## Verification
Local subset at head `4fb5e46c` (after merging `origin/main`); the two
commits after it touch only the changeset, and the changeset-reading
gates were re-run at `d12c72c8`. CI runs the full farm.
<details><summary>Premise and ablation (base `525ea3632`)</summary>
Each leg swapped files from the base into the tree with `git restore
--source=525ea3632`, checked the blob hash, ran, and restored from
`HEAD` under a trap (restore proven by `git hash-object` equal to the
`HEAD` blob and `git diff HEAD` empty).
- Renderer and widget at base: the two new files, 15 failed / 17 passed
(every subject red, every control green). The treemap drew three
one-level leaves (`Prospect`, `Proposal`, `Won`) with no children; pie,
donut and funnel each rendered one `Pie` / `Funnel` element (bound to
the first owner's column).
- Widget measure filter alone at base (renderer fixed): 8 failed: the
four "stored one-dimension TYPE with two measures still draws its first
measure" controls, and the four `fires iff the second measure's values
leave the drawn chart unchanged` rows of
`DatasetWidget.singleSeriesMeasures-11417.test.tsx` for pie, donut,
funnel and treemap.
</details>
<details><summary>Tests</summary>
- `pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-charts/
packages/plugin-dashboard/` (under the verify lock, at `5d26a453`): Test
Files 297 passed, 1 failed (298); the failure was the
`ChartRenderer.yAxisUndrawn-10691` pie control described in Acceptance
notes, updated in the next commit.
- The updated control, the 27563 renderer file and every ui test file
that names a pie, donut, funnel or treemap outside the two packages
(console `unfulfilled-chart-stubs-8760`, app-shell
`listViewLegacyChartRetired-6152` and `ObjectView.chartRelay-7823`,
plugin-chatbot `ChatbotEnhanced.chartOrder-13492` and
`useObjectChat.chartOrderRehydrate-13492`, plugin-list
`ListView.chart-capability-7544`, the whole
`plugin-report/src/__tests__/`): Test Files 38 passed (38), Tests 390
passed, 7 skipped.
- At `4fb5e46c`: the two 27563 files, the 10691 file,
`DatasetWidget.singleSeriesMeasures-11417`, and both 27460 sankey files:
Test Files 6 passed (6), Tests 81 passed.
- `pnpm --filter @object-ui/plugin-charts run type-check && pnpm
--filter @object-ui/plugin-dashboard run type-check`: exit 0 (both
programs include the new tests via `tsconfig.test.json`).
- eslint on the touched files: 0 errors; the warnings on new lines are
`no-explicit-any` in the renderer test's recharts mocks, the shape the
27460 test uses.
</details>
<details><summary>Derived gates (dispatch-gates --commands --repo
objectstack-ai/objectstack: 35; all run; reconciled with
--ran)</summary>
```text
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
```
`dispatch-gates --ran`: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` untouched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the touched
files: 0 errors; the repo-wide run is CI's.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CanXq6PY9nH1pZ5E5m)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6d8372a commit d7625ea
6 files changed
Lines changed: 571 additions & 23 deletions
File tree
- .changeset
- ui/packages
- plugin-charts/src
- plugin-dashboard/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1154 | 1154 | | |
1155 | 1155 | | |
1156 | 1156 | | |
1157 | | - | |
1158 | | - | |
1159 | | - | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
1160 | 1162 | | |
1161 | | - | |
| 1163 | + | |
1162 | 1164 | | |
1163 | 1165 | | |
1164 | 1166 | | |
| |||
1240 | 1242 | | |
1241 | 1243 | | |
1242 | 1244 | | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
| 1289 | + | |
| 1290 | + | |
| 1291 | + | |
| 1292 | + | |
| 1293 | + | |
| 1294 | + | |
| 1295 | + | |
| 1296 | + | |
| 1297 | + | |
| 1298 | + | |
| 1299 | + | |
| 1300 | + | |
| 1301 | + | |
| 1302 | + | |
| 1303 | + | |
| 1304 | + | |
| 1305 | + | |
| 1306 | + | |
1243 | 1307 | | |
1244 | 1308 | | |
1245 | 1309 | | |
| |||
1385 | 1449 | | |
1386 | 1450 | | |
1387 | 1451 | | |
| 1452 | + | |
| 1453 | + | |
| 1454 | + | |
1388 | 1455 | | |
1389 | | - | |
| 1456 | + | |
| 1457 | + | |
1390 | 1458 | | |
1391 | 1459 | | |
1392 | 1460 | | |
1393 | | - | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
| 1468 | + | |
| 1469 | + | |
| 1470 | + | |
| 1471 | + | |
| 1472 | + | |
| 1473 | + | |
| 1474 | + | |
| 1475 | + | |
| 1476 | + | |
| 1477 | + | |
| 1478 | + | |
| 1479 | + | |
| 1480 | + | |
| 1481 | + | |
| 1482 | + | |
| 1483 | + | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
| 1491 | + | |
| 1492 | + | |
| 1493 | + | |
| 1494 | + | |
| 1495 | + | |
| 1496 | + | |
| 1497 | + | |
| 1498 | + | |
| 1499 | + | |
| 1500 | + | |
| 1501 | + | |
| 1502 | + | |
| 1503 | + | |
| 1504 | + | |
| 1505 | + | |
1394 | 1506 | | |
1395 | 1507 | | |
1396 | 1508 | | |
| |||
2137 | 2249 | | |
2138 | 2250 | | |
2139 | 2251 | | |
| 2252 | + | |
| 2253 | + | |
| 2254 | + | |
| 2255 | + | |
| 2256 | + | |
| 2257 | + | |
| 2258 | + | |
| 2259 | + | |
| 2260 | + | |
| 2261 | + | |
| 2262 | + | |
| 2263 | + | |
| 2264 | + | |
| 2265 | + | |
| 2266 | + | |
| 2267 | + | |
| 2268 | + | |
| 2269 | + | |
| 2270 | + | |
| 2271 | + | |
| 2272 | + | |
2140 | 2273 | | |
2141 | 2274 | | |
2142 | 2275 | | |
| |||
2617 | 2750 | | |
2618 | 2751 | | |
2619 | 2752 | | |
| 2753 | + | |
| 2754 | + | |
| 2755 | + | |
| 2756 | + | |
| 2757 | + | |
| 2758 | + | |
2620 | 2759 | | |
2621 | 2760 | | |
2622 | 2761 | | |
| |||
2690 | 2829 | | |
2691 | 2830 | | |
2692 | 2831 | | |
| 2832 | + | |
| 2833 | + | |
| 2834 | + | |
| 2835 | + | |
| 2836 | + | |
2693 | 2837 | | |
2694 | 2838 | | |
2695 | 2839 | | |
| |||
2782 | 2926 | | |
2783 | 2927 | | |
2784 | 2928 | | |
| 2929 | + | |
| 2930 | + | |
| 2931 | + | |
| 2932 | + | |
| 2933 | + | |
| 2934 | + | |
| 2935 | + | |
| 2936 | + | |
| 2937 | + | |
| 2938 | + | |
| 2939 | + | |
| 2940 | + | |
| 2941 | + | |
| 2942 | + | |
| 2943 | + | |
| 2944 | + | |
| 2945 | + | |
| 2946 | + | |
| 2947 | + | |
| 2948 | + | |
| 2949 | + | |
| 2950 | + | |
| 2951 | + | |
| 2952 | + | |
| 2953 | + | |
| 2954 | + | |
| 2955 | + | |
| 2956 | + | |
| 2957 | + | |
2785 | 2958 | | |
2786 | 2959 | | |
2787 | 2960 | | |
| |||
2820 | 2993 | | |
2821 | 2994 | | |
2822 | 2995 | | |
2823 | | - | |
| 2996 | + | |
2824 | 2997 | | |
2825 | 2998 | | |
2826 | 2999 | | |
| |||
0 commit comments