@fundar/data-chart-telling
v0.0.14
Published
A Svelte 5 component library for building data visualisation charts, built on top of [svelteplot](https://svelteplot.dev). It provides low-level **plot** primitives (single graphs) and will grow to include higher-level **chart** compositions with titles,
Readme
data-chart-telling
A Svelte 5 component library for building data visualisation charts, built on top of svelteplot. It provides low-level plot primitives (single graphs) and will grow to include higher-level chart compositions with titles, legends, facets, and timelines.
Build & publish
After making changes, rebuild the package distributable:
pnpm buildprepack runs svelte-package followed by publint automatically before packing, so a publish will always ship a clean build:
# dry-run — inspect what will be uploaded
pnpm pack
# publish to the npm registry
pnpm publish --access publicBump
versioninpackage.jsonbefore each publish.
Tests
Unit tests (node)
Pure logic tests — no browser required.
pnpm testThis runs both the node unit project and the browser component project (Chromium via Playwright).
To run only the node unit tests:
pnpm vitest run --project=unitTo run only the browser component tests:
pnpm vitest run --project=browserStorybook interaction tests
Run stories as Vitest tests (requires Storybook to boot internally):
pnpm test:storiesStorybook
Serve the component explorer locally:
pnpm storybook
# → http://localhost:6006Build a static Storybook for deployment:
pnpm build-storybookChart types
Vocabulary: a plot is a single graph (axes + marks). A chart wraps a plot with a title, caption, legend, facets, and/or a timeline.
Charts
| Component | Import path | Description |
| --- | --- | --- |
| BarChart | $lib/charts/bar/Chart.svelte | Vertical bar chart with title/caption/legend/facet/timeline chrome. |
| HeatmapChart | $lib/charts/heatmap/Chart.svelte | Grid of colour-coded cells. Colour domain is computed over the full dataset for cross-facet comparability. |
| LineChart | $lib/charts/line/Chart.svelte | Line chart. Default timeline mode is upTo (cumulative reveal). |
| PyramidChart | $lib/charts/pyramid/Chart.svelte | Population-pyramid-style diverging bar chart with a shared zero baseline. |
Plots
| Component | Import path | Description |
| --- | --- | --- |
| BarPlot | $lib/plots/bar/Plot.svelte | Vertical bar chart. Supports multiple series and per-series range segments. |
| HeatmapPlot | $lib/plots/heatmap/Plot.svelte | Grid of colour-coded cells. |
| LinePlot | $lib/plots/line/Plot.svelte | Line chart. Supports multiple series and per-series range segments with independent stroke styles. |
| PyramidPlot | $lib/plots/pyramid/Plot.svelte | Diverging horizontal bar chart; each series is mirrored left or right of a shared zero baseline. |
All components accept explicit width and height in pixels (svelteplot does not auto-size).
Plot props
Plots accept either series (pre-built Series[]) or the data + x/y/z flat-rows path — these are mutually exclusive.
| Prop | Type | Default | BarPlot | HeatmapPlot | LinePlot | PyramidPlot |
| ---- | ---- | ------- | ------- | ----------- | -------- | ----------- |
| width | number | — | ✅ | ✅ | ✅ | ✅ |
| height | number | — | ✅ | ✅ | ✅ | ✅ |
| series | Series[] | — | ✅ | ✅ | ✅ | ✅ |
| data | TData[] | — | ✅ | ✅ | ✅ | ✅ |
| x | Accessor | — | ✅ | ✅ | ✅ | ✅ |
| y | Accessor | — | ✅ | ✅ | ✅ | ✅ |
| z | Accessor | — | ✅ | ✅ | ✅ | ✅ |
| segments | Segments<TStyle> | [] | ✅ | ✅ | ✅ | ✅ |
| markers | Marker[] | [] | ✅ | ✅ | ✅ | ✅ |
| scales | ScalesConfig | {} | ✅ | ✅ | ✅ | ✅ |
| margins | MarginConfig | {} | ✅ | ✅ | ✅ | ✅ |
| tooltip | TooltipRuntime | | ✅ | ✅ | ✅ | ✅ |
| styles | PlotStyles | | ✅ | ✅ | ✅ | ✅ |
PlotStyles sub-properties (plots)
| Sub-prop | Type | BarPlot default | HeatmapPlot default | LinePlot default | PyramidPlot default |
| -------- | ---- | --------------- | ------------------- | ---------------- | ------------------- |
| styles.values.show | boolean | false | true | false (last point) | false |
| styles.values.format | (v: number, seriesName: string) => string | v => \${v}`|v => `${v}`|(v, name) => `${name} - ${v}`|v => `${v}`|
|styles.values.anchor|'outside' | 'start' | 'middle' | 'end'|'outside'| — |'start'|'outside'|
|styles.values.dx|number| | | | |
|styles.values.dy|number| | | | |
|styles.values.textAnchor|'start' | 'end' | 'middle'| |'middle'| | |
|styles.values.lineAnchor|'top' | 'bottom' | 'middle'| |'middle'| | |
|styles.values.rotate|number| | | | |
|styles.values.class|string| | | | |
|styles.values.textClass|string| | | | |
|styles.colors.min|string| — | cfgcontinuous.min| — | — |
|styles.colors.max|string| — | cfgcontinuous.max| — | — |
|styles.colors.left|string| — | — | — | palette index 0 |
|styles.colors.right|string` | — | — | — | palette index 1 |
Chart props
| Prop | Type | Default | BarChart | HeatmapChart | LineChart | PyramidChart |
| ---- | ---- | ------- | -------- | ------------ | --------- | ------------ |
| width | number | — | ✅ | ✅ | ✅ | ✅ |
| height | number | — | ✅ | ✅ | ✅ | ✅ |
| data | TRow[] | — | ✅ | ✅ | ✅ | ✅ |
| x | Accessor | — | ✅ | ✅ | ✅ | ✅ |
| y | Accessor | — | ✅ | ✅ | ✅ | ✅ |
| z | Accessor | — | ✅ | ✅ | ✅ | ✅ |
| title | string | | ✅ | ✅ | ✅ | ✅ |
| subtitle | string | | ✅ | ✅ | ✅ | ✅ |
| caption | string | | ✅ | ✅ | ✅ | ✅ |
| header | Snippet | | ✅ | ✅ | ✅ | ✅ |
| footer | Snippet | | ✅ | ✅ | ✅ | ✅ |
| legend | LegendSection[] | | ✅ | ✅ | ✅ | ✅ |
| segments | Segments<TStyle> | [] | ✅ | ✅ | ✅ | ✅ |
| markers | Marker[] | [] | ✅ | ✅ | ✅ | ✅ |
| scales | ScalesConfig | {} | ✅ | ✅ | ✅ | ✅ |
| margins | MarginConfig | {} | ✅ | ✅ | ✅ | ✅ |
| facet | FacetConfig | | ✅ | ✅ | ✅ | ✅ |
| timeline | TimelineConfig | | ✅ | ✅ | ✅ | ✅ |
| tooltip | TooltipProp | | ✅ | ✅ | ✅ | ✅ |
| styles | PlotStyles | | ✅ | ✅ | ✅ | ✅ |
header and footer are Svelte 5 snippets that replace the string props when richer markup is needed (custom styles, logos, links, etc.). When header is provided it takes full precedence over title/subtitle; when footer is provided it takes full precedence over caption. Use the natural snippet syntax:
<LineChart ...>
{#snippet header()}
<div style="display:flex; justify-content:space-between">
<h3 style="color:#2563eb">My Chart</h3>
<img src="/logo.svg" alt="Logo" />
</div>
{/snippet}
{#snippet footer()}
<small>Source: <a href="...">Dataset</a> · Updated Q3 2024</small>
{/snippet}
</LineChart>See the PlotStyles sub-properties table above — charts accept the same styles shape and pass it through to their underlying plot.
Segments
The segments prop separates data ranges from visual styling. Every plot kind shares the same underlying types; the style shape (TStyle) differs per mark.
| Plot | segments type |
| ---- | --------------- |
| LinePlot / LineChart | Segments<LineSegmentStyle> |
| BarPlot / BarChart | Segments<BarSegmentStyle> |
| PyramidPlot / PyramidChart | Segments<BarSegmentStyle> |
| HeatmapPlot / HeatmapChart | Segments<CellSegmentStyle> ('default' key only) |
Core types
type Range = { from?: AxisValue; to?: AxisValue }; // inclusive on both sides
type Area = { x?: Range; y?: Range }; // 2-D rectangular region
type Segment<TStyle> = { areas?: Area[]; style?: TStyle };A Segment matches a data point (or heatmap cell) when the point falls inside any of its areas. Omitting areas creates a catch-all that matches every data point — but catch-alls are always evaluated last, regardless of their position in the list, so area-specific segments always win. This lets a catch-all placed first act as a fallback baseline that highlighted ranges override.
An Area restricts by x, y, or both. Both bounds of a Range are inclusive. Omitting x or y leaves that axis unrestricted.
Segments structure
segments is always an array of groups. Each group is a plain object whose keys map to Segment[] lists:
// 'default' key — applies its segments to every series
type DefaultSegmentsMap<TStyle> = { default: Segment<TStyle>[] };
// named keys — applies segments only to the named series
type SeriesAwareSegmentsMap<TStyle> = Record<string, Segment<TStyle>[]>;
// one element of the array — either form, or a mix of both key types
type SegmentsStyle<TStyle> = DefaultSegmentsMap<TStyle> | SeriesAwareSegmentsMap<TStyle>;
// the prop type: an ordered list of groups
type Segments<TStyle> = SegmentsStyle<TStyle>[];Having multiple groups lets you organise segments by concern — for example one group for per-series stroke colours and a second for shared dot markers applied via 'default'. Groups are processed in order and their resolved segments are concatenated per series before evaluation.
Examples
Line chart — baseline colour with a highlighted range (catch-all as fallback):
<LinePlot
{width} {height}
series={[{ name: 'gdp', x: 'year', y: 'value', data: GDP }]}
segments={[
{
gdp: [
{ style: { stroke: { stroke: '#94a3b8', strokeWidth: 1 } } },
{ areas: [{ x: { from: 2008, to: 2010 } }],
style: { stroke: { stroke: '#ef4444', strokeWidth: 3 } } },
],
},
]}
/>Line chart — per-series baseline colours + shared dot markers (two-group approach):
<LinePlot
{width} {height}
data={LIFE_EXPECTANCY} x="year" y="value" z="sex"
segments={[
// Group 1: per-series catch-all stroke colours
{ female: [{ style: { stroke: { stroke: 'steelblue' } } }],
male: [{ style: { stroke: { stroke: 'tomato' } } }] },
// Group 2: dots for all series in a specific range
{ default: [
{ areas: [{ x: { from: 2010, to: 2012 } }],
style: { dots: { dotRadius: 5, dotSymbol: 'circle' } } },
],
},
]}
/>Heatmap — highlight specific columns, rows, and individual cells:
<HeatmapChart
{width} {height} {data} {x} {y} {z}
segments={[
{
default: [
{ areas: [{ x: { from: 'Q1', to: 'Q1' } }, { x: { from: 'Q3', to: 'Q3' } }],
style: { stroke: '#f59e0b', strokeWidth: 2 } },
{ areas: [{ x: { from: 'Q2', to: 'Q2' }, y: { from: 1, to: 2 } }],
style: { fill: '#ef4444', fillOpacity: 0.8 } },
],
},
]}
/>Omitting segments (or passing []) renders every mark in its default palette colour.
Public API
import {
...
} from '@fundar/data-chart-telling';