Chart Gallery
sidebar_link: false) and renders every component in its key variants over
a real dev_matt mart. Not a client-facing dashboard.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)
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.
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.
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
seriessplits it, the lines take theCategorical palettefrom theECharts 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)
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.
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.
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
colorprop. Single-series bars always use the Mode C violet→teal gradient from theECharts theme. Multi-series categorical bars (theCategorical palette) are a separate component — seeS3StackedBarbelow.
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.
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.
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.
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
seriessplits it, the bands take theCategorical palettefrom theECharts 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.
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.
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.
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.
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.
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.
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
colorprop. Multi-series bars always draw from theCategorical palettein theECharts 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), useS3BarChart.
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
S3BarChartreads 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.
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.
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
colorprop. Slices always draw from theCategorical palettein theECharts theme— the bounded carve-out (ADR 0005). No hex is duplicated in the page or component. The total in the hole is summed fromvalue, 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.
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.
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 theECharts 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.
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.
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.
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
colorprop. Stages always draw from thesequentialneutral ramp in theECharts 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 anS3LineChart. 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 aseriessplits 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.
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.
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.
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
colorprop. A single-series scatter is the neutral Ash mark; a multi-series plot draws its groups from theCategorical palettein theECharts 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 anS3Scatter. The colour ramp is the theme's sequential neutral scale (Pale Blue Grey → Ash Grey) driven by avisualMap— 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.
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.
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
colorprop. Cells always draw from thesequentialneutral ramp in theECharts theme, interpolated by avisualMapbetween 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-accentslate (the same neutral mark a loneS3LineChartuses).
KPI strip
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.
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.
periodClosedandpriorCompleteare not the same flag.priorCompletedescribes the prior period (is the comparison basis honest?);periodCloseddescribes 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 minDenominator ⇒ marked. |
priorComplete | boolean | null | false ⇒ suppressed: 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'sfmtis an Excel-style number pattern (#,##0), not the chartyFmtshorthand (gbp/pct). Useprefix/suffixfor 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.
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:
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 calendar — current_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:
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
WARNING 2
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:
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.KpiCardhas 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).
| Channel | Net revenue | Orders | Daily 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.
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).
Sparklineis 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-accentneutral 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.)
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.
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.
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.
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
FlagCardsrenders 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.
† 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 inapp.cssand thedefaultcolorScaleinevidence.config.yaml— the wrapper duplicates no hex.freezeFirstColumn,heatmapand the fourmark*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 explicitfmt; with marking off, or nothing marked, or the query still loading, the originaldatagoes 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 whenrowNumbersis 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.
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 at — avoid "last updated", "generated at", "as at". |