Repository navigation
Commit 2d3aa8c
feat(plugin-report): a report chart draws its seriesBy split (#28305)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoDx1NeBKzNcQPKReWziCHqf)_
Fixes #28206
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:报表图表写了 `chart.seriesBy` 后真的按第二个维度拆分系列了(例如每个负责人一根柱,按阶段分段);台账里
`chart.seriesBy` 从 planned 改为 live;顺手改掉了 lint 说明里因此变假的两句话。
- 为什么改:`seriesBy` 已能写、发布时也会校验,但图表只查 `[xAxis] × [yAxis]`,拆分从来没画出来;HotCRM
的「按负责人和阶段的商机漏斗」只能退回矩阵表。
- 风险与回滚:只有写了 `seriesBy` 的报表图表行为改变;没写的图表查询和输出逐字节不变(有对照用例钉住)。回滚即 revert 本
PR。
- 先看:`ui/packages/plugin-report/src/DatasetReportRenderer.tsx` 的
`DatasetReportChart`(查询维度、系列排序、系列标签三处)、新测试
`DatasetReportRenderer.chartSeriesBy-28206.test.tsx`、`packages/spec/liveness/report.json`
的 `seriesBy` 行。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/plugin-report/src/__tests__/DatasetReportRenderer.chartSeriesBy-28206.test.tsx`,预期
4 个用例全绿。
- 需要你做的:无,席位落地。
## What changes
**Renderer (`ui/packages/plugin-report/src/DatasetReportRenderer.tsx`,
`DatasetReportChart`).**
- Reads `chart.seriesBy`. When it is set on a series chart, the chart's
own dataset query is `[xAxis, seriesBy]` × `[yAxis]` instead of
`[xAxis]` × `[yAxis]`, and the rows go to `buildChartSeries` with both
dimensions. That takes the helper's existing pivot branch (the one a
two-dimension dashboard widget already takes): one row per x bucket, one
series per split value, null group included. `chart-series.ts` is
unchanged.
- The split dimension joins the chart's dimension-metadata read, so its
values are relabeled to their (localized) option labels, and the series
are listed in that dimension's declared option order with the empty
group last. This is the same derivation `DatasetWidget` runs for its
second dimension (`seriesOrder`), and it uses the same opt-out the
categories use: a report `order` that reaches the chart's selection
keeps the server's order.
- Series labels: with a split, every series is a dimension value, so the
measure's display name (objectui#4020) is no longer stamped onto a
series. A series keeps the value it plots, unless a `chart.series[]`
entry names that value (the spec's reading of `series[].name` when
`seriesBy` is set). Then that entry's presentation lands on it through
`mergeAuthoredSeries`, and its label resolves through `pickLocalized` in
the UI language rather than first-string-wins.
- Without `seriesBy`, the query, the rows and the series are what they
were.
- The file's own prose that said the chart queries only `chart.xAxis` ×
`chart.yAxis` now names the split.
**Liveness (`packages/spec/liveness/report.json`).**
- `chart.children.seriesBy`: `planned` → `live`, `verifiedAt`
2026-10-31, the renderer and the new test as evidence. `authorWarn` /
`authorHint` are dropped (a `live` row may not carry them), so `os
validate` stops printing "nothing draws it yet" for the key.
- S2: the `chart` row's note dropped its stale sentence about a
`groupBy` sub-key (`ChartConfigSchema` declares no `groupBy`) and now
says the chart also groups by `seriesBy` when it is set.
- `packages/spec/liveness/state-counts/report.md` regenerated
(`gen:liveness-counts`): report live 39 → 40, planned 1 → 0.
**Prose this change made false, corrected (no rule, severity or finding
change).**
- `packages/lint/src/validate-chart-bindings.ts`, module header: the
report-charts bullet quoted the renderer as "queries only `chart.xAxis`
× `chart.yAxis`", and the `chart-axis-not-selected` section quoted "the
selection is exactly one dimension × one measure ... returns ONE
series". Both now quote the current renderer text and say what happens
with a split.
- Same file, `ChartBinding.ownSelection` doc comment: "queries
`chart.xAxis` × `chart.yAxis` alone and derives exactly ONE series" now
names the split.
- `packages/lint/src/rule-explanations.ts`, `chart-axis-not-selected`
explanation (`os explain`): "the embedded chart queries exactly one
dimension × one measure and derives ONE series" now says it queries its
`chart.xAxis` dimension (and its `chart.seriesBy` split, when set) ×
that one measure and, without a split, derives one series.
**Changeset:** `.changeset/28206-report-chart-series-by-renderer.md`,
patch for `@object-ui/plugin-report`, `@objectstack/spec` (the published
liveness ledger) and `@objectstack/lint` (the `os explain` text), with
both declaration lines.
## Tests
New
`ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.chartSeriesBy-28206.test.tsx`,
four cases over HotCRM's `opportunity_funnel_owner_stage` shape (a bar
per owner, split by stage; the table groups by owner alone, so the only
selection naming `stage` is the chart's):
1. the chart's query is `{ dimensions: ['owner', 'stage'], measures:
['total_amount'] }`, and the chart gets `xAxisKey: 'owner'` and one row
per owner carrying each stage's amount (the null stage under `(None)`);
2. the series are in the stage field's declared order (Prospecting,
Proposal, Negotiation, Closed Won), the null group last, each keeping
its own column;
3. a `series[]` entry naming the value `Proposal` lands its colour on
that series and its `{ en, zh }` label resolves to the zh limb on a zh
console; the other series are untouched;
4. control: the same chart without `seriesBy` queries `owner` alone and
draws one `total_amount` series labelled `Total Amount`, rows passed
through.
Ablation (one-off, no permanent file): `node
scripts/ablation-replace.mjs` replaced the split arm of the chart query
(`(seriesBy ? [xAxis, seriesBy] : [xAxis]) : [];` → `[xAxis] : [];`,
anchor 1 → 0, blob `280c006b` → `983b4e78`). Predicted before the run:
cases 1–3 red, the control green. Measured: 3 failed, 1 passed. Restore
verified: blob back to `280c006b` and `git diff HEAD` empty.
## Acceptance notes
- **S1 is not in this PR.** The seat dropped it per triage 6528521739
(spec lane). Measured location for the hand-over:
`validateChartBindings` lives in
`packages/lint/src/validate-chart-bindings.ts` (`@objectstack/lint`),
not in `packages/spec`. The warning wanted is `seriesBy === xAxis` on a
report chart.
- **A single-series chart family with `seriesBy`.** A report `pie` /
`donut` / `funnel` that sets `seriesBy` now hands the chart several
series, which the chart component draws as its single-series refusal
notice rather than a split; before this PR it drew one series and
ignored the key. The spec does not refuse `seriesBy` on those types
today. Noted for the spec lane beside S1, not filed.
- **What `series[].name` matches with a split.** It matches the series
key, which is the split value as the rows carry it: the server-resolved
display label (ADR-0021), not the stored option value. For a select
split, an entry naming the stored value (`proposal`) lands on nothing,
as an unknown name does today. Noted, not filed.
## Verification
All readings on head `39eb423b` unless named.
<details><summary>Affected package tests and type-checks</summary>
- `pnpm --dir ui exec vitest run packages/plugin-report/` under
`os-verify-lock`: Test Files 31 passed (31), Tests 282 passed, 7
skipped. VERDICT command-exit 0.
- `pnpm --filter @object-ui/plugin-report run type-check` (after
building its dependency closure with declarations): VERDICT command-exit
0.
- `pnpm --filter @objectstack/lint test`: Test Files 181 passed (181),
Tests 7679 passed. `pnpm --filter @objectstack/lint typecheck`: VERDICT
command-exit 0.
- eslint over the four changed source/test files: 0 errors; the 5
warnings are pre-existing lines outside this diff.
</details>
<details><summary>Derived gates (dispatch-gates --commands, reconciled
with --ran)</summary>
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran` → 71 derived families accounted for: 70 run, all exit 0, 1 NOT
MEASURED.
- NOT MEASURED: `pnpm check:dual-build-cjs-loads`, exit 3 (PREREQUISITE
NOT MET: reads built `dist/` of every package; not built locally).
Declared to CI.
- `check:liveness` first went red on the stale `state-counts/report.md`
shard; regenerated with `gen:liveness-counts` and re-run green.
- `node scripts/check-plugin-teardown-shape.mjs --self-test` first
exited 3 on the shallow clone (its pinned fixture commit was missing);
green after fetching that commit.
- `node scripts/check-sdui-manifest.mjs` and its `--self-test`: exit 0.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibvUw5sBjvzer4Z7WHt)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent aaa5c1d commit 2d3aa8c
7 files changed
Lines changed: 360 additions & 52 deletions
File tree
- .changeset
- packages
- lint/src
- spec/liveness
- state-counts
- ui/packages/plugin-report/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 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3302 | 3302 | | |
3303 | 3303 | | |
3304 | 3304 | | |
3305 | | - | |
3306 | | - | |
| 3305 | + | |
| 3306 | + | |
| 3307 | + | |
3307 | 3308 | | |
3308 | 3309 | | |
3309 | 3310 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
62 | | - | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
63 | 64 | | |
64 | | - | |
| 65 | + | |
| 66 | + | |
65 | 67 | | |
66 | 68 | | |
67 | 69 | | |
| |||
117 | 119 | | |
118 | 120 | | |
119 | 121 | | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
127 | 131 | | |
128 | 132 | | |
129 | 133 | | |
| |||
293 | 297 | | |
294 | 298 | | |
295 | 299 | | |
296 | | - | |
297 | | - | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
298 | 303 | | |
299 | 304 | | |
300 | 305 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | | - | |
| 54 | + | |
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | | - | |
64 | | - | |
| 63 | + | |
| 64 | + | |
65 | 65 | | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
| 66 | + | |
| 67 | + | |
70 | 68 | | |
71 | 69 | | |
72 | 70 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
0 commit comments