Skip to content

Commit b9062c9

Browse files
os-zhuangclaude
andauthored
feat(spec): Report matrix columns (across) + drilldown flag (ADR-0021 D2) (#1732)
A matrix report now pivots `rows` (down) × `columns` (across dimension names) with `values` in the cells — the pivot triple ADR-0021 D2 specified; the flattened rows-only stopgap retires. Joined blocks gain the same `columns` field. `drilldown` (boolean, default true) declares click-through from an aggregated row/cell to the underlying records; the host resolves the dataset's object and dimension→field mapping. reportForm surfaces both in the Dataset binding section (columns gated on type == 'matrix'); the showcase matrix fixture moves to the true pivot form. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent de5f475 commit b9062c9

5 files changed

Lines changed: 35 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
ADR-0021 D2: `Report` gains `columns` (dimension names across — a `matrix` report pivots `rows` × `columns` with `values` in the cells; also on joined blocks) and `drilldown` (boolean, default `true` — click an aggregated row/cell to open the underlying records). `reportForm` surfaces both in the Dataset binding section (`columns` visible for matrix only).

examples/app-showcase/src/reports/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ export const StatusPriorityMatrixReport: Report = {
2727
label: 'Status × Priority (Matrix)',
2828
description: 'Task counts cross-tabulated by status and priority.',
2929
type: 'matrix',
30-
// ADR-0021 Phase 2 — dataset binding (dual-form). Matrix flattens rows+across
31-
// into `rows` for now (cell values identical); across-dimension is a follow-up.
30+
// ADR-0021 D2 — true pivot: `rows` down × `columns` across, measures in cells.
3231
dataset: 'showcase_task_metrics',
33-
rows: ['status', 'priority'],
32+
rows: ['status'],
33+
columns: ['priority'],
3434
values: ['est_hours'],
3535
};
3636

packages/spec/src/ui/report.form.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export const reportForm = defineForm({
3939
{ field: 'dataset', widget: 'ref:dataset', helpText: 'Dataset to bind (measures/dimensions come from its semantic layer)' },
4040
{ field: 'values', widget: 'string-tags', helpText: 'Measure names (from the dataset) to display' },
4141
{ field: 'rows', widget: 'string-tags', helpText: 'Dimension names (from the dataset) to group rows by' },
42+
// CEL visibility — only Matrix reports pivot across a second dimension.
43+
{ field: 'columns', widget: 'string-tags', visibleOn: "data.type == 'matrix'", helpText: 'Dimension names across (matrix only)' },
44+
{ field: 'drilldown', helpText: 'Click an aggregated row/cell to open the underlying records' },
4245
],
4346
},
4447
{

packages/spec/src/ui/report.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,24 @@ describe('ReportSchema (dataset-bound)', () => {
2525
expect(r.rows).toEqual(['stage']);
2626
});
2727

28-
it('accepts a matrix report (rows = down × across, flattened) + runtimeFilter', () => {
28+
it('accepts a matrix report (rows down × columns across) + runtimeFilter', () => {
2929
const r = ReportSchema.parse({
3030
name: 'hours_matrix', label: 'Hours', type: 'matrix',
31-
dataset: 'tasks', rows: ['owner', 'category'], values: ['est_hours', 'actual_hours'],
31+
dataset: 'tasks', rows: ['owner'], columns: ['category'], values: ['est_hours', 'actual_hours'],
3232
runtimeFilter: { is_completed: true },
3333
});
34-
expect(r.rows).toHaveLength(2);
34+
expect(r.rows).toEqual(['owner']);
35+
expect(r.columns).toEqual(['category']);
3536
expect(r.runtimeFilter).toEqual({ is_completed: true });
3637
});
3738

39+
it('drilldown defaults on and can be disabled', () => {
40+
const on = ReportSchema.parse({ name: 'r1', label: 'R', type: 'summary', dataset: 'sales', rows: ['stage'], values: ['revenue'] });
41+
expect(on.drilldown).toBe(true);
42+
const off = ReportSchema.parse({ name: 'r2', label: 'R', type: 'summary', dataset: 'sales', rows: ['stage'], values: ['revenue'], drilldown: false });
43+
expect(off.drilldown).toBe(false);
44+
});
45+
3846
it('accepts an embedded chart', () => {
3947
const r = ReportSchema.parse({
4048
name: 'rep_x', label: 'R', type: 'summary', dataset: 'sales', rows: ['stage'], values: ['revenue'],

packages/spec/src/ui/report.zod.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export const JoinedReportBlockSchema: z.ZodTypeAny = lazySchema(() => z.object({
9191
dataset: SnakeCaseIdentifierSchema.optional().describe('Dataset name to bind (ADR-0021)'),
9292
/** Dimension names (from the dataset) to group rows by. Dataset-bound only. */
9393
rows: z.array(z.string()).optional().describe('Dimension names down (dataset-bound)'),
94+
/** Dimension names across — matrix blocks pivot rows × columns (ADR-0021 D2). */
95+
columns: z.array(z.string()).optional().describe('Dimension names across (matrix, dataset-bound)'),
9496
/** Measure names (from the dataset) to display. Dataset-bound only. */
9597
values: z.array(z.string()).optional().describe('Measure names to show (dataset-bound)'),
9698
/** Render-time scope filter, ANDed at query time. Dataset-bound only. */
@@ -121,10 +123,21 @@ export const ReportSchema = lazySchema(() => z.object({
121123
dataset: SnakeCaseIdentifierSchema.optional().describe('Dataset name to bind (ADR-0021)'),
122124
/** Dimension names (from the dataset) to group rows by (down axis). */
123125
rows: z.array(z.string()).optional().describe('Dimension names down'),
126+
/**
127+
* Dimension names across (ADR-0021 D2) — a `matrix` report pivots
128+
* `rows` × `columns` with `values` in the cells. Ignored for other types.
129+
*/
130+
columns: z.array(z.string()).optional().describe('Dimension names across (matrix)'),
124131
/** Measure names (from the dataset) to display. */
125132
values: z.array(z.string()).optional().describe('Measure names to show'),
126133
/** Render-time scope filter, ANDed at query time. */
127134
runtimeFilter: FilterConditionSchema.optional().describe('Render-time scope filter'),
135+
/**
136+
* ADR-0021 D2 — click an aggregated row/cell to open the underlying
137+
* records (dataset-backed; the host resolves the dataset's object and
138+
* dimension→field mapping). Default on; set `false` to disable.
139+
*/
140+
drilldown: z.boolean().default(true).describe('Click-through to underlying records'),
128141

129142
/** Visualization */
130143
chart: ReportChartSchema.optional().describe('Embedded chart configuration'),

0 commit comments

Comments
 (0)