Skip to content

Commit 8377332

Browse files
committed
feat(ui): add period-over-period comparison and chart series styling
1 parent 82e8a08 commit 8377332

3 files changed

Lines changed: 84 additions & 5 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,24 @@ export const ChartSeriesSchema = lazySchema(() => z.object({
124124

125125
/** Axis binding */
126126
yAxis: z.enum(['left', 'right']).default('left').describe('Bind to specific Y-Axis'),
127+
128+
/**
129+
* Series role.
130+
*
131+
* - `'primary'` (default) — normal styling using the chart palette.
132+
* - `'comparison'` — secondary period-over-period overlay; renderers
133+
* render it muted (lower opacity, dashed stroke for line/area,
134+
* lighter fill for bars) so it visually backgrounds against the
135+
* primary series. Pair with `DashboardWidget.compareTo` on data-
136+
* bound charts; for hand-authored series, set it directly.
137+
*/
138+
variant: z.enum(['primary', 'comparison']).default('primary').optional().describe('Series visual role'),
139+
140+
/** Override stroke dash pattern (e.g. "4 4" for dashed lines). */
141+
dashArray: z.string().optional().describe('SVG stroke-dasharray override'),
142+
143+
/** Override series opacity (0–1). */
144+
opacity: z.number().min(0).max(1).optional().describe('Series opacity override'),
127145
}));
128146

129147
/**

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,32 @@ export const DashboardWidgetSchema = lazySchema(() => z.object({
142142

143143
/** Data Filter (MongoDB-style FilterCondition) */
144144
filter: FilterConditionSchema.optional().describe('Data filter criteria'),
145-
145+
146+
/**
147+
* Period-over-period comparison primitive.
148+
*
149+
* When set, the renderer runs a second query against a shifted time
150+
* window and surfaces the delta (metric widgets show a secondary
151+
* value + arrow; chart widgets render a muted/dashed overlay series).
152+
*
153+
* - `'previousPeriod'` — auto-detect the comparison window from the
154+
* widget's `filter` date macros (e.g. `{current_month_start}` →
155+
* `{last_month_start}`). Falls back to no comparison when the
156+
* filter contains no resolvable date range.
157+
* - `'previousYear'` — shift the resolved filter window back by one
158+
* calendar year.
159+
* - `{ offset: '7d' | '1M' | '1y' }` — shift by an explicit
160+
* ISO-8601-like duration. Units: `d` (days), `w` (weeks),
161+
* `M` (months), `y` (years).
162+
*/
163+
compareTo: z.union([
164+
z.literal('previousPeriod'),
165+
z.literal('previousYear'),
166+
z.object({
167+
offset: z.string().regex(/^\d+[dwMy]$/, 'Offset must match <N>(d|w|M|y), e.g. "7d", "1M", "1y"'),
168+
}),
169+
]).optional().describe('Period-over-period comparison window'),
170+
146171
/** Category Field (X-Axis / Group By) */
147172
categoryField: z.string().optional().describe('Field for grouping (X-Axis)'),
148173

skills/objectstack-ui/SKILL.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,12 +450,25 @@ export const SalesDashboard: Dashboard = {
450450
filter: { stage: { $nin: ['closed_won', 'closed_lost'] } },
451451
valueField: 'amount', aggregate: 'sum',
452452
layout: { x: 0, y: 0, w: 3, h: 2 },
453-
options: {
454-
icon: 'DollarSign', format: '0,0',
455-
trend: { value: 8.4, direction: 'up', label: 'vs last quarter' },
456-
},
453+
options: { icon: 'DollarSign', format: '0,0' },
454+
// Period-over-period: renderer fetches the prior quarter and
455+
// surfaces a secondary value + delta arrow automatically.
456+
compareTo: 'previousPeriod',
457457
actionType: 'url', actionUrl: '/objects/opportunity?filter=open',
458458
},
459+
460+
// Chart widget with comparison overlay (M2). The renderer issues a
461+
// second query with the time window shifted by `compareTo` and
462+
// overlays it as a muted/dashed series.
463+
{
464+
id: 'revenue_vs_last_year', type: 'line',
465+
title: 'Revenue — This Year vs Last',
466+
object: 'order',
467+
filter: { closed_at: { $gte: '{current_year_start}', $lte: '{current_year_end}' } },
468+
categoryField: 'closed_at', valueField: 'total', aggregate: 'sum',
469+
compareTo: 'previousYear',
470+
layout: { x: 3, y: 0, w: 9, h: 4 },
471+
},
459472
],
460473
};
461474
```
@@ -465,6 +478,29 @@ export const SalesDashboard: Dashboard = {
465478
> The full list of supported date placeholders is documented in
466479
> [Date Macros](#date-macros--filter-placeholders) below.
467480
481+
### Period-over-period — `compareTo`
482+
483+
Set `compareTo` on any data-bound widget to add a second query against a
484+
shifted time window. The renderer derives the comparison automatically;
485+
no second `filter` is required.
486+
487+
| Value | Behaviour |
488+
|:--|:--|
489+
| `'previousPeriod'` | Inspect the widget `filter` for date-macro tokens (`{current_month_start}`, `{last_7_days}`, …) and shift the window back by one period of the same kind. |
490+
| `'previousYear'` | Shift the resolved filter window back by one calendar year. |
491+
| `{ offset: '7d' }` | Shift by an explicit duration. Units: `d` (days), `w` (weeks), `M` (months), `y` (years). |
492+
493+
* **Metric widgets** — the prior-period value renders as a small caption
494+
beneath the headline number, alongside a green/red delta arrow.
495+
Authors should *not* hand-author `options.trend` when `compareTo` is
496+
set; the renderer wins and overwrites it.
497+
* **Chart widgets** — the comparison series is appended after the
498+
primary series with `variant: 'comparison'` and styled as a muted /
499+
dashed overlay. Single-series charts (bar / line / area) get an
500+
automatic second series; multi-series pivots are not yet supported.
501+
* **Requirements**`compareTo` is a no-op when the filter contains no
502+
resolvable date macros and no global `dateRange` is configured.
503+
468504
---
469505

470506
## Date Macros — Filter Placeholders

0 commit comments

Comments
 (0)