Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/28206-report-chart-series-by-renderer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/plugin-report': patch
'@objectstack/spec': patch
'@objectstack/lint': patch
---

feat(plugin-report): a report chart draws its `seriesBy` split

Clause-②: no
Metadata-protocol: none

A report chart that sets `chart.seriesBy` now draws the split it declares: one bar per `xAxis` value, split into one series per value of the `seriesBy` dimension (a bar per owner, split by stage). The chart's own dataset query asks for `[xAxis, seriesBy]` × `[yAxis]` and the rows are pivoted the way a dashboard widget with two dimensions pivots them, null group included. The series follow the split dimension's declared option order, and a `chart.series[]` entry whose `name` is a split value lands its colour and label on that series. Before this, the key was accepted and checked at publish, but the chart still drew a single series.

- **Liveness.** The `chart.seriesBy` row of the report ledger is graded `live` with the renderer as its evidence, so `os validate` no longer prints the "nothing draws it yet" author warning for it.
- **Lint.** `os explain chart-axis-not-selected` now says the report chart queries its `xAxis` dimension and, when set, its `seriesBy` split. No rule, severity or finding changes.

Nothing to do for existing reports. A chart without `seriesBy` queries and draws exactly as before.
5 changes: 3 additions & 2 deletions packages/lint/src/rule-explanations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3302,8 +3302,9 @@ const CHART_AXIS_NOT_SELECTED_EXPLANATION: RuleExplanation = {
'chart drawn is unaffected. Advisory: the selection may legitimately be widened at runtime. A ' +
'query position is never reported: it IS the selection, or the query itself.',
'The selection is per surface. On a page chart it is `values`, the measure set the query asks ' +
'for. On a report it is the chart\'s own `chart.yAxis`: the embedded chart queries ' +
'exactly one dimension × one measure and derives ONE series, while `report.values` is the ' +
'for. On a report it is the chart\'s own `chart.yAxis`: the embedded chart queries its ' +
'`chart.xAxis` dimension (and its `chart.seriesBy` split, when set) × that one measure and, ' +
'without a split, derives ONE series, while `report.values` is the ' +
'selection of the table beneath it. So a report\'s `chart.yAxis` is never reported — a chart ' +
'cannot fail to select what it queries — and a `chart.series[].name` override lands only when ' +
'it names `chart.yAxis`. With `chart.seriesBy` set the chart derives one series per value of ' +
Expand Down
29 changes: 17 additions & 12 deletions packages/lint/src/validate-chart-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@
* the three surfaces do not agree, so the tier and the consequence are per
* POSITION rather than per rule:
*
* - **Report charts.** The chart runs its OWN query out of the two axis
* strings — `useDatasetRows(dataset, [xAxis], [yAxis], …)` in
* - **Report charts.** The chart runs its OWN query out of the axis
* strings — `useDatasetRows(dataset, [xAxis], [yAxis], …)`, or
* `[xAxis, seriesBy]` × `[yAxis]` with a split, in
* `plugin-report/src/DatasetReportRenderer.tsx`, and in that file's own
* words *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*.
* words *"the embedded chart queries only `chart.xAxis` (with
* `chart.seriesBy`) × `chart.yAxis`"*.
* So `chart.yAxis` IS the binding and keeps `error`. `chart.series[]` is
* not: it is *"The author's per-chart override for ONE measure's display
* name — the entry of `chart.series[]` whose `name` IS that measure"*,
Expand Down Expand Up @@ -117,13 +119,15 @@
*
* The consequence is per POSITION; the selection it is measured against is per
* SURFACE — and on the report surface that selection is not `report.values`.
* At the same pinned revision `DatasetReportRenderer.tsx` runs
* `useDatasetRows(dataset, plan.kind === 'series' && xAxis ? [xAxis] : [],
* wantsQuery && yAxis ? [yAxis] : [], …)` and derives the plotted series with
* `buildChartSeries(…, [xAxis], [yAxis], …)` — *"the selection is exactly one
* dimension × one measure, so this takes the helper's single-dimension branch
* and returns ONE series"*. `report.values` is the selection of the TABLE
* beneath the chart; the chart's own is the axis pair. Two things follow:
* `DatasetReportRenderer.tsx` queries `[xAxis]` (`[xAxis, seriesBy]` with a
* split) × `[yAxis]` through `useDatasetRows` and derives the plotted series
* with `buildChartSeries(…, queryDimensions, [yAxis], …)` — *"Without
* `seriesBy` the selection is one dimension × one
* measure, so this takes the helper's single-dimension branch and returns ONE
* series"*; with it, one series per `seriesBy` value, whose override names a
* value and is not judged here (below). `report.values` is the selection of
* the TABLE beneath the chart; the chart's own is the axis pair. Two things
* follow:
*
* - **Nothing to report at the report `chart.yAxis`.** That position IS the
* chart's query, so it cannot fail to select itself. The warning that fired
Expand Down Expand Up @@ -293,8 +297,9 @@ interface ChartBinding {
/**
* The measures THIS chart's own query selects, when that is narrower than
* the `values` limb above (#15734). Set on the report surface and nowhere
* else: the embedded report chart queries `chart.xAxis` × `chart.yAxis`
* alone and derives exactly ONE series from it, while `report.values` is the
* else: the embedded report chart queries `chart.xAxis` (with its
* `chart.seriesBy` split, when set) × `chart.yAxis` alone and, without a
* split, derives exactly ONE series from it, while `report.values` is the
* selection of the table beneath it. `chart-axis-not-selected` resolves
* against this set where it is present — which is both why a `series[].name`
* override is measured against the singleton `{ chart.yAxis }`, and why the
Expand Down
12 changes: 5 additions & 7 deletions packages/spec/liveness/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"chart": {
"status": "live",
"verifiedAt": "2026-09-27",
"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`.",
"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. With `chart.seriesBy` set it also groups by that second dimension and draws a series per value of it (the `seriesBy` row below). 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`.",
"childrenDefault": "live",
"children": {
"aria": {
Expand All @@ -60,13 +60,11 @@
"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."
},
"seriesBy": {
"status": "planned",
"verifiedAt": "2026-10-30",
"status": "live",
"verifiedAt": "2026-10-31",
"evidenceScope": "in-repo",
"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)",
"authorWarn": true,
"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`.",
"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."
"evidence": "ui/packages/plugin-report/src/DatasetReportRenderer.tsx#DatasetReportChart (reads `chart.seriesBy`, queries `[xAxis, seriesBy]` × `[yAxis]` and pivots the rows through `buildChartSeries` into one series per value); ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.chartSeriesBy-28206.test.tsx; 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)",
"note": "The report chart series split (#27974 spec half, #28206 renderer half): 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. LIVE: DatasetReportChart widens its own query to `[xAxis, seriesBy]` × `[yAxis]` and hands both dimensions to `buildChartSeries` (ui/packages/core/src/utils/chart-series.ts), whose pivot branch draws one row per x bucket and one series per `seriesBy` value, the null group included; the series follow the split dimension's declared option order, and a `series[]` entry whose `name` is a split value lands its presentation on that series. Without `seriesBy` the chart queries and draws exactly what it did before. The other children of `chart` inherit the row's `live` verdict through `childrenDefault`; this row stays to carry the reader as evidence."
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/state-counts/report.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them.

| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `report` | 39 | 0 | 0 | 1 | 1 | 41 |
| `report` | 40 | 0 | 0 | 1 | 0 | 41 |
Loading
Loading