Files
astrolabe/src/core/theme-preview-specs.ts
T

284 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Theme Builder gallery specs (docs/chart-theming-scope.md §4.4).
*
* A fixed set of small, self-contained Vega-Lite specs the Theme Builder
* renders side by side with the draft config, so an edit is previewed across
* every chart surface a config styles. Every structured control should have a
* live mirror here: titles + subtitles, axes/grids/ticks, facet headers, the
* mark types (bar, line, area, point, arc), and each color family — categorical
* (`range.category`), sequential (`range.heatmap` for rect, `range.ramp` for a
* continuous legend), and diverging (`range.diverging`, a quantitative color
* scale with a `domainMid`).
*
* The specs deliberately use BARE mark strings (`mark: 'point'`, not
* `{ type: 'point', size: 80 }`): a property hard-coded in the spec overrides
* the same key in the injected config, which would make the corresponding Marks
* control a no-op in the preview. Add visual mark properties to the config (a
* theme), never inline here. Two controls have no static mirror by nature — the
* default chart SIZE (`view.continuousWidth/Height`: the cards are fixed-size
* swatches) and TOOLTIPS (`mark.tooltip`: hover-only) — and `countTitle` has
* none (no count aggregation in the samples). Inline data only, compact fixed
* sizes — swatches, not analyses.
*/
import type { JsonObject } from './spec-config';
const SCHEMA = 'https://vega.github.io/schema/vega-lite/v6.json';
export interface ThemePreviewSpec {
/** Stable key for React lists and test assertions. */
id: string;
/** What surface this card exercises (shown as the card caption). */
caption: string;
spec: JsonObject;
}
const bar: ThemePreviewSpec = {
id: 'bar',
caption: 'Bar — title, axes, grid',
spec: {
$schema: SCHEMA,
title: 'Revenue by region',
width: 200,
height: 140,
data: {
values: [
{ region: 'North', revenue: 42 },
{ region: 'South', revenue: 61 },
{ region: 'East', revenue: 28 },
{ region: 'West', revenue: 55 },
{ region: 'Central', revenue: 47 },
],
},
mark: 'bar',
encoding: {
x: { field: 'region', type: 'nominal', axis: { labelAngle: 0 } },
y: { field: 'revenue', type: 'quantitative' },
},
},
};
const line: ThemePreviewSpec = {
id: 'line',
caption: 'Line — subtitle, curve & markers',
spec: {
$schema: SCHEMA,
title: { text: 'Signups over time', subtitle: 'Weekly, by plan' },
width: 200,
height: 140,
data: {
values: [
{ week: 1, plan: 'Free', n: 20 },
{ week: 2, plan: 'Free', n: 28 },
{ week: 3, plan: 'Free', n: 26 },
{ week: 4, plan: 'Free', n: 34 },
{ week: 1, plan: 'Pro', n: 8 },
{ week: 2, plan: 'Pro', n: 11 },
{ week: 3, plan: 'Pro', n: 17 },
{ week: 4, plan: 'Pro', n: 21 },
{ week: 1, plan: 'Team', n: 3 },
{ week: 2, plan: 'Team', n: 4 },
{ week: 3, plan: 'Team', n: 9 },
{ week: 4, plan: 'Team', n: 12 },
],
},
// Bare `mark: 'line'` — the curve, stroke width, and point markers come from
// the draft config (the Marks panel's Lines & areas group), not the spec, so
// those controls have a live mirror. Same for every card below.
mark: 'line',
encoding: {
x: { field: 'week', type: 'quantitative', axis: { tickCount: 4 } },
y: { field: 'n', type: 'quantitative' },
color: { field: 'plan', type: 'nominal' },
},
},
};
const area: ThemePreviewSpec = {
id: 'area',
caption: 'Normalized area — palette, %',
spec: {
$schema: SCHEMA,
width: 200,
height: 140,
data: {
values: [
{ q: 1, channel: 'Web', v: 30 },
{ q: 2, channel: 'Web', v: 36 },
{ q: 3, channel: 'Web', v: 41 },
{ q: 4, channel: 'Web', v: 38 },
{ q: 1, channel: 'Store', v: 22 },
{ q: 2, channel: 'Store', v: 19 },
{ q: 3, channel: 'Store', v: 24 },
{ q: 4, channel: 'Store', v: 27 },
{ q: 1, channel: 'Partner', v: 12 },
{ q: 2, channel: 'Partner', v: 16 },
{ q: 3, channel: 'Partner', v: 14 },
{ q: 4, channel: 'Partner', v: 18 },
],
},
mark: 'area',
encoding: {
x: { field: 'q', type: 'quantitative', axis: { tickCount: 4 } },
// `stack: 'normalize'` makes the y-axis a 0100% scale, so its labels use
// `config.normalizedNumberFormat` — the mirror for that Formats control.
y: { field: 'v', type: 'quantitative', stack: 'normalize' },
color: { field: 'channel', type: 'nominal' },
},
},
};
const scatter: ThemePreviewSpec = {
id: 'scatter',
caption: 'Points — size, shape, gradient legend',
spec: {
$schema: SCHEMA,
width: 200,
height: 140,
data: {
values: [
{ x: 4, y: 7, z: 12 },
{ x: 8, y: 3, z: 31 },
{ x: 12, y: 11, z: 45 },
{ x: 16, y: 6, z: 22 },
{ x: 20, y: 14, z: 60 },
{ x: 24, y: 9, z: 38 },
{ x: 28, y: 17, z: 74 },
{ x: 32, y: 12, z: 51 },
{ x: 36, y: 20, z: 88 },
],
},
// Bare `mark: 'point'` — size, shape, and fill come from the Marks panel's
// Points group, so those controls have a mirror (a hard-coded size/filled
// here would shadow them).
mark: 'point',
encoding: {
x: { field: 'x', type: 'quantitative' },
y: { field: 'y', type: 'quantitative' },
color: { field: 'z', type: 'quantitative' },
},
},
};
const heatmap: ThemePreviewSpec = {
id: 'heatmap',
caption: 'Heatmap — sequential scale',
spec: {
$schema: SCHEMA,
width: 200,
height: 140,
data: {
values: ['Mon', 'Tue', 'Wed', 'Thu'].flatMap((day, d) =>
['AM', 'Noon', 'PM'].map((slot, s) => ({ day, slot, v: (d + 1) * (s + 2) * 3 })),
),
},
mark: 'rect',
encoding: {
x: { field: 'day', type: 'nominal', axis: { labelAngle: 0 } },
y: { field: 'slot', type: 'nominal' },
color: { field: 'v', type: 'quantitative' },
},
},
};
const diverging: ThemePreviewSpec = {
id: 'diverging',
caption: 'Diverging — scale around a midpoint',
spec: {
$schema: SCHEMA,
title: 'Net change by topic',
width: 200,
height: 140,
data: {
values: [
{ topic: 'Cost', delta: -8 },
{ topic: 'Speed', delta: -3 },
{ topic: 'Help', delta: 1 },
{ topic: 'Look', delta: 6 },
{ topic: 'Value', delta: 11 },
],
},
mark: 'bar',
encoding: {
x: { field: 'topic', type: 'nominal', axis: { labelAngle: 0 } },
y: { field: 'delta', type: 'quantitative' },
// `domainMid` makes this a diverging color scale, so it reads from
// `range.diverging` rather than `range.ramp`/`heatmap` (the others above).
color: {
field: 'delta',
type: 'quantitative',
scale: { domainMid: 0 },
legend: { title: null },
},
},
},
};
const donut: ThemePreviewSpec = {
id: 'donut',
caption: 'Pie & donut — palette, legend',
spec: {
$schema: SCHEMA,
width: 200,
height: 140,
data: {
values: [
{ browser: 'Firefox', share: 32 },
{ browser: 'Chrome', share: 41 },
{ browser: 'Safari', share: 18 },
{ browser: 'Other', share: 9 },
],
},
// Bare `mark: 'arc'` renders a pie; the Marks panel's Arc group (donut hole,
// corner radius, pad angle) reshapes it — so those controls have a mirror.
mark: 'arc',
encoding: {
theta: { field: 'share', type: 'quantitative' },
color: { field: 'browser', type: 'nominal' },
},
},
};
const facet: ThemePreviewSpec = {
id: 'facet',
caption: 'Facets — headers, date format',
spec: {
$schema: SCHEMA,
width: 80,
height: 110,
// Faceted by a raw temporal field (no time unit) so the header labels are
// dates formatted by `config.timeFormat` — the mirror for that Formats
// control (which does NOT affect axes, only text/legend/header labels) — on
// top of the header colour/size/weight the Headers panel styles.
data: {
values: [
{ period: '2026-01-15', team: 'Alpha', v: 14 },
{ period: '2026-01-15', team: 'Beta', v: 9 },
{ period: '2026-01-15', team: 'Gamma', v: 11 },
{ period: '2026-06-15', team: 'Alpha', v: 21 },
{ period: '2026-06-15', team: 'Beta', v: 13 },
{ period: '2026-06-15', team: 'Gamma', v: 15 },
],
},
mark: 'bar',
encoding: {
x: { field: 'team', type: 'nominal', axis: { labelAngle: 0 } },
y: { field: 'v', type: 'quantitative' },
column: { field: 'period', type: 'temporal' },
},
},
};
/** The gallery, in display order — the three continuous-color examples
* (scatter → ramp, heatmap → heatmap, diverging → diverging) sit together. */
export const THEME_PREVIEW_SPECS: ReadonlyArray<ThemePreviewSpec> = [
bar,
line,
area,
scatter,
heatmap,
diverging,
donut,
facet,
];