Display
Chart
Shows numbers as a picture, with a title and a one-line takeaway.
Installation
npx sinaui add chartCopy src/lib/ui/Chart.svelte and tokens.css, then install TanStack
Charts:
pnpm add -E @tanstack/charts@0.18.0The exact version is pinned: TanStack Charts is before 1.0, so a new minor version may change its API. Check its release notes before upgrading.
Examples
Each chart draws itself in the first time it's scrolled into view. Focus one and use the arrow keys to read it point by point; hovering shows the same tooltips.
Area
Stacked series, with a legend.
<script lang="ts">
import { areaY, colorLegend, colorLegendItems, defineChart, stack } from '@tanstack/charts';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { scalePoint } from '@tanstack/charts/scales/point';
import { tooltip } from '@tanstack/charts/tooltip';
import Chart from '#lib/ui/Chart.svelte';
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
const scripts = {
Naskh: [64, 72, 80, 78, 91, 98],
Thuluth: [22, 25, 24, 30, 33, 36],
Kufic: [12, 10, 14, 13, 15, 18]
};
const copies = Object.entries(scripts).flatMap(([script, counts]) =>
counts.map((count, i) => ({ month: months[i], script, count }))
);
const definition = defineChart({
marks: [
areaY(copies, {
x: 'month',
y: 'count',
color: 'script',
layout: stack({ order: Object.keys(scripts) }),
fillOpacity: 0.75
})
],
scales: {
x: { scale: () => scalePoint<string>().padding(0.1), axis: { label: 'Month' } },
y: {
scale: scaleLinear,
nice: true,
grid: true,
axis: { label: 'Copies', ticks: { count: 5 } }
}
},
color: {
domain: Object.keys(scripts),
legend: colorLegend({
label: 'Script',
items: colorLegendItems({ justify: 'start', gap: 16 })
})
},
tooltip
});
</script>
<Chart
title="Copies made, by script"
description="Naskh makes up most copies, and all three scripts are up since January."
{definition}
/>Bar
<script lang="ts">
import { barY, defineChart } from '@tanstack/charts';
import { scaleBand } from '@tanstack/charts/scales/band';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { tooltip } from '@tanstack/charts/tooltip';
import Chart from '#lib/ui/Chart.svelte';
const loans = [
{ month: 'Jan', loans: 412 },
{ month: 'Feb', loans: 486 },
{ month: 'Mar', loans: 531 },
{ month: 'Apr', loans: 468 },
{ month: 'May', loans: 597 },
{ month: 'Jun', loans: 642 }
];
const definition = defineChart({
marks: [barY(loans, { x: 'month', y: 'loans', radius: 4 })],
scales: {
x: { scale: () => scaleBand<string>().padding(0.3), axis: { label: 'Month' } },
y: {
scale: scaleLinear,
nice: true,
grid: true,
axis: { label: 'Loans', ticks: { count: 5 } }
}
},
tooltip
});
</script>
<Chart
title="Loans per month"
description="Loans dipped in April, then rose to their highest in June."
{definition}
/>Line
Series take their colors from the theme, in order, in light and dark.
<script lang="ts">
import { colorLegend, colorLegendItems, defineChart, lineY } from '@tanstack/charts';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { scalePoint } from '@tanstack/charts/scales/point';
import { tooltip } from '@tanstack/charts/tooltip';
import Chart from '#lib/ui/Chart.svelte';
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
const visits = [
...[180, 210, 245, 230, 290, 320].map((v, i) => ({
month: months[i],
room: 'Baghdad',
visits: v
})),
...[140, 150, 190, 220, 215, 260].map((v, i) => ({
month: months[i],
room: 'Córdoba',
visits: v
}))
];
const definition = defineChart({
marks: [lineY(visits, { x: 'month', y: 'visits', z: 'room', strokeWidth: 2.5, points: true })],
scales: {
x: { scale: () => scalePoint<string>().padding(0.3), axis: { label: 'Month' } },
y: {
scale: scaleLinear,
nice: true,
grid: true,
axis: { label: 'Visits', ticks: { count: 5 } }
}
},
color: {
legend: colorLegend({
label: 'Reading room',
items: colorLegendItems({ justify: 'start', gap: 16 })
})
},
tooltip
});
</script>
<Chart title="Reading-room visits" description="Two rooms, six months." {definition} />Pie
A donut, with the total in the middle.
3,670 manuscripts
<script lang="ts">
import { defineChart } from '@tanstack/charts';
import { pie, polar, radialArc } from '@tanstack/charts/polar';
import { tooltip } from '@tanstack/charts/tooltip';
import Chart from '#lib/ui/Chart.svelte';
const holdings = [
{ subject: 'Medicine', count: 1240 },
{ subject: 'Astronomy', count: 860 },
{ subject: 'Mathematics', count: 720 },
{ subject: 'Philosophy', count: 540 },
{ subject: 'Geography', count: 310 }
];
const total = holdings.reduce((sum, h) => sum + h.count, 0);
const subjects = holdings.map((h) => h.subject);
const definition = defineChart({
marks: [
polar({
inset: 8,
radiusRatio: 0.9,
marks: [
radialArc(pie(holdings, { value: 'count' }), {
innerRadius: ({ radius }) => radius * 0.62,
cornerRadius: 4,
padAngle: () => 0.012,
color: 'subject',
key: 'subject'
})
],
scales: { angle: null, radius: null }
})
],
scales: { x: null, y: null },
color: { domain: subjects },
tooltip
});
</script>
{#snippet center()}
<strong class="total">{new Intl.NumberFormat().format(total)}</strong>
<span class="unit">manuscripts</span>
{/snippet}
<Chart
{center}
title="Holdings by subject"
description="Medicine is a third of the collection."
{definition}
/>
<style>
.total {
font-size: 1.5rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.unit {
color: var(--ui-muted);
font-size: 0.8125rem;
}
</style>Radar
One value per direction, on a polygon grid.
<script lang="ts">
import { defineChart } from '@tanstack/charts';
import { angleGrid, polar, radialArea, radialGrid, radialLine } from '@tanstack/charts/polar';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { scalePoint } from '@tanstack/charts/scales/point';
import { tooltip } from '@tanstack/charts/tooltip';
import Chart from '#lib/ui/Chart.svelte';
// Share of each subject's shelf that is cataloged, 0 to 1.
const cataloged = [
{ subject: 'Medicine', share: 0.92 },
{ subject: 'Astronomy', share: 0.78 },
{ subject: 'Mathematics', share: 0.85 },
{ subject: 'Philosophy', share: 0.6 },
{ subject: 'Geography', share: 0.48 },
{ subject: 'Poetry', share: 0.7 }
];
// Repeating the first point closes the shape.
const closed = [...cataloged, cataloged[0]];
const definition = defineChart({
marks: [
polar({
radiusRatio: 0.72,
scales: {
angle: {
scale: scalePoint<string>().domain(cataloged.map((c) => c.subject)),
wrap: true
},
radius: { scale: scaleLinear().domain([0, 1]) }
},
guides: [
radialGrid({ values: [0.25, 0.5, 0.75, 1], shape: 'polygon', strokeOpacity: 0.15 }),
angleGrid({ labels: true, strokeOpacity: 0.15 })
],
marks: [
radialArea(closed, {
angle: 'subject',
radius: 'share',
fill: 'var(--ts-chart-1)',
fillOpacity: 0.2
}),
radialLine(closed, {
angle: 'subject',
radius: 'share',
stroke: 'var(--ts-chart-1)',
strokeWidth: 2,
points: true
})
]
})
],
scales: { x: null, y: null },
tooltip
});
</script>
<Chart
title="How much of each shelf is cataloged"
description="Medicine and mathematics are nearly done; geography is under half."
{definition}
/>Radial
Bars around a circle.
<script lang="ts">
import { colorLegend, colorLegendItems, defineChart } from '@tanstack/charts';
import { polar, radialBarAngle } from '@tanstack/charts/polar';
import { scaleBand } from '@tanstack/charts/scales/band';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { tooltip } from '@tanstack/charts/tooltip';
import Chart from '#lib/ui/Chart.svelte';
const seats = [
{ room: 'Baghdad', taken: 86 },
{ room: 'Fez', taken: 71 },
{ room: 'Córdoba', taken: 64 },
{ room: 'Cairo', taken: 52 },
{ room: 'Samarkand', taken: 39 }
];
const rooms = seats.map((s) => s.room);
const bar = { radius: 'room', key: 'room', cornerRadius: 'full' } as const;
const definition = defineChart({
marks: [
polar({
radiusRatio: 0.9,
scales: {
angle: { scale: scaleLinear().domain([0, 100]) },
radius: {
scale: () => scaleBand<string>().domain(rooms).padding(0.28),
range: [({ radius }) => radius * 0.28, ({ radius }) => radius]
}
},
marks: [
// A faint full ring behind each bar: the whole of the room.
radialBarAngle(
seats.map((s) => ({ ...s, taken: 100 })),
{ ...bar, angle: 'taken', fill: 'var(--ui-subtle)' }
),
radialBarAngle(seats, { ...bar, angle: 'taken', color: 'room' })
]
})
],
scales: { x: null, y: null },
color: {
domain: rooms,
legend: colorLegend({ items: colorLegendItems({ justify: 'center', gap: 16 }) })
},
tooltip
});
</script>
<Chart
title="Seats taken today (%)"
description="Baghdad is fullest; Samarkand has the most room."
{definition}
/>Custom tooltip
Your own tooltip content, here with a second value the bars don't show.
<script lang="ts">
import { barY, defineChart } from '@tanstack/charts';
import { scaleBand } from '@tanstack/charts/scales/band';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { tooltip } from '@tanstack/charts/tooltip';
import Chart from '#lib/ui/Chart.svelte';
const returns = [
{ day: 'Sat', onTime: 48, late: 6 },
{ day: 'Sun', onTime: 52, late: 9 },
{ day: 'Mon', onTime: 61, late: 4 },
{ day: 'Tue', onTime: 57, late: 7 },
{ day: 'Wed', onTime: 63, late: 5 },
{ day: 'Thu', onTime: 44, late: 11 }
];
const nf = new Intl.NumberFormat();
const definition = defineChart({
marks: [barY(returns, { x: 'day', y: 'onTime', radius: 4 })],
scales: {
x: { scale: () => scaleBand<string>().padding(0.3), axis: { label: 'Day' } },
y: {
scale: scaleLinear,
nice: true,
grid: true,
axis: { label: 'Returned on time', ticks: { count: 4 } }
}
},
tooltip
});
</script>
{#snippet tooltipBody({ points }: { points: readonly { datum: unknown }[] })}
{@const row = points[0]?.datum as (typeof returns)[number] | undefined}
{#if row}
<div class="tip">
<strong>{row.day}</strong>
<span><i class="swatch on"></i>On time <b>{nf.format(row.onTime)}</b></span>
<span><i class="swatch late"></i>Late <b>{nf.format(row.late)}</b></span>
</div>
{/if}
{/snippet}
<Chart
title="Returns this week"
description="Thursday had the most late returns."
{definition}
{tooltipBody}
/>
<style>
.tip {
display: grid;
gap: 0.25rem;
min-inline-size: 9rem;
}
.tip span {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--ui-muted);
}
.tip b {
margin-inline-start: auto;
color: var(--ui-fg);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.swatch {
inline-size: 0.625rem;
block-size: 0.625rem;
border-radius: 2px;
}
.on {
background: var(--ts-chart-1);
}
.late {
background: var(--ts-chart-2);
}
</style>Props
| Prop | Type | Default |
|---|---|---|
title | string (required) | |
description | string | |
definition | a TanStack Charts defineChart(…) | |
aspectRatio | number | 16 / 9 |
height | number (px) | |
center | Snippet: shown in the middle of the plot | |
tooltipBody | Snippet: your own tooltip content |
Accessibility
- The chart is named by its title and described by its description, both also shown on screen.
- It can be focused and read point by point with the arrow keys; each point shows its tooltip.
- Its space is reserved before it draws, so the page doesn't shift.
- Say what the chart shows in the description: not everyone can see the picture.