Theme Builder: accordion panels across the full config surface, vertical-tab nav, reflective gallery

This commit is contained in:
2026-06-21 17:14:58 +03:00
parent 318a0919c6
commit d76a7a5014
24 changed files with 2420 additions and 845 deletions
+48 -27
View File
@@ -3,12 +3,22 @@
*
* 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: titles and subtitles, axes and grids,
* facet headers, the major mark types, and — so every color control has a
* mirror — each color family: categorical (`range.category`), sequential
* (`range.heatmap` for rect, `range.ramp` for a continuous legend), and
* diverging (`range.diverging`, selected by a quantitative color scale with a
* `domainMid`). Inline data only, compact fixed sizes — swatches, not analyses.
* 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';
@@ -50,7 +60,7 @@ const bar: ThemePreviewSpec = {
const line: ThemePreviewSpec = {
id: 'line',
caption: 'Line — subtitle, series legend',
caption: 'Line — subtitle, curve & markers',
spec: {
$schema: SCHEMA,
title: { text: 'Signups over time', subtitle: 'Weekly, by plan' },
@@ -72,7 +82,10 @@ const line: ThemePreviewSpec = {
{ week: 4, plan: 'Team', n: 12 },
],
},
mark: { type: 'line', point: true },
// 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' },
@@ -83,7 +96,7 @@ const line: ThemePreviewSpec = {
const area: ThemePreviewSpec = {
id: 'area',
caption: 'Stacked area — categorical palette',
caption: 'Normalized area — palette, %',
spec: {
$schema: SCHEMA,
width: 200,
@@ -107,7 +120,9 @@ const area: ThemePreviewSpec = {
mark: 'area',
encoding: {
x: { field: 'q', type: 'quantitative', axis: { tickCount: 4 } },
y: { field: 'v', type: 'quantitative' },
// `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' },
},
},
@@ -115,7 +130,7 @@ const area: ThemePreviewSpec = {
const scatter: ThemePreviewSpec = {
id: 'scatter',
caption: 'Scatter — gradient legend',
caption: 'Points — size, shape, gradient legend',
spec: {
$schema: SCHEMA,
width: 200,
@@ -133,7 +148,10 @@ const scatter: ThemePreviewSpec = {
{ x: 36, y: 20, z: 88 },
],
},
mark: { type: 'point', filled: true, size: 80 },
// 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' },
@@ -198,7 +216,7 @@ const diverging: ThemePreviewSpec = {
const donut: ThemePreviewSpec = {
id: 'donut',
caption: 'Donut — palette, symbol legend',
caption: 'Pie & donut — palette, legend',
spec: {
$schema: SCHEMA,
width: 200,
@@ -211,7 +229,9 @@ const donut: ThemePreviewSpec = {
{ browser: 'Other', share: 9 },
],
},
mark: { type: 'arc', innerRadius: 32 },
// 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' },
@@ -221,29 +241,30 @@ const donut: ThemePreviewSpec = {
const facet: ThemePreviewSpec = {
id: 'facet',
caption: 'Facets — header labels',
caption: 'Facets — headers, date format',
spec: {
$schema: SCHEMA,
width: 70,
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: [
{ team: 'Alpha', month: 'Jan', v: 14 },
{ team: 'Alpha', month: 'Feb', v: 21 },
{ team: 'Alpha', month: 'Mar', v: 17 },
{ team: 'Beta', month: 'Jan', v: 9 },
{ team: 'Beta', month: 'Feb', v: 13 },
{ team: 'Beta', month: 'Mar', v: 19 },
{ team: 'Gamma', month: 'Jan', v: 11 },
{ team: 'Gamma', month: 'Feb', v: 8 },
{ team: 'Gamma', month: 'Mar', v: 15 },
{ 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: 'month', type: 'nominal', axis: { labelAngle: 0 } },
x: { field: 'team', type: 'nominal', axis: { labelAngle: 0 } },
y: { field: 'v', type: 'quantitative' },
column: { field: 'team', type: 'nominal' },
column: { field: 'period', type: 'temporal' },
},
},
};