Skip to content

Commit 8d36669

Browse files
feat(spec): a report chart can split its bars by a second dataset dimension (chart.seriesBy) (#28191)
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D <!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoXKA5bTPC7aHkxyxEdEEiFv)_ Fixes #27974 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: follow-up #28207 (skills/objectstack-ui/rules/dashboards.md, to land with the renderer half #28206) ## 维护者速读 - 改了什么:报表图表(`report.chart`)新增一个可选键 `seriesBy`,填数据集的第二个维度名,把同一个度量按这个维度的每个取值拆成多组柱子,比如"每个负责人一根柱,按阶段分组"。 - 为什么改:报表图表以前只有一个 X 维度和一个 Y 度量,画不出"按 X 再按 Y 拆分"的图;HotCRM 的商机漏斗按负责人×阶段只能退回成矩阵表。仪表盘早就能这样画,报表缺这一个键。 - 风险与回滚:只加了一个可选键,不填的报表解析和绘制完全不变;拼错的维度名在 `os validate` / 发布时报错。回滚即 revert 本 PR。本 PR 只是契约这一半,图表还不会画出拆分,作者写了会收到"已声明、尚未生效"的提示,渲染器那一半是后续卡。 - 先看:`packages/spec/src/ui/report.zod.ts` 的 `ReportChartSchema`、`packages/lint/src/validate-chart-bindings.ts` 的 `checkReportSurface` 图表分支、`packages/spec/liveness/report.json` 的 `chart.children.seriesBy`。 - 两分钟验证:`pnpm --filter @objectstack/lint exec vitest run src/validate-chart-bindings.test.ts -t seriesBy`,预期 2 条全绿(拼错的 `seriesBy` 报 `chart-dimension-unknown` 并提示最近的维度名)。 - 需要你做的:无,席位落地。 ## What changes A report chart could plot one `xAxis` dimension against one `yAxis` measure, so a bar per owner split by stage was not expressible; HotCRM's `opportunity_funnel_owner_stage` fell back to an owner × stage `matrix` (hotcrm#2379). This is the spec half of the card: the key, its publish-time check, and its liveness grade. - **Spec (`packages/spec/src/ui/report.zod.ts`).** `ReportChartSchema` declares one optional key, `seriesBy`: a second dimension name of the report's bound dataset. It splits the chart's one `yAxis` measure into a series per value of that dimension. It is the report's spelling of a dashboard widget's two-entry `dimensions` selection (`[xAxis, seriesBy]` × `[yAxis]`), which the dataset path already pivots into grouped series (`buildChartSeries` in `ui/packages/core`). With `seriesBy` set, a `chart.series[]` entry's `name` names a VALUE of that dimension, because each derived series is one value. The docblock and the `.describe()` say both. - **Lint (`packages/lint/src/validate-chart-bindings.ts`, `validateChartBindings`).** This is the check that resolves `chart.xAxis` against the dataset's declared dimensions. `chart.seriesBy` now goes through the same `dimensionRef`, so an undeclared name is a `chart-dimension-unknown` error at `os validate`, `os build` and the report publish door, with the nearest declared dimension suggested. When `seriesBy` is set, the `chart.series[].name` overrides are not judged against the chart's measure, since they name dimension values that no static check can see. The `os explain chart-axis-not-selected` text (`rule-explanations.ts`) says so. - **Liveness (`packages/spec/liveness/report.json`).** The `chart` row now drills: `childrenDefault: live` keeps every existing chart key on the row's verdict, `seriesBy` is graded `planned` with `authorWarn`, so an author who writes it is told nothing draws it yet, and the tombstoned `aria` child, now walked, is graded `dead`. Drilling surfaced the three nested containers under `chart` (`series`, `annotations`, `interaction`); they are recorded in `undrilled-containers.baseline.json` in place of the single `report/chart` row they used to ride under, with no per-key evidence invented. `state-counts/report.md` regenerated. - **Generated.** `content/docs/references/ui/report.mdx` (`gen:docs`) and `authorable-surface/ui.json` (one `ui/ReportChart:seriesBy` line). - **Changeset.** `.changeset/27974-report-chart-series-by.md`: `@objectstack/spec` minor, `@objectstack/lint` minor. ### The shape chosen, and the alternatives not taken - **A new `seriesBy` key, not the inherited `series`.** `ChartConfigSchema.series` already exists on the report chart as the per-series presentation override array (`{ name, label, color, stack, … }`); reusing that name for a dimension would give one key two meanings. `ChartConfigSchema` declares no dimension-valued series member to reuse. - **Not `groupBy`.** On the react `ObjectChart` block's `aggregate` `groupBy` is the X-axis category, and on kanban views it is the lane field, so a report author reading `groupBy` would expect the X axis. - **Not widening `xAxis` to a list.** That changes the type of a key every renderer and the Studio inspector read as one string. - **Not reusing the report's own `columns`.** `columns` is the matrix table's across axis; tying the chart's split to the table would stop a summary report from splitting its chart. ## Tests - `packages/spec/src/ui/report.test.ts`: `seriesBy` is accepted and kept as authored, absent stays `undefined`, and a list value is refused at `seriesBy`. - `packages/spec/src/ui/chart.test.ts`: the `.extend()` strictness pin accepts `seriesBy` as the report chart's own key and still refuses an undeclared one with the inherited rename. - `packages/lint/src/validate-chart-bindings.test.ts`: a misspelled `seriesBy` is one `chart-dimension-unknown` error at `reports[0].chart.seriesBy` with a did-you-mean, against a declared-dimension control; a `series[].name` naming a value warns without `seriesBy` (control) and is silent with it. ## Acceptance notes - **The renderer half is a follow-up `domain:ui` card** (the seat files it when this lands): `DatasetReportChart` in `ui/packages/plugin-report/src/DatasetReportRenderer.tsx` queries `[xAxis]` × `[yAxis]` and passes one dimension to `buildChartSeries`. It needs to add `seriesBy` to the query dimensions and pass `[xAxis, seriesBy]`, match `chart.series[].name` overrides against the pivoted series keys, and flip the `chart.seriesBy` liveness row to `live`. Until then the key is graded `planned` and authors are warned. - **No rename aliases for `seriesBy`.** The report chart's unknown-key answer is `ChartConfigSchema`'s error map, which rides `.extend()`. A report-only alias such as `groupBy → seriesBy` would have to live on that shared base, where it is wrong for the inline-data tier. - **`seriesBy` equal to `xAxis`** parses and lints clean. It draws one series per bar, which is harmless; a refusal can follow if it turns up in practice. - **Stacking** stays a series property (`series[].stack`). With `seriesBy` set, stacking the split means naming each value with a shared `stack` group; a chart-level switch is not part of this card. ## Verification All readings at head `061a780d` on objectstack-ai/objectstack (merge base `9f13ee81`). <details><summary>Tests</summary> - `pnpm --filter @objectstack/spec exec vitest run src/ui scripts/liveness`: Test Files 176/176, Tests 6227/6227. - `pnpm --filter @objectstack/lint test`: Test Files 178/178, Tests 7646/7646. - `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter @objectstack/lint typecheck`: lock VERDICT command-exit 0. - Ablations through `scripts/ablation-replace.mjs`, each restored to the HEAD blob: (1) the `seriesBy` `dimensionRef` line deleted, lint suite 1 failed / 80; (2) the `seriesBy ? []` series guard reverted, 1 failed / 80. - Author warning probe (`lintLivenessProperties` on a report with and without `chart.seriesBy`): one `liveness-planned-property` finding, on the report that sets it. </details> <details><summary>Gates</summary> - `dispatch-gates --commands` at `061a780d`: 106 families, each run with stdin from `/dev/null`, exit codes recorded before any pipe. `--ran`: 106 derived, 106 run, 0 NOT MEASURED, 0 unrun, 0 red. - Four families first answered exit 3 (prerequisite not met) and were re-run after their prerequisite, all exit 0: `check-plugin-teardown-shape --self-test` (after fetching the pinned control commit into the shallow clone), spec `check:skill-examples`, `check:dual-build-cjs-loads` and `check:lean-entry-closure` (after a full `packages/**` build). - `pnpm --filter @objectstack/spec check:generated`: all 14 generated artifacts up to date. `check:liveness`: exit 0. - The full CI farm (`test:repo`, the UI lanes) is CI's. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibxKEFRdVkHHwAWLUSE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3a2f4c0 commit 8d36669

12 files changed

Lines changed: 135 additions & 8 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
feat(spec): a report chart can split its bars by a second dataset dimension with `chart.seriesBy`
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: added
10+
11+
`ReportChartSchema` declares one optional key, `seriesBy`: the name of a second dimension of the report's bound dataset. It splits the chart's one `yAxis` measure into a series per value of that dimension, so a report can draw a bar per owner split by stage. It is the report's spelling of a dashboard widget's two-entry `dimensions` selection. With `seriesBy` set, a `chart.series[]` entry's `name` names a value of that dimension, because each derived series is one value.
12+
13+
- **Lint.** `os validate`, `os build` and the report publish door resolve `chart.seriesBy` against the dataset's declared dimensions exactly as they resolve `chart.xAxis`: an undeclared name is a `chart-dimension-unknown` error with the nearest declared dimension suggested. When `seriesBy` is set, `chart.series[].name` is no longer judged against the chart's measure, since it names a dimension value.
14+
- **Liveness.** The key is graded `planned` with an author warning: this is the contract half, and the report chart still draws one series until the renderer reads `seriesBy` in a later change.
15+
16+
Nothing to do for existing reports. A chart without `seriesBy` parses and draws exactly as before.

‎content/docs/references/ui/report.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ const result = JoinedReportBlockSchema.parse(data);
104104
| **annotations** | `{ type?: Enum<'line' \| 'region'>; axis?: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` |
105105
| **interaction** | `{ tooltips?: boolean; brush?: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` |
106106
| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `<ObjectChart>` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
107+
| **seriesBy** | `string` | optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension |
107108

108109
### Nested Shape: `Report.dateRange`
109110

@@ -167,6 +168,7 @@ const result = JoinedReportBlockSchema.parse(data);
167168
| **annotations** | `{ type?: Enum<'line' \| 'region'>; axis?: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` |
168169
| **interaction** | `{ tooltips?: boolean; brush?: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` |
169170
| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `<ObjectChart>` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
171+
| **seriesBy** | `string` | optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension |
170172

171173
### Allowed Values: `ReportChart.type`
172174

‎packages/lint/src/rule-explanations.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3306,7 +3306,9 @@ const CHART_AXIS_NOT_SELECTED_EXPLANATION: RuleExplanation = {
33063306
'exactly one dimension × one measure and derives ONE series, while `report.values` is the ' +
33073307
'selection of the table beneath it. So a report\'s `chart.yAxis` is never reported — a chart ' +
33083308
'cannot fail to select what it queries — and a `chart.series[].name` override lands only when ' +
3309-
'it names `chart.yAxis`. The verdict quotes at most three names of the selection.',
3309+
'it names `chart.yAxis`. With `chart.seriesBy` set the chart derives one series per value of ' +
3310+
'that dimension, so an override names a value, which no static check can see, and is not ' +
3311+
'judged. The verdict quotes at most three names of the selection.',
33103312
CHART_BINDING_SURFACES,
33113313
],
33123314
};

‎packages/lint/src/validate-chart-bindings.test.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,49 @@ describe('validateChartBindings — report charts', () => {
128128
expect(findings[0].path).toBe('reports[0].chart.xAxis');
129129
});
130130

131+
// #27974 — the split dimension is resolved exactly like `xAxis`, so a
132+
// misspelled one is refused at publish rather than silently plotting one series.
133+
it('errors on a seriesBy naming an undeclared dimension, and passes a declared one', () => {
134+
const report = (seriesBy: string) => ({
135+
...baseStack(),
136+
reports: [
137+
{
138+
name: 'r',
139+
dataset: 'task_metrics',
140+
values: ['task_count'],
141+
chart: { type: 'bar', xAxis: 'status', yAxis: 'task_count', seriesBy },
142+
},
143+
],
144+
});
145+
expect(validateChartBindings(report('priority')), 'control: a declared dimension').toEqual([]);
146+
const findings = validateChartBindings(report('priorty'));
147+
expect(findings).toHaveLength(1);
148+
expect(findings[0].severity).toBe('error');
149+
expect(findings[0].rule).toBe(CHART_DIMENSION_UNKNOWN);
150+
expect(findings[0].path).toBe('reports[0].chart.seriesBy');
151+
expect(findings[0].hint).toContain('Did you mean "priority"?');
152+
});
153+
154+
// With `seriesBy` set the derived series are the split dimension's VALUES,
155+
// so an override's `name` names a value — not a measure, and not judged.
156+
it('does not judge `series[].name` against the measures when `seriesBy` splits the chart', () => {
157+
const chart = (extra: Record<string, unknown>) => ({
158+
...baseStack(),
159+
reports: [
160+
{
161+
name: 'r',
162+
dataset: 'task_metrics',
163+
values: ['task_count'],
164+
chart: { type: 'bar', xAxis: 'status', yAxis: 'task_count', series: [{ name: 'high', label: 'High' }], ...extra },
165+
},
166+
],
167+
});
168+
// Control: without the split, `high` is no declared measure and warns.
169+
const unsplit = validateChartBindings(chart({}));
170+
expect(unsplit.map((f) => [f.rule, f.path])).toEqual([[CHART_MEASURE_UNKNOWN, 'reports[0].chart.series[0].name']]);
171+
expect(validateChartBindings(chart({ seriesBy: 'priority' }))).toEqual([]);
172+
});
173+
131174
// #15734 — this fixture WARNED until the set moved, and this pin held the
132175
// warning in place. At the pinned `@object-ui` revision the embedded chart
133176
// runs its OWN query out of the axis pair — `useDatasetRows(dataset,

‎packages/lint/src/validate-chart-bindings.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* object/array shapes to bare STRINGS, which is why simply pointing the
2020
* dashboard rule at reports would find nothing: its `Array.isArray(yAxis)`
2121
* guard skips a string silently. `series[].name` keeps the array shape.
22+
* `seriesBy`, the optional split dimension, is a string dimension ref
23+
* like `xAxis` and is resolved the same way.
2224
* A `joined` report draws no chart (#20161): its container `chart` is
2325
* refused by `ReportSchema` and a block has no `chart` key, so neither is
2426
* a binding this rule checks. Checking one would tell the author their
@@ -269,6 +271,8 @@ interface ChartBinding {
269271
values?: { names: string[]; path: string };
270272
/** Single dimension ref (report `xAxis`). */
271273
xAxis?: { name: string; path: string };
274+
/** Single dimension ref (report `seriesBy`, the split dimension). */
275+
seriesBy?: { name: string; path: string };
272276
/** Single measure ref (report `yAxis`). */
273277
yAxis?: { name: string; path: string };
274278
/**
@@ -501,6 +505,7 @@ export function validateChartBindings(stack: AnyRec): ChartBindingFinding[] {
501505
}
502506
}
503507
if (binding.xAxis) dimensionRef(binding.xAxis.name, binding.xAxis.path);
508+
if (binding.seriesBy) dimensionRef(binding.seriesBy.name, binding.seriesBy.path);
504509
// No selection is passed at this position: the report `yAxis` IS the query
505510
// the chart issues, so it cannot name a measure that query does not ask
506511
// for (#15734). The `chart-measure-unknown` half still runs — an undeclared
@@ -576,15 +581,22 @@ export function validateChartBindings(stack: AnyRec): ChartBindingFinding[] {
576581
if (!isRec(chart)) return;
577582
const xAxisName = strName(chart.xAxis);
578583
const yAxisName = strName(chart.yAxis);
584+
const seriesByName = strName(chart.seriesBy);
579585
checkAgainst(ds, dsName, {
580586
xAxis: xAxisName ? { name: xAxisName, path: `${path}.chart.xAxis` } : undefined,
581587
yAxis: yAxisName ? { name: yAxisName, path: `${path}.chart.yAxis` } : undefined,
588+
// The split dimension is a dimension ref exactly like `xAxis`: the
589+
// chart's query groups by it, so an undeclared name is no column.
590+
seriesBy: seriesByName ? { name: seriesByName, path: `${path}.chart.seriesBy` } : undefined,
582591
// The chart's own selection: the ONE measure it queries and derives a
583592
// series from. Empty when no `yAxis` is authored — the chart plots
584593
// nothing at all then, which the shape rules own, so no override can
585594
// be reported as landing on nothing.
586595
ownSelection: yAxisName ? [yAxisName] : [],
587-
series: recordsOf(chart.series)
596+
// With `seriesBy` set the chart derives one series per VALUE of that
597+
// dimension, so a `series[].name` override names a value — data no
598+
// static check can see — and is not judged against any measure set.
599+
series: seriesByName ? [] : recordsOf(chart.series)
588600
.map((s, si) => ({
589601
name: strName(s.name),
590602
path: `${path}.chart.series[${si}].name`,

‎packages/spec/authorable-surface/ui.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,7 @@
12601260
"ui/ReportChart:height",
12611261
"ui/ReportChart:interaction",
12621262
"ui/ReportChart:series",
1263+
"ui/ReportChart:seriesBy",
12631264
"ui/ReportChart:showDataLabels",
12641265
"ui/ReportChart:showLegend",
12651266
"ui/ReportChart:subtitle",

‎packages/spec/liveness/report.json‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,24 @@
5151
"chart": {
5252
"status": "live",
5353
"verifiedAt": "2026-09-27",
54-
"note": "objectui: DatasetReportChart plots `chart.xAxis` (dataset dimension) × `chart.yAxis` (measure) via useDatasetRows, honoring `type`/`title` (framework#1890/#3441). Supersedes the 2026-06 audit's DEAD finding. Sub-key `groupBy` stays experimental (its own describe marker) — the dataset renderer plots a single series and never reads it. Live on NON-joined reports only (#20161, re-read at the `.objectui-sha` pin f8a9d0fb): the one `report.chart` read (DatasetReportRenderer.tsx:1557) sits after the joined branch's return (:1462-1524), so on a `type: 'joined'` report the schema's refinement REFUSES `chart` rather than accepting a key nothing reads. A `joined` report draws no chart (packages/spec/src/ui/report.zod.ts, the joined-chart prescription, #20161; read point moved out of spec source, #23434): at objectui @f8a9d0fb0596 packages/plugin-report/src/DatasetReportRenderer.tsx#DatasetReportRenderer the joined branch draws each block as a `DatasetMatrixTable` / `DatasetReportTable` and returns before the one `report.chart` read, and no renderer reads a block's `chart`."
54+
"note": "objectui: DatasetReportChart plots `chart.xAxis` (dataset dimension) × `chart.yAxis` (measure) via useDatasetRows, honoring `type`/`title` (framework#1890/#3441). Supersedes the 2026-06 audit's DEAD finding. Sub-key `groupBy` stays experimental (its own describe marker) — the dataset renderer plots a single series and never reads it. Live on NON-joined reports only (#20161, re-read at the `.objectui-sha` pin f8a9d0fb): the one `report.chart` read (DatasetReportRenderer.tsx:1557) sits after the joined branch's return (:1462-1524), so on a `type: 'joined'` report the schema's refinement REFUSES `chart` rather than accepting a key nothing reads. A `joined` report draws no chart (packages/spec/src/ui/report.zod.ts, the joined-chart prescription, #20161; read point moved out of spec source, #23434): at objectui @f8a9d0fb0596 packages/plugin-report/src/DatasetReportRenderer.tsx#DatasetReportRenderer the joined branch draws each block as a `DatasetMatrixTable` / `DatasetReportTable` and returns before the one `report.chart` read, and no renderer reads a block's `chart`.",
55+
"childrenDefault": "live",
56+
"children": {
57+
"aria": {
58+
"status": "dead",
59+
"verifiedAt": "2026-10-30",
60+
"note": "REMOVED in @objectstack/spec 17 (ADR-0049 D2; the `chart-config-aria-removed` conversion): `ChartConfigSchema` tombstones it with retiredKey, so authoring `report.chart.aria` is a tsc error and a parse error carrying the prescription (use `description` on this same chart config). The row exists because the tombstone keeps the key in the walked shape now that `chart` drills; the retirement record is `dashboard.json`'s `widgets.chartConfig.aria` row."
61+
},
62+
"seriesBy": {
63+
"status": "planned",
64+
"verifiedAt": "2026-10-30",
65+
"evidenceScope": "in-repo",
66+
"evidence": "packages/spec/src/ui/report.zod.ts#ReportChartSchema (declares `seriesBy`, an optional dataset dimension name); packages/lint/src/validate-chart-bindings.ts#validateChartBindings (resolves it against the report's bound dataset dimensions exactly as `xAxis`, `chart-dimension-unknown` at error, at os validate / os build and the report publish door)",
67+
"authorWarn": true,
68+
"authorHint": "Keep it, but nothing draws it yet: the report chart still draws a single series, and the split by this dimension appears once the report renderer reads `seriesBy`.",
69+
"note": "Spec half of the report chart series split (#27974): a second dataset dimension that splits the one `yAxis` measure into a series per value, the report's spelling of a dashboard widget's two-entry `dimensions` selection. Planned because the renderer half is a follow-up: at this tree ui/packages/plugin-report/src/DatasetReportRenderer.tsx#DatasetReportChart still queries `[xAxis]` × `[yAxis]` and hands `buildChartSeries` one dimension, so the split is not drawn. The other children of `chart` inherit the row's `live` verdict through `childrenDefault`; this row exists to keep that blanket from vouching for the one key nothing reads yet. Flip to `live` with the renderer card."
70+
}
71+
}
5572
},
5673
"dateRange": {
5774
"status": "live",

‎packages/spec/liveness/state-counts/report.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them.
1212

1313
| Type | live | exp | elsewhere | dead | planned | classified |
1414
|---|---|---|---|---|---|---|
15-
| `report` | 27 | 0 | 0 | 0 | 0 | 27 |
15+
| `report` | 39 | 0 | 0 | 1 | 1 | 41 |

‎packages/spec/scripts/liveness/undrilled-containers.baseline.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@
119119
"query/groupBy",
120120
"query/orderBy",
121121
"report/blocks",
122-
"report/chart",
122+
"report/chart.annotations",
123+
"report/chart.interaction",
124+
"report/chart.series",
123125
"report/order",
124126
"skill/triggerConditions",
125127
"translation/apps",

‎packages/spec/src/ui/chart.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,12 @@ describe('批 15 — the five closed chart sites', () => {
370370

371371
it('strictness RIDES `.extend()` onto ReportChartSchema — the webhook/view trap, here on purpose', () => {
372372
// `.extend()` inherits `.strict()` AND the error map. `ReportChartSchema`
373-
// narrows xAxis/yAxis to dataset names and adds no key of its own, so the
374-
// inherited key set is exactly right — but that has to be asserted, not
375-
// assumed, because the same mechanic is what made finding 16 a finding.
373+
// narrows xAxis/yAxis to dataset names and adds one key of its own
374+
// (`seriesBy`), so the key set is the inherited one plus that — but that
375+
// has to be asserted, not assumed, because the same mechanic is what made
376+
// finding 16 a finding.
376377
expect(ReportChartSchema.safeParse({ type: 'bar', xAxis: 'dim', yAxis: 'measure' }).success, 'control').toBe(true);
378+
expect(ReportChartSchema.safeParse({ type: 'bar', xAxis: 'dim', yAxis: 'measure', seriesBy: 'dim2' }).success, 'own key').toBe(true);
377379
const msg = reject(ReportChartSchema as never, { type: 'bar', xAxis: 'd', yAxis: 'm', chartType: 'bar' });
378380
expect(msg).toContain('chartType');
379381
expect(msg, 'the base error map rides too, so the report surface gets the rename').toContain('`chartType` → `type`');

0 commit comments

Comments
 (0)