Chart
Line, bar, doughnut and other charts drawn on a canvas by Chart.js, in the colours and type of the rest of the page.
A line chart takes rows and a series
Each chart is a canvas with an
aria-label inside a .chart box, and one
call to tsunami.chart() draws it. A series with
surface: true fills a flat, translucent area under
its line.
<div class="chart"><canvas id="ch-line" aria-label="Monthly sales, January to June"></canvas></div>
<script>
tsunami.chart('#ch-line', {
type: 'line',
data: [
{ label: 'Jan', sales: 4200 },
{ label: 'Feb', sales: 5100 },
{ label: 'Mar', sales: 4800 },
{ label: 'Apr', sales: 6300 },
{ label: 'May', sales: 7000 },
{ label: 'Jun', sales: 6600 },
],
series: { sales: { label: 'Sales', surface: true } },
});
</script>
Bars with a legend
legend: true shows the legend under the chart. The
default type is bar; the series take the colours of
the palette in turn.
<div class="chart"><canvas id="ch-bars" aria-label="Orders and returns by month"></canvas></div>
<script>
tsunami.chart('#ch-bars', {
legend: true,
data: [
{ label: 'Jan', orders: 320, returns: 24 },
{ label: 'Feb', orders: 410, returns: 31 },
{ label: 'Mar', orders: 380, returns: 22 },
{ label: 'Apr', orders: 470, returns: 36 },
{ label: 'May', orders: 520, returns: 29 },
{ label: 'Jun', orders: 495, returns: 33 },
],
series: {
orders: { label: 'Orders' },
returns: { label: 'Returns' },
},
});
</script>
The legend aligns to the start, center or end
legendAlign places the legend under the chart:
start, center (the default) or
end.
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1rem">
<div class="chart" style="aspect-ratio: 4 / 3"><canvas id="ch-align-start" aria-label="Orders by quarter, legend at the start"></canvas></div>
<div class="chart" style="aspect-ratio: 4 / 3"><canvas id="ch-align-center" aria-label="Orders by quarter, legend at the center"></canvas></div>
<div class="chart" style="aspect-ratio: 4 / 3"><canvas id="ch-align-end" aria-label="Orders by quarter, legend at the end"></canvas></div>
</div>
<script>
const rows = [
{ label: 'Q1', orders: 1110 },
{ label: 'Q2', orders: 1485 },
{ label: 'Q3', orders: 1320 },
{ label: 'Q4', orders: 1760 },
];
for (const legendAlign of ['start', 'center', 'end']) {
tsunami.chart('#ch-align-' + legendAlign, {
data: rows,
legend: true,
legendAlign,
series: { orders: { label: 'Orders' } },
});
}
</script>
Two lines: this year against last year
A color takes any CSS colour or
var(--ok-fg). dataset goes straight to
Chart.js, here to draw the comparison dashed.
<div class="chart"><canvas id="ch-two" aria-label="Sales this year and last year by month"></canvas></div>
<script>
tsunami.chart('#ch-two', {
type: 'line',
legend: true,
data: [
{ label: 'Jan', thisYear: 4200, lastYear: 3600 },
{ label: 'Feb', thisYear: 5100, lastYear: 4000 },
{ label: 'Mar', thisYear: 4800, lastYear: 4300 },
{ label: 'Apr', thisYear: 6300, lastYear: 4700 },
{ label: 'May', thisYear: 7000, lastYear: 5200 },
{ label: 'Jun', thisYear: 6600, lastYear: 5600 },
],
series: {
thisYear: { label: 'This year' },
lastYear: {
label: 'Last year',
color: 'var(--muted)',
dataset: { borderDash: [4, 4] },
},
},
});
</script>
Stacked bars
Stacking is a Chart.js option: set stacked on both
scales through options.
<div class="chart"><canvas id="ch-stack" aria-label="Sales by payment method, stacked by month"></canvas></div>
<script>
tsunami.chart('#ch-stack', {
legend: true,
data: [
{ label: 'Jan', card: 2400, paypal: 1100, transfer: 700 },
{ label: 'Feb', card: 2900, paypal: 1300, transfer: 900 },
{ label: 'Mar', card: 2700, paypal: 1200, transfer: 900 },
{ label: 'Apr', card: 3500, paypal: 1600, transfer: 1200 },
{ label: 'May', card: 3900, paypal: 1800, transfer: 1300 },
{ label: 'Jun', card: 3700, paypal: 1700, transfer: 1200 },
],
series: {
card: { label: 'Card' },
paypal: { label: 'PayPal' },
transfer: { label: 'Bank transfer' },
},
options: { scales: { x: { stacked: true }, y: { stacked: true } } },
});
</script>
Bars with a line on a second axis
A series with its own type is drawn as that type, and
an axis such as y1 gives it a scale of
its own on the right.
<div class="chart"><canvas id="ch-combo" aria-label="Orders by month as bars, average order value as a line"></canvas></div>
<script>
tsunami.chart('#ch-combo', {
legend: true,
data: [
{ label: 'Jan', orders: 320, aov: 62 },
{ label: 'Feb', orders: 410, aov: 58 },
{ label: 'Mar', orders: 380, aov: 64 },
{ label: 'Apr', orders: 470, aov: 67 },
{ label: 'May', orders: 520, aov: 71 },
{ label: 'Jun', orders: 495, aov: 69 },
],
series: {
orders: { label: 'Orders' },
aov: {
label: 'Average order value',
type: 'line',
axis: 'y1',
color: 'var(--warning-fg)',
},
},
});
</script>
A doughnut: each row is a slice
pie, doughnut and
polarArea take one series. Each row is a slice, named
by labelKey, and a row may carry its own
color; the others take the palette in turn.
<div class="chart" style="max-width: 24rem; aspect-ratio: 1"><canvas id="ch-donut" aria-label="Orders by payment method"></canvas></div>
<script>
tsunami.chart('#ch-donut', {
type: 'doughnut',
legend: true,
data: [
{ label: 'Card', orders: 1840 },
{ label: 'PayPal', orders: 920 },
{ label: 'Bank transfer', orders: 410, color: 'var(--muted)' },
{ label: 'Cash on delivery', orders: 230 },
],
series: { orders: { label: 'Orders' } },
options: { cutout: '65%' },
});
</script>
Performance: stacked bars and a total line
A series with total: true is listed last in the
tooltip, under a line. Chart.js options format the axis and the
tooltip.
<div class="chart"><canvas id="ch-perf" aria-label="Sales, returns and net revenue by month in US dollars"></canvas></div>
<script>
const usd = (n) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}).format(n);
tsunami.chart('#ch-perf', {
legend: true,
data: [
{ label: 'Jan', sales: 4200, returns: -300, net: 3900 },
{ label: 'Feb', sales: 5100, returns: -420, net: 4680 },
{ label: 'Mar', sales: 4800, returns: -260, net: 4540 },
{ label: 'Apr', sales: 6300, returns: -510, net: 5790 },
{ label: 'May', sales: 7000, returns: -380, net: 6620 },
{ label: 'Jun', sales: 6600, returns: -450, net: 6150 },
],
series: {
sales: { label: 'Sales', color: 'var(--accent)' },
returns: { label: 'Returns', color: 'var(--danger-fg)' },
net: {
label: 'Net revenue',
type: 'line',
color: 'var(--fg)',
total: true,
},
},
options: {
scales: {
x: { stacked: true },
y: { stacked: true, ticks: { callback: (v) => usd(v) } },
},
plugins: {
tooltip: {
callbacks: {
label: (c) => c.dataset.label + ': ' + usd(c.parsed.y),
},
},
},
},
});
</script>
A wide box
The box is 16:9. Another ratio is an inline
aspect-ratio on the .chart wrapper; the
canvas fills it.
<div class="chart" style="aspect-ratio: 4 / 1"><canvas id="ch-wide" aria-label="Visitors by month, wide"></canvas></div>
<script>
tsunami.chart('#ch-wide', {
type: 'line',
data: [
{ label: 'Jan', visitors: 8200 },
{ label: 'Feb', visitors: 9100 },
{ label: 'Mar', visitors: 8700 },
{ label: 'Apr', visitors: 10400 },
{ label: 'May', visitors: 11800 },
{ label: 'Jun', visitors: 11200 },
{ label: 'Jul', visitors: 12600 },
{ label: 'Aug', visitors: 12100 },
],
series: {
visitors: { label: 'Visitors', surface: true, color: 'var(--info-fg)' },
},
});
</script>
Calling it again redraws the chart
A second call on the same canvas replaces the data and returns a new Chart.js instance. Here two buttons choose the year.
<div class="stack" data-direction="row" data-gap="2">
<button type="button" class="btn" data-variant="primary" data-size="sm" data-chart-year="2026">2026</button>
<button type="button" class="btn" data-variant="outline" data-size="sm" data-chart-year="2025">2025</button>
</div>
<div class="chart"><canvas id="ch-redraw" aria-label="Sales by month for the chosen year"></canvas></div>
<script>
const years = {
2026: [4200, 5100, 4800, 6300, 7000, 6600],
2025: [3600, 4000, 4300, 4700, 5200, 5600],
};
const draw = (year) =>
tsunami.chart('#ch-redraw', {
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'].map((label, i) => ({
label,
sales: years[year][i],
})),
series: { sales: { label: 'Sales ' + year } },
legend: true,
});
draw(2026);
document.addEventListener('click', (e) => {
const button = e.target.closest('[data-chart-year]');
if (!button) return;
for (const b of document.querySelectorAll('[data-chart-year]'))
b.dataset.variant = b === button ? 'primary' : 'outline';
draw(button.dataset.chartYear);
});
</script>
A page loads Chart.js itself, before the Tsunami script
The drawing is
Chart.js 4.5.1 (MIT
licence). It is not in tsunami.min.js: a page that
has a chart loads dist/chart.min.js with its own
<script> first. A page with no chart does not
load it and is not affected. With the script missing,
tsunami.chart() throws an error that says so.
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tsunami.min.js"></script>
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.chart |
The box. It is 16:9 with a minimum height of 10rem.
Another ratio is an inline aspect-ratio. The
canvas fills it.
|
|
canvas |
The element Chart.js draws in. It is the first argument of
tsunami.chart(), as an element or a selector.
|
|
aria-label |
text | On the canvas. Names the chart for a screen reader; every canvas needs one. |
data-chart-initialized |
true |
Set by the script once the chart is drawn. |
Options
tsunami.chart(selector | element, options) takes
these options.
| Option | Values | Default | Description |
|---|---|---|---|
type |
bar, line, pie, doughnut, polarArea, radar, any Chart.js type | bar |
The chart type. |
labelKey |
a field of the rows | label |
The field that names a row: a point on the category axis, or a slice. |
data |
array of objects | [] |
One object per point or slice. Its other fields hold the values to draw. |
series |
object | every field but the label and color |
One entry per field to draw, keyed by the field name. |
legend |
true, false | false |
Shows the legend under the chart. |
legendAlign |
start, center, end | center |
Where the legend sits under the chart. |
tooltip |
true, false | true |
Shows the tooltip at the pointer. |
options |
Chart.js options object | Merged over the defaults at any depth. Arrays and functions replace. |
Series
Each entry of series takes these options. With no
series, every field of the rows but the label and
color is drawn.
| Series option | Values | Default | Description |
|---|---|---|---|
label |
text | the field name | The name in the legend and the tooltip. |
color |
any CSS colour, var(--ok-fg) |
the palette in turn |
The palette is --accent,
--info-fg, --ok-fg,
--warning-fg, --danger-fg. A
var() is resolved from the computed style.
|
type |
a Chart.js type | the chart type | Draws this series as another type, for example a line over bars. |
axis |
a scale id, for example y1 | Puts the series on a second scale, on the right. | |
surface |
true | A flat translucent area under a line. | |
total |
true | Lists the series last in the tooltip, under a line, and not with the others. | |
dataset |
Chart.js dataset object |
Passed to Chart.js, for example
{ borderDash: [4, 4] }.
|
Pie, doughnut and polar area charts have one series. Each row is a
slice, and a row's own color field colours it.
API
tsunami.chart(selector | element, options) draws the
chart and returns the Chart.js instance. Calling it again on the
same canvas destroys that chart and draws a new one.
-
tsunami.chart.get(el)returns the Chart.js instance on a canvas, ornull. -
tsunami.chart.destroy(el)destroys the chart and removesdata-chart-initialized.
A chart is destroyed by itself when its canvas leaves the page.
Colours are read from the CSS variables when a chart is drawn, so
on
tsunami:themechange every chart is drawn again, with
no animation. Under prefers-reduced-motion the
animation is off.
Events
| Event | Detail | Description |
|---|---|---|
tsunami:chartready |
{ chart } |
Bubbles from the canvas every time the chart is drawn, including a redraw and a redraw after a theme change. |
Accessibility
A canvas has no text of its own: give every one an
aria-label that says what the chart shows, and put
the figures that matter in the page as text or a table. The
tooltip is drawn on the canvas, so it is for the pointer.