Chart Gallery

The specimens below read daily net revenue from the orders mart (staging dataset dev_matt) — a real single-series time series, refreshed on every rebuild.

The bar specimens read net revenue by acquisition channel from the same orders mart — a real single-series categorical comparison, the shape a bar chart is for.

The area specimens read cumulative net revenue by acquisition channel group from the same orders mart — a real composition-over-time, the shape a stacked area is for. The lone-area specimen derives the total (sum across groups) from the same source.

The stacked-bar specimens read net revenue by acquisition channel group split by order type (One-off vs Subscription) from the same orders mart — a real multi-series categorical composition, the shape a stacked (or grouped) bar is for.

The donut specimens read net revenue by acquisition channel group (one row per group) from the same orders mart — a real part-to-whole of a handful of slices, the shape a donut is for.

The KpiCard specimens read a headline summary from the same orders mart — the recent half of the sample window against the prior half — so the delta pill shows a real period-over-period movement. Its inline micro-trend reads the daily series (daily_rev) above.

The Sparkline specimens read the daily net-revenue series per acquisition channel from the same orders mart — a real per-channel micro-trend, the shape an in-table sparkline is for. The page groups the rows by channel into the array each Sparkline reads.

The S3DataTable specimen reads per-channel order economics from the same orders mart — order count, net revenue, average order value and each channel's share of total revenue — a real multi-column tabular detail view, the shape a data table (not a chart) is for.

The wide/frozen specimen reads the same per-channel grain widened to ~16 columns from the same orders mart — the One-off vs Subscription split, per-order min/avg/max, the active-day window and share of total — a genuinely many-column table, the shape the contained horizontal scroll and the frozen key-dimension column are for.

S3LineChart

The on-brand line chart, for a trend over time — one line, or several. Evidence-native props (data, x, y, series, yFmt, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card. A single line defaults to neutral Ash — action colours are reserved for action-specific content, never generic decoration. Pass a series column to get a multi-series line whose lines are drawn from the theme's Categorical palette (action accents first), with a branded legend.

Single series (self-framed)

Net revenue
Daily · dev_matt.orders
Loading...
Total net revenue £2,841
Days 41

The card carries the title, subtitle and an optional footer-stat row; the crosshair tooltip and subtle entrance motion come from the shared theme.

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout (a KPI strip, a custom grid). Title and framing are then the page's job.

Loading...

Multi-series (series)

Pass a series column and the chart draws one line per distinct value — here daily net revenue split by acquisition channel group, a real several-lines trend (one line per group). Each line takes a distinct colour from the theme's Categorical palette (action accents first); the branded legend and the shared crosshair tooltip both list every series. color is ignored — the palette drives the lines, the same rule the multi-series area and bar follow.

Daily net revenue by channel group
Multi-series · dev_matt.orders
Loading...
Channel groups 4
Days 44

Usage

<S3LineChart
    data={daily_rev}
    x="order_date"
    y="net_revenue"
    yFmt="gbp"
    title="Net revenue"
    subtitle="Daily · dev_matt.orders"
/>

<!-- multi-series: one line per `series` value, from the Categorical palette + a legend -->
<S3LineChart
    data={daily_by_group}
    x="order_date"
    y="net_revenue"
    series="channel_group"
    yFmt="gbp"
    title="Daily net revenue by channel group"
    subtitle="Multi-series · dev_matt.orders"
/>

<!-- bare, no card chrome -->
<S3LineChart data={daily_rev} x="order_date" y="net_revenue" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block (long format when series is set).
x string x-accessor column (dates render as D Mmm, UTC).
y string y-accessor column.
series string undefined Series-key column. Its presence draws one line per value from the Categorical palette, with a legend.
yFmt string 'num' gbp · usd · eur · pct · num, or a literal prefix ('£').
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 280 Plot height.
frame boolean true false renders bare, no card chrome.
color string neutral Ash Line colour for a lone line — pass an action colour only for action-specific content. Ignored for multi-series (the Categorical palette drives the lines).
area boolean false Fill a subtle gradient under the line.
smooth boolean true Monotone-smoothed line.
stats array [] Footer figures: [{ label, value, accent? }].

Lone line → Ash, multi-series → Categorical palette. A single line is a neutral mark; the moment a series splits it, the lines take the Categorical palette from the ECharts theme — no hex is duplicated in the page or component (the ADR 0005 carve-out).

S3BarChart

The on-brand bar chart for a categorical comparison (revenue per channel, orders per category). Evidence-native props (data, x, y, yFmt, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card. Every bar wears the signed-off Mode C fill — an identical Muted Violet → Soft Teal gradient with top-rounded corners, never Light Blue (the PROTECT accent is reserved). The fill is fixed by the theme, so there is deliberately no color prop to break the brand rule.

Vertical (self-framed)

Net revenue by channel
dev_matt.orders · net revenue
Loading...
Channels 7
Top channel Paid Meta

The gradient is box-relative, so a short bar and a tall bar read as the same violet→teal ramp — the "identical every bar" rule — while a shadow (band) tooltip replaces the line crosshair. The subtle staggered entrance comes from the shared theme.

Horizontal (swapXY)

Add swapXY for horizontal bars — better when category labels are long or the chart is a ranking. The gradient runs along the bar (violet at the tip → teal at the baseline) and the corners round on the right.

Net revenue by channel
dev_matt.orders · horizontal

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout. Title and framing are then the page's job.

Loading...

Horizontal, bare (swapXY frame={false})

The fourth variant: horizontal bars with no card chrome — a bare ranking to drop into another layout.

Usage

<S3BarChart
    data={channel_rev}
    x="channel"
    y="net_revenue"
    yFmt="gbp"
    title="Net revenue by channel"
    subtitle="dev_matt.orders"
/>

<!-- horizontal (ranking / long labels) -->
<S3BarChart data={channel_rev} x="channel" y="net_revenue" yFmt="gbp" swapXY />

<!-- bare, no card chrome -->
<S3BarChart data={channel_rev} x="channel" y="net_revenue" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block.
x string Category accessor (dates render as D Mmm, UTC).
y string Value accessor.
yFmt string 'num' gbp · usd · eur · pct · num, or a literal prefix ('£').
swapXY boolean false true renders horizontal bars (category on the y-axis).
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 280 Plot height.
frame boolean true false renders bare, no card chrome.
stats array [] Footer figures: [{ label, value, accent? }].

No color prop. Single-series bars always use the Mode C violet→teal gradient from the ECharts theme. Multi-series categorical bars (the Categorical palette) are a separate component — see S3StackedBar below.

S3AreaChart

The on-brand area chart, for a magnitude trend (a single filled area) or a composition over time (a stacked area). Evidence-native props (data, x, y, series, yFmt, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card. A lone area defaults to neutral Ash — action colours are reserved for action-specific content, never generic decoration. Pass a series column to get a stacked area whose bands are drawn from the theme's Categorical palette (action accents first), with a branded legend.

Single filled area (self-framed)

A lone area is a filled trend line — here the cumulative net revenue building over the sample window. It stays neutral Ash and wears the same soft vertical fade as the line's area fill.

Cumulative net revenue
Running total · dev_matt.orders
Loading...
Total to date £2,841
Days 44

Stacked (series)

Pass a series column and the area stacks — one band per channel group, so the chart reads as composition: each band's thickness is that group's contribution and the top edge is the running total. Bands are coloured from the Categorical palette (Soft Teal, Muted Violet, Light Blue…) with the shared crosshair tooltip listing every series.

Cumulative net revenue by channel group
Stacked · dev_matt.orders
Loading...

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout (a KPI strip, a custom grid). Title and framing are then the page's job. The stacked area with its legend renders bare all the same.

Loading...

Usage

<!-- single filled area (lone → neutral Ash) -->
<S3AreaChart
    data={cum_total}
    x="order_date"
    y="cum_net_revenue"
    yFmt="gbp"
    title="Cumulative net revenue"
    subtitle="Running total · dev_matt.orders"
/>

<!-- stacked: one band per series value, from the Categorical palette -->
<S3AreaChart
    data={cum_by_group}
    x="order_date"
    y="cum_net_revenue"
    series="channel_group"
    yFmt="gbp"
    title="Cumulative net revenue by channel group"
/>

<!-- bare, no card chrome -->
<S3AreaChart data={cum_by_group} x="order_date" y="cum_net_revenue"
             series="channel_group" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block (long format when series is set).
x string x-accessor column (dates render as D Mmm, UTC).
y string Value accessor.
series string undefined Series-key column. Its presence switches to a stacked multi-band area.
yFmt string 'num' gbp · usd · eur · pct · num, or a literal prefix ('£').
stacked boolean derived true when series is set; pass false to overlay the bands instead.
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 280 Plot height.
frame boolean true false renders bare, no card chrome.
color string neutral Ash Fill colour for a lone area — pass an action colour only for action-specific content. Ignored for multi-series (the Categorical palette drives the bands).
smooth boolean true Monotone-smoothed edge (auto-disabled on stacked bands to stop overshoot).
stats array [] Footer figures: [{ label, value, accent? }].

Lone area → Ash, stacked → Categorical palette. A single area is a neutral mark (like the line); the moment a series splits it, the bands take the Categorical palette from the ECharts theme — no hex is duplicated in the page or component.

S3StackedBar

The on-brand multi-series categorical bar, for a composition (stacked — each band's height is that series' share of the category total) or a direct comparison (grouped — bars side by side, or 100% stacked — the share-of-total mix, stack="percent"). Evidence-native props (data, x, y, series, yFmt, type, stack, swapXY, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card. This is the first component to reach for the Categorical palette — the bounded carve-out from action-exclusivity (ADR 0005): a multi-series categorical chart reads the action colours as distinct cool categories, so each series takes one solid palette hex (Soft Teal, Muted Violet, Light Blue…) with a branded legend. It deliberately drops the single-series Mode C gradient, which is the identity of a lone bar.

Stacked (self-framed)

Pass a series column and the bars stack — one band per order type, so each bar reads as composition: the band thickness is that type's contribution and the full bar is the channel group's total. A shadow (band) tooltip lists every series.

Net revenue by channel group
Stacked by order type · dev_matt.orders
Loading...
Channel groups 4
Series 2

Grouped (type="grouped")

Switch to type="grouped" and the bands sit side by side instead of stacking — better when the question is a direct comparison of each series across categories rather than the running total. Same Categorical palette, same legend.

Net revenue by channel group
Grouped by order type · dev_matt.orders
Loading...

Horizontal (swapXY)

Add swapXY for horizontal bars — better when category labels are long or the chart is a ranking. Stacking runs left→right and the outer band rounds on the right.

Net revenue by channel group
Stacked · horizontal · dev_matt.orders
Loading...

100% stacked (stack="percent")

Add stack="percent" and each category's bands are normalised to sum to 100% — every bar fills the axis, so the read shifts from how much to what share: the mix of order types within each channel group, comparable across groups even where their absolute totals differ. The value axis becomes a 0–100% scale, and the tooltip shows each band's raw value and its share of the group total. yFmt still formats the raw figure in the tooltip.

Order-type mix by channel group
100% stacked · dev_matt.orders
Loading...
Channel groups 4
Series 2

100% stacked, horizontal (stack="percent" + swapXY)

Percent is a normalised stack, so it composes with swapXY — the same share-of-total read running left→right, better when the category labels are long or the chart is a ranking of mixes. Each bar still fills the full 0–100% axis and the outer band rounds on the right.

Order-type mix by channel group
100% stacked · horizontal · dev_matt.orders
Loading...

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout. Title and framing are then the page's job; the legend renders bare all the same.

Loading...

Usage

<!-- stacked: one band per series value, from the Categorical palette -->
<S3StackedBar
    data={cg_by_type}
    x="channel_group"
    y="net_revenue"
    series="order_type"
    yFmt="gbp"
    title="Net revenue by channel group"
    subtitle="Stacked by order type · dev_matt.orders"
/>

<!-- grouped: bars side by side (direct comparison) -->
<S3StackedBar data={cg_by_type} x="channel_group" y="net_revenue"
              series="order_type" type="grouped" yFmt="gbp" />

<!-- horizontal (ranking / long labels) -->
<S3StackedBar data={cg_by_type} x="channel_group" y="net_revenue"
              series="order_type" swapXY yFmt="gbp" />

<!-- 100% stacked (share-of-total): each bar fills the axis, tooltip shows raw + share -->
<S3StackedBar data={cg_by_type} x="channel_group" y="net_revenue"
              series="order_type" stack="percent" yFmt="gbp" />

<!-- 100% stacked, horizontal -->
<S3StackedBar data={cg_by_type} x="channel_group" y="net_revenue"
              series="order_type" stack="percent" swapXY yFmt="gbp" />

<!-- bare, no card chrome -->
<S3StackedBar data={cg_by_type} x="channel_group" y="net_revenue"
              series="order_type" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block, long format (one row per x × series).
x string Category accessor (dates render as D Mmm, UTC).
y string Value accessor.
series string Series-key column — one band (stacked) or bar (grouped) per distinct value.
yFmt string 'num' gbp · usd · eur · pct · num, or a literal prefix ('£').
type string 'stacked' 'stacked' (composition) or 'grouped' (bars side by side).
stack string 'absolute' 'percent' normalises each category's bands to 100% (share-of-total; axis becomes 0–100%, tooltip shows raw + share). Forces stacking, overriding type="grouped".
swapXY boolean false true renders horizontal bars (category on the y-axis).
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 280 Plot height.
frame boolean true false renders bare, no card chrome.
stats array [] Footer figures: [{ label, value, accent? }].

No color prop. Multi-series bars always draw from the Categorical palette in the ECharts theme — the bounded carve-out (ADR 0005), kept to multi-series categorical charts. No hex is duplicated in the page or component. For a single-series bar (the Mode C gradient), use S3BarChart.

S3Donut

The on-brand donut (ring) chart, for a part-to-whole composition of a handful of slices — net revenue split across channel groups, orders by type. Evidence-native props (data, name, value, fmt, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card. Slices draw from the theme's Categorical palette (action accents first) — the same bounded carve-out (ADR 0005) the stacked bar and area use — and each slice's outside label (name · value · percent) doubles as the legend. The ring's hole carries the composition's total.

A donut is a special case, not a default. Reach for it only for a true part-to-whole of 2–4 slices where the split itself is the point; beyond ~4 slices a sorted S3BarChart reads far more accurately (length on a common baseline beats angle). Never a donut for a plain comparison.

Self-framed (with centre total)

Each slice is a channel group's share of net revenue; the label carries its value and percent of the whole, and the centre holds the formatted total. centerLabel is the caption under it.

Net revenue by channel group
Share of total · dev_matt.orders
Loading...
Channel groups 4
Top group Paid Media

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout (a KPI strip, a custom grid). Title and framing are then the page's job; the centre total and slice labels render bare all the same. Passing centerLabel="" would drop the centre label for a plain ring.

Loading...

Usage

<!-- part-to-whole: one slice per group, from the Categorical palette, total in the hole -->
<S3Donut
    data={rev_by_group}
    name="channel_group"
    value="net_revenue"
    fmt="gbp"
    centerLabel="Net revenue"
    title="Net revenue by channel group"
    subtitle="Share of total · dev_matt.orders"
/>

<!-- bare, no card chrome; centerLabel="" drops the centre label for a plain ring -->
<S3Donut data={rev_by_group} name="channel_group" value="net_revenue" fmt="gbp" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block (one row per slice).
name string Slice-name accessor column (the category).
value string Slice-value accessor column (the measure).
fmt string 'num' gbp · usd · eur · pct · num, or a literal prefix ('£').
centerLabel string | false 'Total' Caption under the auto-computed total in the ring's hole; "" / false hides the centre label.
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 300 Plot height.
frame boolean true false renders bare, no card chrome.
stats array [] Footer figures: [{ label, value, accent? }].

No color prop. Slices always draw from the Categorical palette in the ECharts theme — the bounded carve-out (ADR 0005). No hex is duplicated in the page or component. The total in the hole is summed from value, so it always matches the slices.

S3Combo

The on-brand dual-axis combo — bars on the left axis, a line on the right — for two measures that share an x but carry different units, so neither fits the other's scale. The specimen reads the same daily series as the line/KPI specimens (daily_rev): net revenue as bars (£, left axis) against order volume as a line (a count, right axis). Evidence-native props (data, x, bar, line, yFmt, y2Fmt, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card.

A combo earns its second axis by unit, not by decoration. Reach for it only when the two series genuinely read on different scales (here £ thousands vs a handful of orders) — a dual axis on two like-scaled series invents a relationship the numbers don't have. The bar is a single series, so it keeps the Mode C violet→teal gradient; the line is the neutral Ash data mark (an action colour would imply it codes a product action).

Self-framed (dual axis)

Bars sit in the daily category bands (revenue, left axis) and the orders line rides their centres against the right axis. A shadow (band) tooltip lists both, each in its own unit.

Revenue and order volume
Daily · dev_matt.orders
Loading...
Net revenue £2,841
Orders 183
Days 41

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout. Title and framing are then the page's job; the legend, both axes and the tooltip render bare all the same.

Loading...

Usage

<!-- dual axis: £ revenue bars (left) against an order-count line (right) -->
<S3Combo
    data={daily_rev}
    x="order_date"
    bar="net_revenue"
    line="orders"
    yFmt="gbp"
    y2Fmt="num"
    barName="Net revenue"
    lineName="Orders"
    title="Revenue and order volume"
    subtitle="Daily · dev_matt.orders"
/>

<!-- bare, no card chrome -->
<S3Combo data={daily_rev} x="order_date" bar="net_revenue" line="orders"
         yFmt="gbp" y2Fmt="num" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block (one row per x).
x string X accessor (dates render as D Mmm, UTC).
bar string Bar y-accessor (left axis). barY is an accepted alias.
line string Line y-accessor (right axis). lineY is an accepted alias.
yFmt string 'num' Left-axis / bar format: gbp · usd · eur · pct · num, or a literal prefix.
y2Fmt string yFmt Right-axis / line format; defaults to yFmt when unset.
barName string col name Legend / tooltip label for the bars.
lineName string col name Legend / tooltip label for the line.
lineColor string Ash Explicit line colour; only when the line is action-specific.
smooth boolean true Smooth the line.
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 300 Plot height.
frame boolean true false renders bare, no card chrome.
stats array [] Footer figures: [{ label, value, accent? }].

Colours are theme-sourced. The bars reuse buildBarOption's Mode C gradient and the line defaults to the neutral Ash mark — both from the ECharts theme, no hex in the page or component. Only the left (bar) axis draws gridlines; the right axis carries just its own labels so the two scales don't overlay a double grid.

S3Funnel

The on-brand funnel — the ordered drop-off view, for one measure narrowing through the successive stages of a pipeline. The specimen reads the lead-capture & sensitivity-quiz funnel from the ecommerce_funnel mart (staging dataset dev_matt): S3's signature engagement path, from clicking the lead CTA through completing the sensitivity quiz, summed across the window. Rows arrive in pipeline order (entry first) and the funnel keeps that order.

Evidence-native props (data, stage, value, fmt, variant, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card. variant="bar" renders the same steps as a horizontal bar chart (see below).

A funnel is for an ordered pipeline, not any comparison. Reach for it only when each stage is a strict subset of the one before, so the measure can only shrink and the drop-off between stages is the point — a plain categorical comparison belongs in a sorted S3BarChart. Stages take the theme's sequential neutral ramp (Pale Blue Grey → Ash Grey) — the magnitude encoding, never the categorical action accents (an action colour would imply each stage codes a distinct product action).

Self-framed

Each trapezoid's width is its share of the entry (top) stage, so the widths read the conversion directly; the label carries the stage name, value and share of entry, and the tooltip adds step-over-step conversion off the previous stage.

Lead & sensitivity-quiz funnel
Events by stage, from lead CTA click · dev_matt.ecommerce_funnel
Loading...
Entry (lead CTA click) 286
Completed quiz 102
Overall conversion 35.7%

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout. Title and framing are then the page's job; the stages, labels and tooltip render bare all the same.

Loading...

Usage

<!-- ordered drop-off: one row per stage, entry first, from the sequential neutral ramp -->
<S3Funnel
    data={lead_quiz_funnel}
    stage="stage"
    value="events"
    fmt="num"
    title="Lead & sensitivity-quiz funnel"
    subtitle="Events by stage, from lead CTA click · dev_matt.ecommerce_funnel"
/>

<!-- bare, no card chrome -->
<S3Funnel data={lead_quiz_funnel} stage="stage" value="events" fmt="num" frame={false} />

Horizontal-bar variant (variant="bar")

The same ordered drop-off steps, drawn as a horizontal bar chart (one bar per step) rather than the trapezoid funnel — for cases where a bar reads more clearly than the funnel shape. The step name rides the y-axis; each bar carries its value and share of entry off its right end, and the tooltip adds step-over-step conversion, exactly as the funnel does. It is a variant of S3Funnel, not S3BarChart: reusing the categorical bar would apply the Mode C violet→teal gradient, the wrong palette for a magnitude drop-off — so the bar variant keeps the funnel's sequential neutral ramp, and the two views stay visually consistent.

Lead & sensitivity-quiz funnel — bar view
Events by stage, from lead CTA click · dev_matt.ecommerce_funnel
Loading...

Order is the pipeline, not the value. Like the trapezoid funnel, the bar variant draws the steps in the order the query gives (entry first → top), so a later-stage anomaly shows rather than being hidden by a re-sort. For a well-formed monotonic funnel that order already places the longest step at top — order the query by the pipeline (stage_order), not by value.

<!-- same funnel data, horizontal-bar view: one bar per step, longest at top -->
<S3Funnel
    data={lead_quiz_funnel}
    stage="stage"
    value="events"
    fmt="num"
    variant="bar"
    title="Lead & sensitivity-quiz funnel — bar view"
/>

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block, in pipeline order (one row per stage, entry first).
stage string Stage-label accessor column. name is an accepted alias.
value string Stage-value accessor column (the measure).
fmt string 'num' gbp · usd · eur · pct · num, or a literal prefix ('£').
variant string 'funnel' 'funnel' (trapezoid, default) or 'bar' (horizontal bar, one bar per step).
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 320 Plot height.
frame boolean true false renders bare, no card chrome.
stats array [] Footer figures: [{ label, value, accent? }].

No color prop. Stages always draw from the sequential neutral ramp in the ECharts theme, interpolated to the stage count — no hex is duplicated in the page or component, and the action accents stay reserved for product actions (ADR 0005). Rows are drawn in the order given, so order the query by the pipeline (stage_order), not by value.

S3Scatter

The on-brand scatter / bubble — the relationship view, for how two continuous measures move together. The specimen reads per-channel order economics from the orders mart (staging dataset dev_matt): order volume (orders, x) against average order value (avg_order_value, y), each mark sized by that channel's total net revenue and coloured by its channel group — a real correlation-and-outlier view over a live mart.

Evidence-native props (data, x, y, size, series, label, xFmt, yFmt, sizeFmt, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card.

A scatter is for a relationship, not a comparison. Reach for it only when both axes are continuous measures and the question is how they move together (correlation, clusters, outliers) — a category-vs-measure comparison belongs in a sorted S3BarChart, a trend over time in an S3LineChart. Both axes frame the cloud to its own range (they start off zero), so the spread reads rather than the distance to an origin. A single series stays neutral Ash (the lone-mark rule); the moment a series splits it, the groups take the Categorical palette (action accents first) with a branded legend.

Bubble + series (self-framed)

The richest form: a size column turns each mark into a bubble whose area (not width) reads the net revenue — encoded √-scaled so twice the value covers twice the area — and a series column colours the marks by channel group. The label names each point in the tooltip; the x, y and size rows each format in their own unit.

Order volume vs value
Bubble = net revenue · colour = channel group · dev_matt.orders
Loading...
Channels 7
Channel groups 4
Top by revenue Paid Meta

Single series (neutral)

Drop the series column and every mark is the neutral Ash lone-mark — the relationship between volume and value alone, with the bubble still sizing each point by net revenue. No action colour, because the points don't code a product action.

Order volume vs value
Bubble = net revenue · dev_matt.orders
Loading...

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout. Title and framing are then the page's job; the legend, both axes and the tooltip render bare all the same.

Loading...

Usage

<!-- bubble + series: size = a third measure (√-area), colour = the series groups -->
<S3Scatter
    data={channel_scatter}
    x="orders"
    y="avg_order_value"
    size="net_revenue"
    series="channel_group"
    label="channel"
    xFmt="num"
    yFmt="gbp"
    sizeFmt="gbp"
    title="Order volume vs value"
    subtitle="Bubble = net revenue · colour = channel group · dev_matt.orders"
/>

<!-- single series → neutral Ash (drop `series`) -->
<S3Scatter data={channel_scatter} x="orders" y="avg_order_value"
           size="net_revenue" label="channel" xFmt="num" yFmt="gbp" sizeFmt="gbp" />

<!-- plain scatter, no bubble (drop `size` for a fixed dot size) -->
<S3Scatter data={channel_scatter} x="orders" y="avg_order_value" label="channel" yFmt="gbp" />

<!-- bare, no card chrome -->
<S3Scatter data={channel_scatter} x="orders" y="avg_order_value"
           size="net_revenue" series="channel_group" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block (one row per point).
x string x-accessor column (a continuous measure).
y string y-accessor column (a continuous measure).
size string undefined Bubble-size accessor — its presence turns points into √-area bubbles.
series string undefined Series-key column — its presence splits the marks into coloured groups + a legend.
label string undefined Point-identity accessor (the tooltip header, e.g. a channel name).
xFmt string 'num' x-axis / tooltip x format: gbp · usd · eur · pct · num, or a literal prefix.
yFmt string 'num' y-axis / tooltip y format.
sizeFmt string 'num' Tooltip size format.
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 320 Plot height.
frame boolean true false renders bare, no card chrome.
stats array [] Footer figures: [{ label, value, accent? }].

No color prop. A single-series scatter is the neutral Ash mark; a multi-series plot draws its groups from the Categorical palette in the ECharts theme — the bounded carve-out (ADR 0005). No hex is duplicated in the page or component. Bubble size is √-area encoded (a mark's area, not its width, reads the magnitude) and the scale spans the whole dataset, so a size means the same across every series.

S3Heatmap

The on-brand heatmap — the magnitude-across-two-categories view, for where one measure concentrates in a grid of two categorical dimensions. The specimen reads order density by weekday × acquisition channel group from the orders mart (staging dataset dev_matt): each cell is the number of orders placed on that weekday through that channel group — a real cross-tab of where orders land, the shape a heatmap is for. The source emits a full 7 × N grid (zero-filled) so every cell renders; a weekday/channel with no orders is a real 0, not a hole.

Evidence-native props (data, x, y, value, fmt, title, subtitle, height, frame) map to a themed ECharts option and render inside the standard Chart card.

A heatmap is for a magnitude over two categories, not a comparison or a relationship. Reach for it only when one measure is read as colour intensity over a grid of two categorical dimensions (x columns × y rows) and the question is where it concentrates — a category-vs-measure comparison belongs in a sorted S3BarChart, two continuous measures moving together in an S3Scatter. The colour ramp is the theme's sequential neutral scale (Pale Blue Grey → Ash Grey) driven by a visualMap — the magnitude encoding, never the categorical action accents (an action colour would imply each cell codes a distinct product action).

Self-framed

Distinct x (day_of_week) and y (channel_group) values become the columns and rows in the query's order, so the columns read Mon → Sun and the rows in a stable order. Each cell's colour is its order count on the neutral ramp; the value is printed in the cell (light text on dark cells, dark on pale) and the tooltip names the row · column pair with its value.

Order density by weekday & channel
Orders by weekday × acquisition channel group · dev_matt.orders
Loading...
Total orders 183
Channel groups 4
Busiest cell 18 orders

Bare (frame={false})

With frame={false} the chart renders with no card chrome — for embedding inside another layout. Title and framing are then the page's job; the tiles, cell labels, the magnitude legend and the tooltip render bare all the same.

Loading...

Usage

<!-- magnitude across two categories: one row per cell, ordered so the columns/rows read as you want -->
<S3Heatmap
    data={channel_dow_orders}
    x="day_of_week"
    y="channel_group"
    value="orders"
    fmt="num"
    title="Order density by weekday & channel"
    subtitle="Orders by weekday × acquisition channel group · dev_matt.orders"
/>

<!-- bare, no card chrome -->
<S3Heatmap data={channel_dow_orders} x="day_of_week" y="channel_group" value="orders" fmt="num" frame={false} />

Props

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block (one row per cell). Zero-fill a full x × y grid for a hole-free matrix.
x string x-accessor column (the columns). Distinct values become the column axis in first-seen order.
y string y-accessor column (the rows). Distinct values become the row axis in first-seen (top → bottom) order.
value string Cell-value accessor column (the measure mapped to colour).
fmt string 'num' gbp · usd · eur · pct · num, or a literal prefix ('£').
title string '' Chart-card title.
subtitle string '' Chart-card subtitle.
height number (px) 320 Plot height.
frame boolean true false renders bare, no card chrome.
stats array [] Footer figures: [{ label, value, accent? }].

No color prop. Cells always draw from the sequential neutral ramp in the ECharts theme, interpolated by a visualMap between its two stops — no hex is duplicated in the page or component, and the action accents stay reserved for product actions (ADR 0005). A cell no row touches renders as a gap; zero-fill the source (a full x × y grid) so every cell is an explicit value. Rows and columns are drawn in the order the query gives, so order the source by the axis you want.

KpiCard

The library's single-figure stat — a headline number with an optional period-over-period delta and an inline SVG micro-trend. It is a member of the Chart library but stays lightweight SVG (no ECharts instance), so a KPI strip renders many cheaply. The card is always self-framed (it is the card — there is no frame prop). It reads the shared --s3-* tokens for surface, type and spacing, so it sits alongside the framed charts on brand.

Delta colour is semantic, not action. The up/down pill uses the semantic --s3-pos / --s3-neg (muted green / clay), deliberately not the action colours — teal/violet/blue carry a product-action meaning, not good/bad. The inline micro-trend strokes the neutral --s3-accent slate (the same neutral mark a lone S3LineChart uses).

KPI strip

Net revenue (recent period)
-18.6%
£1,274.84
vs £1,566.30 prior period
Orders (recent period)
-11.3%
86
vs 97 prior period
Total net revenue
£2,841

The first card carries all three optional layers — the delta pill (net revenue is down on the prior half, so the pill is clay and points down), the vs … prior period sub-line, and the hover-tracking inline sparkline. The second drops the trend; the third is a big bare total with no comparison. All three take their row from a real dev_matt mart.

Presentation states

The card resolves every figure through components/presentationState.js (PRD044 §9), the one place that decides which of four states a figure renders in. The house discipline it encodes: a marked figure is still shown — suppression is reserved for figures that would be actively misleading, never for figures that are merely thin (CONTEXT.md, Small-n marking).

The four states are opt-in. A card given none of the state props resolves no state at all and renders exactly as the three specimens above do.

Normal
118
Marked (small-n)
3
rests on a denominator of 3, below the bar of 5
Suppressed comparison
118
The comparison period reaches before trading began on 16 June 2026.
Too early
too early
the period has not closed

Reading them left to right: normal is the ordinary card. Marked still prints its figure and carries a marker with the denominator it rests on beside it. Suppressed prints the figure and withholds only the comparison — the delta pill and the vs … prior period sub-line are gone and the reason is printed against a rule, so a reader can tell a deliberate withholding from a comparison nobody configured. Too early prints the words, never zero and never an empty cell, because the underlying NULL means the period has not had time to produce the behaviour being measured.

periodClosed and priorComplete are not the same flag. priorComplete describes the prior period (is the comparison basis honest?); periodClosed describes the period the figure itself covers (has it finished?). Collapsing them would make "too early" and "comparison withheld" one state, and they render differently by design.

invertDelta is the other half of the honesty story: the delta pill is green for "up" regardless of the metric, which is wrong for CAC or subscriber churn. invertDelta flips the colour only — the arrow keeps pointing the way the number actually moved.

Usage

<!-- headline stat: value + period-over-period comparison + inline micro-trend -->
<KpiCard
    data={kpi}
    value="net_revenue"
    comparison="net_revenue_prior"
    prefix="£"
    fmt="#,##0.00"
    title="Net revenue (recent period)"
    trend={daily_rev}
    trendAccessor="net_revenue"
    trendDateAccessor="order_date"
/>

<!-- plainest form: just a value + title (like the index scaffold check) -->
<KpiCard data={kpi} value="orders" fmt="#,##0" title="Orders" />

<!-- big hero figure -->
<KpiCard data={rev_summary} value="total_net_revenue" prefix="£" fmt="#,##0" title="Total" big />

Props

Prop Type Default Notes
data query result Rows from an Evidence SQL block; the card reads the first row.
value string Column holding the headline figure.
title string '' The label above the figure.
comparison string null Column holding the prior value; its presence adds the delta pill + sub-line.
comparisonLabel string 'vs' Prefix on the … prior period sub-line.
fmt string null #,##0 · #,##0.00 · 0.0 · 0.00 (Excel-style patterns), else a plain thousands format.
prefix string '' Prepended to the formatted figure (e.g. £).
suffix string '' Appended to the formatted figure (e.g. %).
trend array null Rows for the inline sparkline; its presence adds the micro-trend.
trendAccessor string null Value column within trend.
trendDateAccessor string 'period' Date column within trend (drives the hover tooltip date).
big boolean false Larger hero figure.
denominator number null What the figure rests on. Below minDenominatormarked.
priorComplete boolean null falsesuppressed: the comparison is withheld and the reason printed. null means no comparison configured, which is a different thing.
periodClosed boolean true false on a null value ⇒ tooEarly. Describes the figure's own period, not the prior one.
minDenominator number 5 The small-n bar (CONTEXT.md's house default; charts pass 30 sessions).
state string null 'normal' · 'marked' · 'suppressed' · 'tooEarly'. Overrides the resolution when the caller already knows the state.
reason string '' The caller's own wording, printed beside the figure in place of the module's generic reason.
invertDelta boolean false Flips the delta pill's colour for metrics where up is bad (CAC, churn). The arrow still follows the number.

Formatter note. KpiCard's fmt is an Excel-style number pattern (#,##0), not the chart yFmt shorthand (gbp/pct). Use prefix/suffix for currency and percent symbols so the two vocabularies don't collide.

MonthlyPacing

The monthly pacing panel from /company (PRD044 slice 3): month-to-date Sales for the month in flight, set against the same month-day range of the prior month and — separately, and labelled as such — against that month's full total. A bare "vs last month" putting three days beside thirty-one is the dishonesty the panel was built to avoid, so it never prints one.

Its full-month projection — MTD Sales ÷ elapsed days × days in the month — is withheld until day 7, resolving to the suppressed state above with the reason printed. The rule lives once, in components/monthlyPacing.js.

This specimen is the reason the panel is a component at all. On live data the published branch cannot be seen until a month reaches its seventh day, and faking a later date to look at it would be a lie told to the build. So the fixture below is a fabricated month at day 9 of 30, fed to the same component through the same rule. /company renders the withheld branch on real data; this renders the published one. Neither is taken on trust.

Not governed by the Window This panel always describes the month in flight, whichever Window is selected above — that is the only sense in which the Window governs it. Month in flight: September 2026, day 9 of 30, 9 of them carrying orders, from data through 9 Sep 2026.
MTD Sales
+29.2%
£2,480.55
1–9 September 2026
vs £1,920.40 · 1–9 August 2026
Like for like: the same month-day range of August 2026. It rests on £1,920.40 over 118 orders. The prior month in FULL is on the row below, labelled as such — the two are different comparisons and the panel never blurs them.
MTD orders
+25.4%
148
1–9 September 2026
vs 118 · 1–9 August 2026
Like for like: orders over the same month-day range of August 2026. It rests on 118 orders.
Full-month projection
£8,268.50
Projected for September 2026
Month-to-date Sales ÷ 9 elapsed days × 30 days in the month. A projection, not a forecast: it assumes the rest of the month trades at the rate the month has traded at so far.
August 2026 · full month · Sales
£6,410.75
August 2026 · full month · orders
402
1–9 August 2026 · Sales
£1,920.40
1–9 August 2026 · orders
118
MTD Sales as a share of August 2026 in full
38.7%

Two comparisons, both spelled out, because they answer different questions. The tile above compares 1–9 September 2026 against 1–9 August 2026 — the same month-day range, so it is a like-for-like read on whether the month has started better or worse. The share on the right compares month-to-date against August 2026 in full, which is a progress read and not a growth read: it is expected to be well under 100% for most of a month. A bare "vs last month" would be neither, and is what this panel exists to avoid.

Day 9 clears the bar, so the projection publishes: £2,480.55 over 9 elapsed days is £275.62 a day, which over 30 days is £8,268.50. Drop elapsed_days below 7 and the same card resolves to suppressed — an em rule, and the reason against a rule — which is what /company shows today on data through 3 August 2026, day 3 of August.

The like-for-like basis is marked when it is thin. Cutting the prior month back to the same month-day range makes the comparison fair; it does not make it large. This fixture sits on 118 prior-range orders, well clear of the house 5-order bar, so it prints the basis and stops there. /company today is the other case: its basis is 1–3 July, £79.96 over 4 orders, which is the entire reason MTD Sales shows a +829.6% pill — July had barely begun trading, not August tripling it. There the basis carries a and the tile says to read the direction rather than the percentage. Marked, not withheld: CONTEXT.md's Small-n marking reserves suppression for figures that would be actively misleading, and a like-for-like comparison that names what it rests on is not one.

The comparison withheld, when its range predates trading

Thin is not the same as dishonest, and the panel renders them differently. A same-month-day range that reaches back before S3 traded at all is not a low number — it is a structural zero, and no percentage against it means anything. PRD044 §5 legislates that case for the whole page set (a prior-period comparison is suppressed unless it sits fully inside trading history, and prints the reason), and the pacing fence decides it off the same first trading date the headline tiles use, handing the verdict to KpiCard's priorComplete.

Like the published projection, this branch cannot be reached on live data — it needed the month in flight to be July, and time has moved past it — so it gets a fixture too:

Not governed by the Window This panel always describes the month in flight, whichever Window is selected above — that is the only sense in which the Window governs it. Month in flight: July 2026, day 3 of 31, 3 of them carrying orders, from data through 3 Jul 2026.
MTD Sales
£201.40
1–3 July 2026
Comparison withheld: the same month-day range of June 2026 reaches back before trading began on 16 June 2026, so the prior side is a structural zero rather than a low number.
MTD orders
12
1–3 July 2026
Comparison withheld: the same month-day range of June 2026 reaches back before trading began on 16 June 2026, so the prior side is a structural zero rather than a low number.
Full-month projection
Projection withheld until day 7 of the month — 3 of 31 days have elapsed, too few to carry a rate. A run-rate off 3 days moves by more than the month will.
June 2026 · full month · Sales
£1,141.41
June 2026 · full month · orders
76
MTD Sales as a share of June 2026 in full
17.6%

Only one comparison survives here. The like-for-like one — 1–3 July 2026 against the same month-day range of June 2026 — is withheld, and the tiles above print the reason. What is left is the share of June 2026 in full, a progress read and not a growth read: it says how far into last month's total this month has got, nothing about whether it is going better. A bare "vs last month" would claim to be both.

Both tiles drop their pill and print the reason; the strip keeps June's full-month totals, because a full month's total is a true total of whatever traded in it, and drops the same-range pair, because a £0.00 meaning "we had not opened yet" must not sit in a row of figures that all mean "this is what traded". The projection is withheld too, on the day-7 rule — day 3 of 31.

The 1st of the month, before a complete day exists

The pacing clock is the calendarcurrent_date() at build time — and not the day the latest order falls on, because a run-rate's denominator has to count the days that sold nothing too. analytics.orders covers 49 calendar days of which only 46 carry an order, so the difference is real money: a build on 26 July would have paced July over 23 days instead of 25 and projected £2,150.62 against a calendar-correct £1,978.57.

The calendar clock has one state the order clock never reached: a build that runs on the 1st, before that day's first order, when the month in flight has no complete day behind it at all. The old clock answered that by presenting the previous, completed month as "in flight" at day 31 of 31, projection and all. The honest answer is that there is nothing to pace yet, and — like the two branches above — it is shown rather than asserted:

Not governed by the Window This panel always describes the month in flight, whichever Window is selected above — that is the only sense in which the Window governs it. Month in flight: September 2026, day 0 of 30, 0 of them carrying orders, from data through 31 Aug 2026.
MTD Sales
£0.00
September 2026, no complete day yet
Comparison withheld: no complete day of September 2026 has passed yet, so there is no month-day range to read August 2026 against.
MTD orders
0
September 2026, no complete day yet
Comparison withheld: no complete day of September 2026 has passed yet, so there is no month-day range to read August 2026 against.
Full-month projection
No projection: the month in flight carries no elapsed days to compute a rate from.
August 2026 · full month · Sales
£6,410.75
August 2026 · full month · orders
402
MTD Sales as a share of August 2026 in full

Only one comparison survives here. The like-for-like one — September 2026, no complete day yet against the same month-day range of August 2026 — is withheld, and the tiles above print the reason. What is left is the share of August 2026 in full, a progress read and not a growth read: it says how far into last month's total this month has got, nothing about whether it is going better. A bare "vs last month" would claim to be both.

The projection resolves to suppressed on monthlyPacing.js's zero-days branch, not on the day-7 one — a different sentence for a different reason. August's full-month totals stay in the strip, because they are true totals of a month that has closed, and the share of them is an em rule rather than a 0.0% that would read as a month off to a bad start.

Props

Prop Type Default Purpose
data query | object One resolved pacing row. Array or bare object; the first row is used.
minElapsedDays number 7 The bar the projection must clear. The printed reason follows whatever it is set to.
minComparisonOrders number 5 The house small-n bar (CONTEXT.md). A like-for-like basis below it is marked, not withheld.

FlagCards

The Flag panel from /company (PRD044 slice 4): the severity-tagged "where to look next" cards that replace the reference dashboard's prose. Every rule is computed in SQL against a threshold, in the page fence, so it is evaluated over the same filtered days as the tile beside it and the two can never disagree (ADR 0007). There is no generated prose on those pages, and AttrCallout — a different, static component further down this page — is not a flag.

The component is the seam, and the seam is a five-column rowset:

Column Type Meaning
severity text SERIOUS or WARNING. No third level.
section text headline · acquisition · website · retention. Rendered verbatim as a tag.
title text The short claim.
detail text One line of supporting fact — never a judgement, never a recommendation.
value numeric The figure the rule tripped on, so every card puts its number in the same slot.

FlagCards.svelte contains no rule name, no threshold and no branch on section, and that is the property this specimen exists to prove. The fixture below carries four flags from four different sections — none of them the two rules /company actually ships — fed to the same component that renders the live panel. If a rule ever has to be taught to the Svelte, this specimen stops rendering first.

SERIOUS 2

acquisition 214.6
£214.60 spent on a channel with no attributed order
One channel carried £214.60 of spend and 0 attributed orders in the window, against a bar of £100.
retention 41.2
41.2% of subscribers cancel before their first renewal
14 of 34 subscribers in the cohort cancelled before a second billing, against a bar of 25%.

WARNING 2

website 312
4 landing pages carry sessions but no order
312 sessions across 4 landing pages produced no attributed order in the window, against a bar of 100 sessions.
headline 38.5
38.5% of gross product sales is discounted
£2,349.77 of £6,110.20 gross product sales was discounted in the window, against a bar of 30%.

Two SERIOUS above two WARNING, each card tagged with its own section and carrying its figure in the same slot — and the component was told none of that. Severity is read off the semantic negative token at two strengths (clay for SERIOUS, clay pulled back toward ash for WARNING) rather than off an action colour: per brand/brand-guidelines.md the three accents mean "this is about that product action" and are never adversarial good/bad, which is the same reason KPI deltas use --s3-pos / --s3-neg. The severity word is printed on every group as well, so the tone is redundancy rather than the only carrier.

value is unit-free by contract. The component cannot know whether a rule measures percent, pounds or sessions, so it applies no symbol — the units are carried by title and detail, which the rule wrote. That is why 312, 214.6, 38.5 and 41.2 sit in the same slot without a currency sign between them.

Nothing tripped

An empty rowset is a result — every rule ran, none crossed its bar — and it renders as one. A blank gap where a panel should be is indistinguishable from a panel that failed to load, which on a nightly-rebuilt static site is the exact confusion the Data baked at stamp exists to remove elsewhere. Half of this branch is reachable on live data — /company drops the unattributed card on the MTD window, where that share sits under its bar — but not the state where both rules are quiet, so it gets a fixture:

Nothing tripped for this Window
Both headline rules were evaluated over the days above and neither crossed its threshold. This is an empty result, not a panel that failed to load.

The wording is the caller's, because only the caller knows what was evaluated: /company says "all ten rules" and names the two bases they were evaluated over, while each drill-down names only its own section's rules, and a generic "no data" would be a third thing that is true of none of them.

Props

Prop Type Default Purpose
data query | rows [] The five-column flag rowset. Grouped by severity, SERIOUS first; row order is preserved within a group.
emptyTitle string Nothing tripped Headline for the empty state.
emptyDetail string see source Its supporting line. Name what was evaluated, so the empty state reads as an answer rather than an absence.

Sparkline

The library's in-table micro-trend — a bare, lightweight SVG line for showing a shape inline where a full framed chart would be too heavy: a table cell, a dense list, beside a label. Per ADR 0004 it stays SVG, not ECharts — an ECharts instance per table row isn't worth it. It strokes the neutral --s3-accent slate by default (a DOM SVG reads the CSS token directly, so no hex is duplicated); pass an action colour only for action-specific content.

Import it explicitly. Evidence ships a built-in <Sparkline> that shadows the S3 one on auto-import, so a page using the S3 Sparkline must import it — import Sparkline from '../../components/Sparkline.svelte'; in a page-level script block (this Gallery already does). Without that line <Sparkline> resolves to Evidence's own component. KpiCard has no such collision and needs no import.

In a table (the primary use case)

One micro-trend per acquisition channel, in a branded table cell — the shape a sparkline is for. The row's array is the channel's daily net-revenue series (a channel with a single day has no trend to draw, so its cell is deliberately blank).

ChannelNet revenueOrdersDaily trend
Paid Meta £1,029.46 55
Klaviyo Email £899.56 55
Unknown £668.19 55
Paid Search - Uncategorised £171.92 8
Paid Search - Generic £33.98 2
Organic Social £19.99 1
Organic Search £18.04 7

Standalone variants

The default fills a soft neutral fade under the line. area={false} drops the fill for a pure stroke; pass an accent (a theme action colour) only when the series is about that action — here Soft Teal for a CALM-specific trend.

Default (neutral, area)
area=false
accent (CALM)

Usage

<!-- Import once per page in the page-level script block, so <Sparkline> is the S3 one
     and not Evidence's built-in:  import Sparkline from '../../components/Sparkline.svelte'  -->

<!-- in a table cell: pass the row's series array + the value column -->
<Sparkline data={channel_daily.filter((d) => d.channel === row.channel)}
           accessor="net_revenue" w={120} h={26} />

<!-- standalone, neutral (default) -->
<Sparkline data={daily_rev} accessor="net_revenue" />

<!-- pure stroke, no area fill -->
<Sparkline data={daily_rev} accessor="net_revenue" area={false} />

<!-- action-specific: pass a theme action colour -->
<Sparkline data={daily_rev} accessor="net_revenue" accent="var(--s3-calm)" />

<!-- a bare array of numbers needs no accessor -->
<Sparkline data={[3, 5, 4, 8, 6, 9]} />

Props

Prop Type Default Notes
data array [] Row objects (with accessor) or a bare number array. Needs ≥ 2 points to draw.
accessor string null Value column within each row; omit for a bare number array.
accent string var(--s3-accent, currentColor) Stroke/fill colour — the neutral slate token by default (with a currentColor keyword fallback so the line never resolves to the SVG initial none if the token is out of scope); pass an action colour only for action-specific content.
area boolean true Fill a soft vertical fade under the line.
w number (px) 220 SVG width.
h number (px) 48 SVG height.
strokeWidth number 1.5 Line stroke width.

Stays SVG (ADR 0004). Sparkline is deliberately not an ECharts chart — one canvas instance per table row would be far heavier than a hand-rolled SVG path. It reads the same --s3-accent neutral as the rest of the library, so it stays on brand without a token duplicate.

S3DataTable

The library's branded data table — a shallow wrapper over Evidence's <DataTable> that bakes in the S3 brand table look so an author never restyles columns: a Pale Blue Grey header (--s3-panel) with Ash small-caps type and a stronger underline, Ash body type with tabular figures, a generous row rhythm (Evidence's default rows are tight), refined cool row dividers and a branded row-hover, all on the standard library card surface — the modern-UI look of the sibling ChannelsTable, in S3 brand tokens. A wide table scrolls horizontally inside the card (the page body never scrolls sideways), and freezeFirstColumn pins the first (key-dimension) column so it stays visible while the measures scroll. Every other prop passes straight through to <DataTable>data, rows, sort, search, totalRow, title — and <Column> children are forwarded untouched, so the full Evidence table API still applies.

Reach for a table when the value is the detail. A data table is right when a reader needs to look up or compare exact figures across several columns — not as a fallback for a chart. For a single-series comparison or trend, a chart reads faster; the table earns its place here as multi-column per-channel detail (value, average, share side by side).

Auto columns (self-framed)

The plainest form — pass data and no <Column> children, and Evidence generates a column per field, auto-formatting by type. The branded header, Ash type, row dividers and generous rows come from the wrapper. (These cells render their values: the wrapper forwards the default slot only when the page actually supplies <Column> children, so Evidence's auto-column fallback fires instead of blanking — the fix at the heart of this component.)

No Results

Explicit columns (formatting + titles)

Pass <Column> children to pick columns, set human titles and format each measure — currency for revenue and average order value, a percent for the share, a right-aligned integer for the order count. totalRow adds a summed footer, and sort/search come from the same passthrough.

No Results

Wide table — horizontal scroll + frozen first column (freezeFirstColumn)

With many dimensions and metrics the table outgrows the page width. It scrolls horizontally inside the card — the page body never scrolls sideways — and freezeFirstColumn pins the first column (here channel, the key dimension) so it stays visible while the rest scrolls under it. Scroll the table right to see the pinned column hold.

No Results

Heatmap — conditional colour-scale formatting (heatmap)

Set heatmap on the table and give the numeric columns you want shaded contentType="colorscale" — Evidence's own colour-scale feature then tints each cell by its value along the theme's sequential neutral ramp (Pale Blue Grey → Ash Grey, the default colorScale), turning a column of figures into an at-a-glance magnitude map while the exact values stay in place. Each column shades independently across its own min→max, and the dimension columns are left unshaded as the anchor. Text stays legible across the full ramp — Evidence sets each shaded cell's text to whichever of dark/light contrasts better against its own fill, so nothing goes ash-on-ash at the dark end. Because the shaded fills are opaque, the brand card, the contained horizontal scroll and the frozen first column all keep working with the heat on — scroll right and the pinned channel column holds, shaded and all.

Heat for magnitude, never for an action. The ramp is the theme's neutral sequential scale on purpose — a heatmap encodes how much, so it must not reach for an action colour (CALM/STRENGTHEN/PROTECT), which would falsely read as a product action. Keep it neutral.

No Results

Row marking — Small-n marking (markWhen / markColumn / markNote)

S3 has traded for 49 days, so Small-n marking (CONTEXT.md) is a first-class convention rather than a polish item: a row resting on a denominator too small to act on is marked, and still shown in full. markWhen names a boolean column the query already computed, markColumn names the cell that carries the glyph, and markNote says what the glyph means in a legend under the table — with the count of marked rows, so the legend cannot drift from the rows.

The bar lives in SQL, never here. The component compares nothing; it renders a flag the fence worked out over the same filtered days as every figure beside it, exactly as FlagCards renders a rule it knows nothing about. And the glyph goes into the data, so it travels with its row through Evidence's own sort, search and paging — marking by DOM position would re-point at the wrong rows the moment a reader clicked a column header. Sort the table below by any column and watch the daggers stay with their channels.

No Results

fewer than 5 orders, so every ratio on the row rests on that denominator. The row is still shown in full — a thin figure is marked, never hidden. 2 of 7 rows on this table carry it.

Usage

<!-- auto columns: one column per field, auto-formatted by type -->
<S3DataTable data={channel_table} rows={6} />

<!-- explicit columns: pick, title and format each; totalRow adds a summed footer -->
<S3DataTable data={channel_table} rows={6} totalRow={true} sort="net_revenue desc">
    <Column id="channel" title="Channel" />
    <Column id="net_revenue" title="Net revenue" fmt="gbp0" totalAgg="sum" />
    <Column id="revenue_share" title="Share" fmt="pct1" />
</S3DataTable>

<!-- wide table: contained horizontal scroll + pinned key-dimension column -->
<S3DataTable data={channel_wide} rows={6} freezeFirstColumn>
    <Column id="channel" title="Channel" />
    <!-- … many more columns … -->
    <Column id="revenue_share" title="Share" fmt="pct1" />
</S3DataTable>

<!-- heatmap: shade chosen numeric columns on the theme's neutral sequential ramp -->
<S3DataTable data={channel_wide} rows={6} heatmap>
    <Column id="channel" title="Channel" />
    <Column id="net_revenue" title="Net revenue" fmt="gbp0" contentType="colorscale" />
    <Column id="revenue_share" title="Share" fmt="pct1" contentType="colorscale" />
</S3DataTable>

<!-- small-n marking: the fence decides which rows are thin, the wrapper marks them -->
<S3DataTable data={channel_marked} markWhen="thin" markColumn="channel"
             markNote="fewer than 5 orders, so every ratio on the row rests on that denominator.">
    <Column id="channel" title="Channel" />
    <Column id="orders" title="Orders" fmt="#,##0" />
</S3DataTable>

Props

S3DataTable owns only the brand styling; everything else is Evidence's <DataTable> API, passed through. The most-used props:

Prop Type Default Notes
data query result [] Rows from an Evidence SQL block.
(children) <Column> slot Optional. Omit for auto-generated columns; pass <Column id fmt title …> to pick and format.
freezeFirstColumn boolean false Pins the first (key-dimension) column so it stays visible while the table scrolls horizontally.
heatmap boolean false Steps the row-hover chrome aside so heatmap columns read. Pair with contentType="colorscale" on the numeric columns you want shaded; they tint on the theme's neutral ramp with per-cell legible text.
(per column) contentType="colorscale" on <Column> Shades that column by value on the default (neutral) colorScale. colorScale, colorMin/colorMax/colorMid, scaleColumn pass through to tune the scale (Evidence's <Column> API).
markWhen string null Name of a boolean column in data that is true on the rows to mark (Small-n marking). The bar itself lives in the SQL fence, never here. Marking is off until both this and markColumn are set.
markColumn string null Name of the column whose cell carries the marker glyph — normally the key dimension.
markGlyph string '†' The marker. Appended to the markColumn cell in the data, so it travels with its row through sort, search and paging.
markNote string '' What the glyph means, printed as a legend under the table with a live count of the marked rows. Without it the glyph is a mystery, so give it one.
rows number 10 Rows per page before pagination.
sort string Initial sort, e.g. "net_revenue desc".
search boolean false Adds a search box.
totalRow boolean false Adds a summed footer row (per-column totalAgg).
title string Table title (Evidence-rendered, above the header).
rowNumbers boolean false Prefix an index column.

Token-sourced, no second look. The header fill, type, row height, row dividers, hover, the frozen-column divider and the heat ramp are driven entirely from the --s3-* tokens in app.css and the default colorScale in evidence.config.yaml — the wrapper duplicates no hex. freezeFirstColumn, heatmap and the four mark* props are the wrapper's only own props; every other prop is a thin pass-through, so any Evidence <DataTable> / <Column> prop (groupBy, link, conditional formatting, compact) works unchanged; the wrapper only sets the S3 look. A marked table passes Evidence a plain array rather than the query, so give its measure columns an explicit fmt; with marking off, or nothing marked, or the query still loading, the original data goes through untouched and keeps its skeleton and empty states.

Auto columns render their values. Evidence draws auto-generated columns as slot fallback, so the wrapper forwards the default slot only when <Column> children are actually supplied — otherwise the fallback fires and the cells fill, rather than blanking. The frozen column pins whatever the first column is (the key dimension, or the index column when rowNumbers is set).

DataBakedAt

The Data baked at footer stamp (CONTEXT.md). Load-bearing, not decorative: the deployed site is static with no runtime BigQuery connection, so a rebuild is the data refresh, and the scheduled refresh only asserts that Cloudflare accepted the deploy hook. A build that fails afterwards leaves the last good deployment live with a green workflow run, and this stamp is the only on-page evidence that the numbers moved.

Which is why it takes its timestamp from s3.build_stamp — a source query that ran against BigQuery at build time — and never from a client-side new Date(), a clock that ticks along happily over week-old Parquet. Rendered in UTC, and says so, because the reader compares it against a dbt Cloud run time rather than against their own timezone.

Data baked at

When the stamp is missing — a build that wrote no build_stamp row — it renders an em rule in the negative colour with build stamp unavailable — treat every figure above as unverified beside it, rather than quietly rendering nothing and letting a stale page look current.

Usage

Query the stamp in a page fence named baked (select baked_at from s3.build_stamp), then place the component at the foot of the page:

<DataBakedAt data={baked} />

Props

Prop Type Default Notes
data query result Rows from s3.build_stamp. The first row is used.
column string 'baked_at' Timestamp column within data.
label string 'Data baked at' The words before the stamp. Per CONTEXT.md, say Data baked atavoid "last updated", "generated at", "as at".