Skip to content

Commit abab018

Browse files
os-zhuangclaude
andcommitted
docs: align UI skill + analytics guides with ADR-0021 + ChartType trim
Hand-written docs still showed the pre-cutover inline form and the removed chart-type variants. Brings them in line with the shipped state (the generated references already regenerated per-PR). - objectstack-ui/SKILL.md: dashboard/report/compareTo examples → dataset form (`dataset` + `dimensions`/`rows` + `values`); the `categoryGranularity` section rewritten for the dataset dimension's `dateGranularity`; prose/tables that referenced `object`/`valueField`/`aggregate`/`groupingsDown`/ `groupingsAcross` updated. - analytics-datasets.mdx: new "Display values are resolved server-side" note — select labels, lookup names, date bucket labels, and measure label/format on the result fields. - airtable-dashboard-analysis.mdx: a "historical snapshot — partly superseded" banner; the grouped-bar/stacked-bar "renderer pending" conclusions were superseded by removing them from ChartTypeSchema (not implementing them). Docs build green (1098 pages). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent be11326 commit abab018

3 files changed

Lines changed: 106 additions & 67 deletions

File tree

content/docs/guides/airtable-dashboard-analysis.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ description: Comprehensive gap analysis comparing ObjectStack Dashboard protocol
1010
> **Scope:** Protocol enhancements (spec) + Component implementations (objectui)
1111
> **Related Issues:** [#712](https://github.com/objectstack-ai/spec/issues/712), [#713](https://github.com/objectstack-ai/spec/issues/713), [#714](https://github.com/objectstack-ai/spec/issues/714), [objectui#585](https://github.com/objectstack-ai/objectui/issues/585), [objectui#586](https://github.com/objectstack-ai/objectui/issues/586), [objectui#587](https://github.com/objectstack-ai/objectui/issues/587), [objectui#588](https://github.com/objectstack-ai/objectui/issues/588)
1212
13+
<Callout type="warn" title="Historical snapshot — partly superseded">
14+
This is a point-in-time benchmark (Feb 2026). Two later decisions supersede parts
15+
of it; treat the **Analytics Datasets** guide and the generated **UI references**
16+
as the current source of truth:
17+
18+
- **ADR-0021 single-form** removed the inline widget/report query
19+
(`object` / `valueField` / `aggregate` / `categoryField` / `categoryGranularity`,
20+
report `columns` / `groupings`). Widgets and reports now bind a semantic
21+
**`dataset`** and select dimensions/measures by name. The inline-form examples
22+
below are illustrative of the original analysis, not current authoring.
23+
- **`ChartTypeSchema` was trimmed** to families the renderer draws distinctly.
24+
The `grouped-bar` / `stacked-bar` (and `bi-polar-bar` / `stacked-area` /
25+
`step-line` / `spline` / `pyramid` / `bubble`) types this doc lists as
26+
"defined, renderer pending" were **removed** rather than implemented — a type
27+
that only renders as its base chart is no longer advertised. They can return
28+
via an opt-in renderer once a real renderer + data model backs them.
29+
</Callout>
30+
1331
---
1432

1533
## Table of Contents

content/docs/guides/analytics-datasets.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,22 @@ POST {basePath}/analytics/dataset/query
149149
The same governed path backs the Studio dataset preview, dashboard widgets, and
150150
dataset-bound reports — so the numbers match everywhere.
151151

152+
### Display values are resolved server-side
153+
154+
The query result is presentation-ready — authors do not format dimension or
155+
measure values by hand:
156+
157+
- **Dimensions** — a `select` dimension returns its option **label** (not the
158+
stored value), a `lookup` / `master_detail` dimension returns the related
159+
record's **display name** (not the FK id), and a `date` dimension with a
160+
`dateGranularity` returns a human bucket label (`month``2026-04`,
161+
`quarter``2026-Q2`, `year``2026`). Unresolved values pass through
162+
unchanged, never blank.
163+
- **Measures** — each measure column carries its `label` and `format` on the
164+
result `fields`, so a KPI or chart legend reads "Total Spent / $616,000"
165+
rather than "spent_sum / 616000". The renderer applies the `format` at display
166+
time (it can't be baked into the numeric row value charts plot).
167+
152168
## Migrating from inline queries
153169

154170
ADR-0021's terminal state is **one** author-facing shape. The migration runs in

skills/objectstack-ui/SKILL.md

Lines changed: 72 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,8 @@ an `aggregate` measure or chart spec, and a `layout: {x,y,w,h}`.
349349
See the **Production Pattern** section below for the full
350350
`Dashboard` shape with `refreshInterval`, header actions, date range,
351351
global filters, widget options, and the period-over-period (`compareTo`)
352-
/ date-bucketing (`categoryGranularity`) modifiers available on every
353-
data-bound widget.
352+
modifier; date bucketing comes from the bound dataset dimension's
353+
`dateGranularity` (ADR-0021).
354354

355355
### Dataset-Bound Widgets
356356

@@ -403,27 +403,29 @@ filter; keep `filter` on the widget when binding a dataset.
403403
```typescript
404404
import type { ReportInput } from '@objectstack/spec/ui';
405405

406+
// ADR-0021: a report binds a `dataset` and selects `rows` (dimensions) +
407+
// `values` (measures) BY NAME. The `opportunity_metrics` dataset defines the
408+
// object, the `amount_sum` measure, and the `forecast_category` + `close_date`
409+
// (dateGranularity: 'quarter') dimensions — see Guides → Analytics Datasets.
406410
export const PipelineCoverageReport: ReportInput = {
407411
name: 'pipeline_coverage_by_quarter',
408412
label: 'Pipeline Coverage (Quarter)',
409-
objectName: 'opportunity',
410413
type: 'matrix',
411-
columns: [
412-
{ field: 'name', label: 'Opportunity' },
413-
{ field: 'amount', label: 'Amount', aggregate: 'sum' },
414-
],
415-
groupingsDown: [{ field: 'forecast_category', sortOrder: 'asc' }],
416-
groupingsAcross: [{ field: 'close_date', dateGranularity: 'quarter' }],
417-
filter: { stage: { $ne: 'closed_lost' } },
418-
chart: { type: 'bar', xAxis: 'forecast_category', yAxis: 'amount' },
414+
dataset: 'opportunity_metrics',
415+
rows: ['forecast_category', 'close_date'],
416+
values: ['amount_sum'],
417+
runtimeFilter: { stage: { $ne: 'closed_lost' } },
418+
chart: { type: 'bar', xAxis: 'forecast_category', yAxis: 'amount_sum' },
419419
};
420420
```
421421

422-
> **`dateGranularity`** on a grouping (`day | week | month | quarter | year`)
423-
> tells the server to bucket date fields in a single aggregate query — do
424-
> **not** pre-compute virtual columns for this.
425-
> **`groupingsDown`** drives row groupings (summary). Add `groupingsAcross`
426-
> to upgrade to a matrix. Multi-level grouping = multiple entries in the array.
422+
> **`dateGranularity`** lives on the dataset's date **dimension**
423+
> (`day | week | month | quarter | year`); selecting that dimension buckets the
424+
> field server-side in a single aggregate query — do **not** pre-compute virtual
425+
> columns for this.
426+
> **`rows`** are the report's grouping dimensions (selected from the dataset by
427+
> name). A `summary` groups by them; a `matrix` cross-tabs them. Multi-level
428+
> grouping = multiple dimension names in the array.
427429
428430
---
429431

@@ -537,7 +539,7 @@ Use this CRM-style structure as the canonical UI assembly reference:
537539
| **Public / anonymous form** | `src/views/*.view.ts` (formView with `sharing.allowAnonymous: true`) | Web-to-Lead / Web-to-Case. Auto-exposed at `GET/POST /api/v1/forms/:slug`. See `guides/public-forms.mdx` |
538540
| App navigation | `src/apps/*.app.ts` | Use grouped nav trees, `viewName` shortcuts, and `requiresObject` for capability-aware visibility |
539541
| Dashboards | `src/dashboards/*.dashboard.ts` | Combine KPI + chart + table widgets with shared `dateRange` and `globalFilters` |
540-
| Reports | `src/reports/*.report.ts` | Prefer `groupingsDown` + `groupingsAcross` + `dateGranularity` for matrix/summary analytics |
542+
| Reports | `src/reports/*.report.ts` | Bind a `dataset` + `rows` (dimensions) + `values` (measures) for tabular/summary/matrix/joined analytics |
541543
| Record pages | `src/pages/*.page.ts` | Compose `regions` + components (`page:header`, `record:highlights`, related lists, tabs) |
542544
| User actions | `src/actions/*.actions.ts` | Use `flow` for orchestration and `modal` for parameterized bulk mutations |
543545

@@ -559,8 +561,8 @@ ObjectUI capabilities exist and prefer the protocol-native shape:
559561
| Line-item grids | Spreadsheet editing, computed cells, ghost row, lookup auto-fill, duplicate, drag reorder, subtotal/tax/total | Keep line fields on the child object; use `position`/`sort_order` and summary fields |
560562
| Record detail | Derived related lists, action slots, system/audit sections, record-page assignment, optional reference rail | Let relationships derive related lists unless a record page needs bespoke placement |
561563
| Pages | Page create flows, block canvas, slotted record pages, block property inspectors, nested container blocks | Use Page metadata for layout; use full Action objects in `page:header.properties.actions` |
562-
| Dashboards | Metric/chart/list/pivot/funnel/table widgets, drill-downs, type-aware cells, date bucketing, dataset-bound widgets | Prefer dataset-bound widgets for governed metrics; inline object widgets remain valid |
563-
| Reports | Spec-native tabular/summary/matrix/joined reports, chart/KPI blocks, drill-downs, dataset-bound reports | Prefer `groupingsDown` / `groupingsAcross` / `dataset` over ad hoc client shaping |
564+
| Dashboards | Metric/chart/list/pivot/funnel/table widgets, drill-downs, type-aware cells, date bucketing, dataset-bound widgets | Bind every widget to a `dataset` + `values` (+ `dimensions`); the inline object/valueField/aggregate form was removed (ADR-0021) |
565+
| Reports | Spec-native tabular/summary/matrix/joined reports, chart/KPI blocks, drill-downs, dataset-bound reports | Bind a `dataset` + `rows` + `values`; joined reports carry dataset-bound `blocks` |
564566
| Actions | Row/global/header actions, modal parameter collection, visible CEL, popup-safe opens, nested action runner sharing | Define actions as metadata; use row context/defaultFromRow instead of custom code |
565567
| Flow designer | Typed node config panels, trigger/decision forms, reference pickers, simulator/debug runner | Author flows with typed config, not advanced JSON fallbacks |
566568
| Console utilities | Integrations & APIs, public forms, flow runs, approvals inbox, settings, marketplace/package management, AI draft review/publish | Link app navigation to these surfaces with capability gates where appropriate |
@@ -603,15 +605,18 @@ export const SalesDashboard: Dashboard = {
603605
optionsFrom: { object: 'user', valueField: 'id', labelField: 'name' } },
604606
],
605607

608+
// ADR-0021: widgets bind a semantic `dataset` and select dimensions/measures
609+
// BY NAME (the `opportunity_metrics` / `order_metrics` datasets define the base
610+
// object, measures, and date dimensions — see Guides → Analytics Datasets). The
611+
// widget `filter` is the presentation-scope runtimeFilter.
606612
widgets: [
607613
{
608614
id: 'total_pipeline_value', type: 'metric',
609615
title: 'Total Pipeline',
610-
object: 'opportunity',
616+
dataset: 'opportunity_metrics', values: ['total_amount'],
611617
filter: { stage: { $nin: ['closed_won', 'closed_lost'] } },
612-
valueField: 'amount', aggregate: 'sum',
613618
layout: { x: 0, y: 0, w: 3, h: 2 },
614-
options: { icon: 'DollarSign', format: '0,0' },
619+
options: { icon: 'DollarSign' }, // the measure's own `format` drives the number
615620
// Period-over-period: renderer fetches the prior quarter and
616621
// surfaces a secondary value + delta arrow automatically.
617622
compareTo: 'previousPeriod',
@@ -620,13 +625,13 @@ export const SalesDashboard: Dashboard = {
620625

621626
// Chart widget with comparison overlay (M2). The renderer issues a
622627
// second query with the time window shifted by `compareTo` and
623-
// overlays it as a muted/dashed series.
628+
// overlays it as a muted/dashed series. The date axis is a dataset
629+
// dimension whose monthly bucketing lives on the dataset (`dateGranularity`).
624630
{
625631
id: 'revenue_vs_last_year', type: 'line',
626632
title: 'Revenue — This Year vs Last',
627-
object: 'order',
633+
dataset: 'order_metrics', dimensions: ['closed_at'], values: ['total_sum'],
628634
filter: { closed_at: { $gte: '{current_year_start}', $lte: '{current_year_end}' } },
629-
categoryField: 'closed_at', valueField: 'total', aggregate: 'sum',
630635
compareTo: 'previousYear',
631636
layout: { x: 3, y: 0, w: 9, h: 4 },
632637
},
@@ -670,63 +675,63 @@ no second `filter` is required.
670675
date-bound clauses.
671676

672677
```typescript
673-
// Metric — WoW delta
674-
{ id: 'done_this_week', type: 'metric', object: 'task',
678+
// Metric — WoW delta (binds the task_metrics dataset; filter = runtimeFilter)
679+
{ id: 'done_this_week', type: 'metric', dataset: 'task_metrics', values: ['task_count'],
675680
filter: { assignee: '{current_user_id}', status: 'done',
676681
completed_at: { $gte: '{week_start}' } },
677-
aggregate: 'count',
678682
compareTo: 'previousPeriod' }
679683

680684
// Bar — YoY overlay on a stable category set
681-
{ id: 'headcount_by_dept', type: 'bar', object: 'employee',
685+
{ id: 'headcount_by_dept', type: 'bar', dataset: 'employee_metrics',
686+
dimensions: ['department'], values: ['headcount'],
682687
filter: { status: { $ne: 'terminated' } },
683-
aggregate: 'count', categoryField: 'department',
684688
compareTo: 'previousYear' }
685689
```
686690

687-
### Server-side date bucketing — `categoryGranularity`
688-
689-
For any chart with `categoryField` pointing at a date/datetime field, set
690-
`categoryGranularity` to bucket values server-side. Without it every
691-
distinct timestamp becomes its own category, which collapses a 12-row
692-
seed dataset into a 12-point flat-line chart.
691+
### Server-side date bucketing — `dateGranularity` (ADR-0021)
693692

694-
| Value | Bucket key |
695-
|:--|:--|
696-
| `'day'` | Calendar day (`YYYY-MM-DD`) |
697-
| `'week'` | ISO week (`YYYY-Www`) |
698-
| `'month'` | Calendar month (`YYYY-MM`) |
699-
| `'quarter'` | Calendar quarter (`YYYY-Qn`) |
700-
| `'year'` | Calendar year (`YYYY`) |
701-
702-
* **Engine support** — Postgres `date_trunc`, MySQL `date_format`, SQLite
703-
`strftime`, MongoDB `$dateTrunc`, in-memory `bucketDateValue` fallback.
704-
All emitted by the analytics service, not the client.
705-
* **Rule of thumb**`day` for ≤30d windows, `week` for ~90d, `month`
706-
for 6–12 months, `quarter` for multi-year, `year` for retention /
707-
compliance scopes.
708-
* **Combines with `compareTo`** — the comparison query is issued with the
709-
same granularity, so the muted overlay aligns bucket-for-bucket.
710-
* **xAxis format** — pair with a matching `chartConfig.xAxis.format`
711-
(`%b %d` for day, `%b %Y` for month, etc.) so the rendered labels
712-
match the bucket grain.
693+
Date bucketing lives on the **dataset dimension**, not the widget. Give a date
694+
dimension a `dateGranularity` and any presentation that selects it groups by that
695+
bucket server-side — without it every distinct timestamp becomes its own
696+
category, collapsing a 12-row seed into a 12-point flat line. (The old widget-level
697+
`categoryGranularity` was removed in the single-form cutover.)
713698

714699
```typescript
715-
// Line chart — monthly trend with YoY overlay
716-
{ id: 'signed_by_month', type: 'line', object: 'contract',
700+
// In the dataset (Guides → Analytics Datasets):
701+
defineDataset({
702+
name: 'contract_metrics', object: 'contract',
703+
dimensions: [{ name: 'signed_date', field: 'signed_date', type: 'date', dateGranularity: 'month' }],
704+
measures: [{ name: 'signed_count', aggregate: 'count' }],
705+
});
706+
707+
// The widget just selects the dimension by name:
708+
{ id: 'signed_by_month', type: 'line',
709+
dataset: 'contract_metrics', dimensions: ['signed_date'], values: ['signed_count'],
717710
filter: { signed_date: { $gte: '{12_months_ago}' } },
718-
aggregate: 'count',
719-
categoryField: 'signed_date',
720-
categoryGranularity: 'month',
721-
compareTo: 'previousYear',
722-
chartConfig: {
723-
type: 'line',
724-
xAxis: { field: 'signed_date', format: '%b %Y' },
725-
yAxis: [{ field: 'value', format: '0,0' }],
726-
},
727-
}
711+
compareTo: 'previousYear' }
728712
```
729713

714+
| `dateGranularity` | Rendered bucket label |
715+
|:--|:--|
716+
| `'day'` | `YYYY-MM-DD` |
717+
| `'week'` | ISO date of the bucket (`YYYY-MM-DD`) |
718+
| `'month'` | `YYYY-MM` |
719+
| `'quarter'` | `YYYY-Qn` |
720+
| `'year'` | `YYYY` |
721+
722+
* **Engine support** — Postgres `date_trunc`, MySQL `date_format`, SQLite
723+
`strftime`, MongoDB `$dateTrunc`, in-memory fallback. All emitted by the
724+
analytics service, not the client.
725+
* **Human labels are automatic** — the analytics layer formats the bucket value
726+
to the label above, and resolves `select`/`lookup` dimension values to their
727+
option label / related-record name. Measures carry their `label` + `format`
728+
(e.g. `$0,0`) so KPIs and legends read "Total Spent / $616,000", not
729+
"spent_sum / 616000". Authors do not format dimension/measure values by hand.
730+
* **Combines with `compareTo`** — the comparison query is issued with the same
731+
granularity, so the muted overlay aligns bucket-for-bucket.
732+
* **Rule of thumb**`day` for ≤30d windows, `week` for ~90d, `month` for
733+
6–12 months, `quarter` for multi-year, `year` for retention / compliance.
734+
730735
---
731736

732737
## Date Macros — Filter Placeholders

0 commit comments

Comments
 (0)