Display

Chart

Shows numbers as a picture, with a title and a one-line takeaway.

Installation

npx sinaui add chart

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.

Copies made, by script Naskh makes up most copies, and all three scripts are up since January.
Naskh makes up most copies, and all three scripts are up since January.
<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

Loans per month Loans dipped in April, then rose to their highest in June.
Loans dipped in April, then rose to their highest in June.
<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.

Reading-room visits Two rooms, six months.
Two rooms, six months.
<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.

Holdings by subject Medicine is a third of the collection.
Medicine is a third of the collection.
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.

How much of each shelf is cataloged Medicine and mathematics are nearly done; geography is under half.
Medicine and mathematics are nearly done; geography is under half.
<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.

Seats taken today (%) Baghdad is fullest; Samarkand has the most room.
Baghdad is fullest; Samarkand has the most room.
<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.

Returns this week Thursday had the most late returns.
Thursday had the most late returns.
<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

PropTypeDefault
titlestring (required)
descriptionstring
definitiona TanStack Charts defineChart(…)
aspectRationumber16 / 9
heightnumber (px)
centerSnippet: shown in the middle of the plot
tooltipBodySnippet: 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.
Introduction
Theming
Changelog
Button
Action button
Confirm button
Copy button
Hold to confirm
Split button
Calendar
Checkbox
Color picker
Combobox
Date picker
Date range picker
File drop
Form
Input
Mention input
Multi select
Number field
OTP input
Password input
Phone input
Radio cards
Radio group
Rich text editor
Search field
Segmented control
Select
Signature pad
Slider
Switch
Tag input
Upload
Wheel picker
Textarea
Time field
Toggle group
Toolbar
Alert dialog
Command palette
Context menu
Dialog
Drawer
Dropdown
Hover card
Popover
Tooltip
Announcement bar
Alert
Empty state
Meter
Progress
Rolling number
Skeleton
Spinner
Toast
Avatar
Avatar group
Badge
Card
Carousel
Chart
Chat
Code block
Data table
Folder
Kbd
Mind map
Resizable panels
Scroll area
Separator
Sortable
Table
Tag group
Virtual list
Audio player
Image
Image compare
Video
Visualizer
Breadcrumb
File tree
Menubar
Navigation menu
Pagination
Sidebar layout
Stepper
Tabs
Alarm clock
Clock
Dynamic island
Fluid orb
Hourglass
Stopwatch
Timer
AI chat
AI media
Auth
Board
Booking
Dashboard
File explorer
Messaging
Notifications
Settings
Team
Overview
Pricing
Agent chat
Calendar
Voice agent
Mail inbox
CMS
Blog
Marketing site
E-commerce
AI agents
SaaS landing page
SaaS app
Accordion
Dark theme
Light theme