Material 3 charts gallery
One screen, the whole viz family. A compact operations dashboard that puts
md-line-chart, md-area-chart,
md-bar-chart, and md-pie-chart
next to the small stuff — md-sparkline KPI tiles,
md-meter readings, and an md-progress-indicator.
A segmented button swaps palette presets by writing --md-sys-color-* custom
properties onto the gallery wrapper: the CSS-driven components re-theme
instantly, and the Canvas2D charts get the one nudge they need — a series
reassignment — to repaint in the new brand.
Live preview — pick a palette preset to re-theme every chart
Northwind Cloud — August 2026
Usage, revenue, and platform health
11,540
visits
visits
Deploys per day
14
p95 latency
318 ms
Error rate
0.21%
Nightly export — 412 of 605 tables
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<md-segmented-button-set aria-label="Chart palette preset">
<md-segmented-button value="default" label="Baseline" selected></md-segmented-button>
<md-segmented-button value="forest" label="Forest"></md-segmented-button>
</md-segmented-button-set>
<md-line-chart label="Weekly active users" height="240px" show-marks curve="monotone"></md-line-chart>
<md-area-chart label="Storage by type" height="230px"></md-area-chart>
<md-bar-chart label="Revenue by region" height="230px"></md-bar-chart>
<md-pie-chart label="Traffic by source" inner-radius="45%" legend="bottom" show-labels="false"></md-pie-chart>
<md-sparkline variant="bar" color="primary" height="36px"></md-sparkline>
<md-meter value="62" label="Storage used" show-label show-value value-text="1.24 TB of 2 TB"></md-meter>
<md-progress-indicator value="412" max="605" label="Nightly export"></md-progress-indicator>
<script type="module">
const wrap = document.querySelector('[data-gallery]');
const presetSet = document.querySelector('md-segmented-button-set');
const line = document.querySelector('[data-line]');
const area = document.querySelector('[data-area]');
const bar = document.querySelector('[data-bar]');
const pie = document.querySelector('[data-pie]');
const months = ['Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'];
// Arrays and objects never cross the attribute boundary — feed every
// chart its data as JS properties.
line.xAxis = { data: months, scale: 'category' };
line.yAxis = { label: 'Users', min: 0 };
line.series = [
{ label: 'Web', color: 'primary', data: [4820, 5110, 5580, 5490, 6120, 6740] },
{ label: 'Mobile', color: 'tertiary', data: [2210, 2480, 2900, 3350, 3610, 4020] },
];
area.xAxis = { data: months, scale: 'category' };
area.yAxis = { label: 'GB', min: 0 };
area.series = [
{ label: 'Media', color: 'primary', data: [520, 548, 600, 646, 700, 741] },
{ label: 'Documents', color: 'tertiary', data: [310, 335, 352, 380, 401, 428] },
{ label: 'Backups', color: 'secondary', data: [180, 182, 190, 204, 210, 222] },
];
area.valueFormatter = function (v) { return v == null ? '' : v + ' GB'; };
bar.xAxis = { data: ['North America', 'Europe', 'APAC', 'LATAM'] };
bar.yAxis = { label: 'USD (k)', min: 0 };
bar.series = [
{ label: 'FY25', color: 'secondary', data: [640, 520, 310, 190] },
{ label: 'FY26', color: 'primary', data: [720, 590, 405, 238] },
];
pie.data = [
{ label: 'Organic', value: 5200 },
{ label: 'Direct', value: 3120 },
{ label: 'Referral', value: 1980 },
{ label: 'Paid', value: 1240 },
];
const sparkDeploys = document.querySelector('[data-spark-deploys]');
const sparkLatency = document.querySelector('[data-spark-latency]');
const sparkErrors = document.querySelector('[data-spark-errors]');
const days = ['Aug 13', 'Aug 14', 'Aug 15', 'Aug 16', 'Aug 17', 'Aug 18', 'Aug 19', 'Aug 20'];
sparkDeploys.data = [6, 9, 7, 11, 8, 12, 10, 14];
sparkDeploys.labels = days;
sparkLatency.data = [412, 398, 405, 371, 362, 340, 331, 318];
sparkLatency.labels = days;
sparkLatency.valueFormatter = function (v) { return (v == null ? 0 : v) + ' ms'; };
sparkErrors.data = [0.42, 0.36, 0.44, 0.31, 0.28, 0.24, 0.26, 0.21];
sparkErrors.labels = days;
sparkErrors.valueFormatter = function (v) { return (v == null ? 0 : v) + '%'; };
// Palette presets: each swaps the MD3 color roles the series resolve to.
// 'default' is the absence of overrides — the site theme shows through.
const presets = {
forest: {
'--md-sys-color-primary': '#3d6838',
'--md-sys-color-on-primary': '#ffffff',
'--md-sys-color-primary-container': '#bef0b3',
'--md-sys-color-on-primary-container': '#255022',
'--md-sys-color-secondary': '#53634e',
'--md-sys-color-secondary-container': '#d6e8cd',
'--md-sys-color-on-secondary-container': '#3b4b37',
'--md-sys-color-tertiary': '#386569',
'--md-sys-color-tertiary-container': '#bcebef',
'--md-sys-color-on-tertiary-container': '#1e4d51',
},
ocean: {
'--md-sys-color-primary': '#00658e',
'--md-sys-color-on-primary': '#ffffff',
'--md-sys-color-primary-container': '#c7e7ff',
'--md-sys-color-on-primary-container': '#004c6c',
'--md-sys-color-secondary': '#4f616e',
'--md-sys-color-secondary-container': '#d2e5f5',
'--md-sys-color-on-secondary-container': '#374955',
'--md-sys-color-tertiary': '#635a7c',
'--md-sys-color-tertiary-container': '#e9ddff',
'--md-sys-color-on-tertiary-container': '#4b4266',
},
sunset: {
'--md-sys-color-primary': '#8f4c38',
'--md-sys-color-on-primary': '#ffffff',
'--md-sys-color-primary-container': '#ffdbd1',
'--md-sys-color-on-primary-container': '#723523',
'--md-sys-color-secondary': '#77574e',
'--md-sys-color-secondary-container': '#ffdbd1',
'--md-sys-color-on-secondary-container': '#5d4037',
'--md-sys-color-tertiary': '#6c5d2f',
'--md-sys-color-tertiary-container': '#f6e1a7',
'--md-sys-color-on-tertiary-container': '#534619',
},
};
// Union of every token any preset touches, so Baseline can clear them all.
const allTokens = {};
Object.keys(presets).forEach(function (name) {
Object.keys(presets[name]).forEach(function (token) { allTokens[token] = true; });
});
function applyPreset(name) {
Object.keys(allTokens).forEach(function (token) { wrap.style.removeProperty(token); });
const preset = presets[name];
if (preset) {
Object.keys(preset).forEach(function (token) {
wrap.style.setProperty(token, preset[token]);
});
}
// Meters, progress and buttons re-theme through CSS alone. The Canvas2D
// charts sample the tokens at paint time, so reassign their data to
// trigger a repaint (a new array reference is enough).
line.series = line.series.slice();
area.series = area.series.slice();
bar.series = bar.series.slice();
pie.data = pie.data.slice();
document.querySelectorAll('md-sparkline').forEach(function (s) {
s.data = s.data.slice();
});
}
presetSet.addEventListener('mdChange', function (e) {
applyPreset(e.detail[0]);
});
</script>import { useMemo, useState } from 'react';
import {
MdAreaChart, MdBarChart, MdLineChart, MdMeter, MdPieChart,
MdProgressIndicator, MdSegmentedButton, MdSegmentedButtonSet, MdSparkline
} from '@awc-ui/react';
const MONTHS = ['May', 'Jun', 'Jul', 'Aug'];
const DAYS = ['Aug 17', 'Aug 18', 'Aug 19', 'Aug 20'];
const LINE = [
{ label: 'Web', color: 'primary', data: [5580, 5490, 6120, 6740] },
{ label: 'Mobile', color: 'tertiary', data: [2900, 3350, 3610, 4020] }
];
const AREA = [
{ label: 'Media', color: 'primary', data: [600, 646, 700, 741] },
{ label: 'Documents', color: 'tertiary', data: [352, 380, 401, 428] },
{ label: 'Backups', color: 'secondary', data: [190, 204, 210, 222] }
];
const BARS = [
{ label: 'FY25', color: 'secondary', data: [640, 520, 310, 190] },
{ label: 'FY26', color: 'primary', data: [720, 590, 405, 238] }
];
const PIE = [
{ label: 'Organic', value: 5200 }, { label: 'Direct', value: 3120 },
{ label: 'Referral', value: 1980 }, { label: 'Paid', value: 1240 }
];
// Palette presets swap the MD3 roles the series resolve to. 'default' is the
// absence of overrides — the site theme shows through. (Full presets also
// swap the on-* / container roles; trimmed to the roles the series sample.)
const PRESETS = {
default: {},
forest: { '--md-sys-color-primary': '#3d6838', '--md-sys-color-secondary': '#53634e', '--md-sys-color-tertiary': '#386569' },
ocean: { '--md-sys-color-primary': '#00658e', '--md-sys-color-secondary': '#4f616e', '--md-sys-color-tertiary': '#635a7c' },
sunset: { '--md-sys-color-primary': '#8f4c38', '--md-sys-color-secondary': '#77574e', '--md-sys-color-tertiary': '#6c5d2f' }
};
const tile = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '12px 16px', borderRadius: '12px', background: 'var(--md-sys-color-surface-container-low)' };
const row = { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '16px' };
export default function ChartsGallery() {
const [preset, setPreset] = useState('default');
// The Canvas2D charts sample the tokens at paint time and only repaint when
// their data changes — so every preset swap hands them FRESH references.
const charts = useMemo(() => ({
line: LINE.map((s) => ({ ...s })),
area: AREA.map((s) => ({ ...s })),
bars: BARS.map((s) => ({ ...s })),
pie: PIE.map((d) => ({ ...d })),
deploys: [8, 12, 10, 14],
latency: [362, 340, 331, 318]
}), [preset]);
return (
<div style={{ display: 'grid', gap: '16px', ...PRESETS[preset] }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: '12px' }}>
<div>
<h2 style={{ margin: 0 }}>Northwind Cloud — August 2026</h2>
<span>Usage, revenue, and platform health</span>
</div>
<MdSegmentedButtonSet aria-label="Chart palette preset" onMdChange={(e) => setPreset(e.detail[0])}>
<MdSegmentedButton value="default" label="Baseline" selected={preset === 'default'} />
<MdSegmentedButton value="forest" label="Forest" selected={preset === 'forest'} />
<MdSegmentedButton value="ocean" label="Ocean" selected={preset === 'ocean'} />
<MdSegmentedButton value="sunset" label="Sunset" selected={preset === 'sunset'} />
</MdSegmentedButtonSet>
</div>
<MdLineChart label="Weekly active users" subtitle="May – August" heightProp="240px"
showMarks curve="monotone" xAxis={{ data: MONTHS, scale: 'category' }}
yAxis={{ label: 'Users', min: 0 }} series={charts.line} />
<div style={row}>
<MdAreaChart label="Storage by type" subtitle="Stacked, GB" heightProp="230px"
xAxis={{ data: MONTHS, scale: 'category' }} yAxis={{ label: 'GB', min: 0 }}
series={charts.area} valueFormatter={(v) => (v == null ? '' : v + ' GB')} />
<MdBarChart label="Revenue by region" subtitle="USD thousands" heightProp="230px"
cornerRadius={4} xAxis={{ data: ['North America', 'Europe', 'APAC', 'LATAM'] }}
yAxis={{ label: 'USD (k)', min: 0 }} series={charts.bars} />
</div>
<div style={row}>
<MdPieChart label="Traffic by source" subtitle="Last 30 days" showLabels={false}
innerRadius="45%" legend="bottom" heightProp="300px" data={charts.pie}>
<div slot="center" style={{ textAlign: 'center' }}>
<strong>11,540</strong><br />visits
</div>
</MdPieChart>
<div style={{ display: 'grid', gap: '12px', alignContent: 'start' }}>
<div style={tile}>
<div><div>Deploys per day</div><strong>14</strong></div>
<MdSparkline variant="bar" color="primary" heightProp="36px" style={{ width: '120px' }}
data={charts.deploys} labels={DAYS} />
</div>
<div style={tile}>
<div><div>p95 latency</div><strong>318 ms</strong></div>
<MdSparkline color="tertiary" heightProp="36px" style={{ width: '120px' }}
data={charts.latency} labels={DAYS}
valueFormatter={(v) => (v == null ? 0 : v) + ' ms'} />
</div>
<div style={{ ...tile, padding: '16px' }}>
<div style={{ display: 'grid', gap: '12px', flex: 1 }}>
<MdMeter value={62} label="Storage used" showLabel showValue valueText="1.24 TB of 2 TB" />
<div>
<div>Nightly export — 412 of 605 tables</div>
<MdProgressIndicator value={412} max={605} label="Nightly export" />
</div>
</div>
<MdMeter variant="circular" value={84} size={88} thickness={8}
label="API quota" showLabel showValue color="tertiary" />
</div>
</div>
</div>
</div>
);
}import { Component } from '@angular/core';
import {
MdAreaChart, MdBarChart, MdLineChart, MdMeter, MdPieChart,
MdProgressIndicator, MdSegmentedButton, MdSegmentedButtonSet, MdSparkline
} from '@awc-ui/angular';
// Palette presets swap the MD3 roles the chart series resolve to. Baseline is
// the absence of overrides — a null binding removes the inline property, so
// the site theme (dark mode included) shows through.
const PRESETS: any = {
forest: { primary: '#3d6838', secondary: '#53634e', tertiary: '#386569' },
ocean: { primary: '#00658e', secondary: '#4f616e', tertiary: '#635a7c' },
sunset: { primary: '#8f4c38', secondary: '#77574e', tertiary: '#6c5d2f' }
};
@Component({
selector: 'app-charts-gallery',
standalone: true,
imports: [
MdAreaChart, MdBarChart, MdLineChart, MdMeter, MdPieChart,
MdProgressIndicator, MdSegmentedButton, MdSegmentedButtonSet, MdSparkline
],
template:
'<div class="gallery"' +
' [style.--md-sys-color-primary]="tokens?.primary || null"' +
' [style.--md-sys-color-secondary]="tokens?.secondary || null"' +
' [style.--md-sys-color-tertiary]="tokens?.tertiary || null">' +
' <header>' +
' <div><h2>Northwind Cloud — August 2026</h2><span>Usage, revenue, and platform health</span></div>' +
' <md-segmented-button-set aria-label="Chart palette preset" (mdChange)="onPreset($event)">' +
' @for (p of segments; track p.value) {' +
' <md-segmented-button [value]="p.value" [label]="p.label" [selected]="preset === p.value"></md-segmented-button>' +
' }' +
' </md-segmented-button-set>' +
' </header>' +
' <md-line-chart label="Weekly active users" subtitle="May – August" heightProp="240px" [showMarks]="true" curve="monotone" [xAxis]="monthsAxis" [yAxis]="usersAxis" [series]="lineSeries"></md-line-chart>' +
' <div class="row">' +
' <md-area-chart label="Storage by type" subtitle="Stacked, GB" heightProp="230px" [xAxis]="monthsAxis" [yAxis]="gbAxis" [series]="areaSeries" [valueFormatter]="fmtGb"></md-area-chart>' +
' <md-bar-chart label="Revenue by region" subtitle="USD thousands" heightProp="230px" [cornerRadius]="4" [xAxis]="regionAxis" [yAxis]="usdAxis" [series]="barSeries"></md-bar-chart>' +
' </div>' +
' <div class="row">' +
' <md-pie-chart label="Traffic by source" subtitle="Last 30 days" [showLabels]="false" innerRadius="45%" legend="bottom" heightProp="300px" [data]="pieData">' +
' <div slot="center" style="text-align: center;"><strong>11,540</strong><br>visits</div>' +
' </md-pie-chart>' +
' <div class="tiles">' +
' <div class="tile"><div><div>Deploys per day</div><strong>14</strong></div>' +
' <md-sparkline variant="bar" color="primary" heightProp="36px" style="width: 120px" [data]="deploys" [labels]="days"></md-sparkline></div>' +
' <div class="tile"><div><div>p95 latency</div><strong>318 ms</strong></div>' +
' <md-sparkline color="tertiary" heightProp="36px" style="width: 120px" [data]="latency" [labels]="days" [valueFormatter]="fmtMs"></md-sparkline></div>' +
' <div class="tile">' +
' <div class="gauges">' +
' <md-meter [value]="62" label="Storage used" [showLabel]="true" [showValue]="true" valueText="1.24 TB of 2 TB"></md-meter>' +
' <div><div>Nightly export — 412 of 605 tables</div>' +
' <md-progress-indicator [value]="412" [max]="605" label="Nightly export"></md-progress-indicator></div>' +
' </div>' +
' <md-meter variant="circular" [value]="84" [size]="88" [thickness]="8" label="API quota" [showLabel]="true" [showValue]="true" color="tertiary"></md-meter>' +
' </div>' +
' </div>' +
' </div>' +
'</div>',
styles: [
'.gallery { display: grid; gap: 16px; } header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; } ' +
'.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; } .tiles { display: grid; gap: 12px; align-content: start; } ' +
'.tile { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-radius: 12px; background: var(--md-sys-color-surface-container-low); } ' +
'.gauges { display: grid; gap: 12px; flex: 1; }'
]
})
export class ChartsGalleryComponent {
segments = [
{ value: 'default', label: 'Baseline' }, { value: 'forest', label: 'Forest' },
{ value: 'ocean', label: 'Ocean' }, { value: 'sunset', label: 'Sunset' }
];
preset = 'default';
tokens: any = null;
days = ['Aug 17', 'Aug 18', 'Aug 19', 'Aug 20'];
monthsAxis: any = { data: ['May', 'Jun', 'Jul', 'Aug'], scale: 'category' };
usersAxis: any = { label: 'Users', min: 0 };
gbAxis: any = { label: 'GB', min: 0 };
regionAxis: any = { data: ['North America', 'Europe', 'APAC', 'LATAM'] };
usdAxis: any = { label: 'USD (k)', min: 0 };
lineSeries: any[] = [
{ label: 'Web', color: 'primary', data: [5580, 5490, 6120, 6740] },
{ label: 'Mobile', color: 'tertiary', data: [2900, 3350, 3610, 4020] }
];
areaSeries: any[] = [
{ label: 'Media', color: 'primary', data: [600, 646, 700, 741] },
{ label: 'Documents', color: 'tertiary', data: [352, 380, 401, 428] },
{ label: 'Backups', color: 'secondary', data: [190, 204, 210, 222] }
];
barSeries: any[] = [
{ label: 'FY25', color: 'secondary', data: [640, 520, 310, 190] },
{ label: 'FY26', color: 'primary', data: [720, 590, 405, 238] }
];
pieData: any[] = [
{ label: 'Organic', value: 5200 }, { label: 'Direct', value: 3120 },
{ label: 'Referral', value: 1980 }, { label: 'Paid', value: 1240 }
];
deploys = [8, 12, 10, 14];
latency = [362, 340, 331, 318];
fmtGb = (v: number | null | undefined) => (v == null ? '' : v + ' GB');
fmtMs = (v: number | null) => (v == null ? 0 : v) + ' ms';
onPreset(event: Event) {
const name = (event as CustomEvent<string[]>).detail[0];
this.preset = name;
this.tokens = PRESETS[name] || null;
// The Canvas2D charts sample the tokens at paint time and only repaint on
// a data change — a fresh array reference per chart is the whole nudge.
this.lineSeries = this.lineSeries.slice();
this.areaSeries = this.areaSeries.slice();
this.barSeries = this.barSeries.slice();
this.pieData = this.pieData.slice();
this.deploys = this.deploys.slice();
this.latency = this.latency.slice();
}
}<script setup lang="ts">
import { computed, ref } from 'vue';
import {
MdAreaChart, MdBarChart, MdLineChart, MdMeter, MdPieChart,
MdProgressIndicator, MdSegmentedButton, MdSegmentedButtonSet, MdSparkline
} from '@awc-ui/vue';
const MONTHS = ['May', 'Jun', 'Jul', 'Aug'];
const DAYS = ['Aug 17', 'Aug 18', 'Aug 19', 'Aug 20'];
const LINE = [
{ label: 'Web', color: 'primary', data: [5580, 5490, 6120, 6740] },
{ label: 'Mobile', color: 'tertiary', data: [2900, 3350, 3610, 4020] }
];
const AREA = [
{ label: 'Media', color: 'primary', data: [600, 646, 700, 741] },
{ label: 'Documents', color: 'tertiary', data: [352, 380, 401, 428] },
{ label: 'Backups', color: 'secondary', data: [190, 204, 210, 222] }
];
const BARS = [
{ label: 'FY25', color: 'secondary', data: [640, 520, 310, 190] },
{ label: 'FY26', color: 'primary', data: [720, 590, 405, 238] }
];
const PIE = [
{ label: 'Organic', value: 5200 }, { label: 'Direct', value: 3120 },
{ label: 'Referral', value: 1980 }, { label: 'Paid', value: 1240 }
];
// Palette presets swap the MD3 roles the series resolve to. Baseline is the
// absence of overrides — an empty style object lets the site theme through.
const PRESETS: Record<string, Record<string, string>> = {
forest: { '--md-sys-color-primary': '#3d6838', '--md-sys-color-secondary': '#53634e', '--md-sys-color-tertiary': '#386569' },
ocean: { '--md-sys-color-primary': '#00658e', '--md-sys-color-secondary': '#4f616e', '--md-sys-color-tertiary': '#635a7c' },
sunset: { '--md-sys-color-primary': '#8f4c38', '--md-sys-color-secondary': '#77574e', '--md-sys-color-tertiary': '#6c5d2f' }
};
const preset = ref('default');
const presetStyle = computed(() => PRESETS[preset.value] || {});
// The Canvas2D charts sample the tokens at paint time and only repaint when
// their data changes. fresh() reads preset, so every computed below re-runs
// on a palette swap and hands its chart a NEW reference — that is the nudge.
function fresh<T extends object>(rows: T[]): T[] {
void preset.value;
return rows.map((r) => ({ ...r }));
}
const lineSeries = computed(() => fresh(LINE));
const areaSeries = computed(() => fresh(AREA));
const barSeries = computed(() => fresh(BARS));
const pieData = computed(() => fresh(PIE));
const deploys = computed(() => { void preset.value; return [8, 12, 10, 14]; });
const latency = computed(() => { void preset.value; return [362, 340, 331, 318]; });
const fmtGb = (v: number | null | undefined) => (v == null ? '' : v + ' GB');
const fmtMs = (v: number | null) => (v == null ? 0 : v) + ' ms';
function onPreset(e: CustomEvent<string[]>) {
preset.value = e.detail[0];
}
</script>
<template>
<div class="gallery" :style="presetStyle">
<header>
<div><h2>Northwind Cloud — August 2026</h2><span>Usage, revenue, and platform health</span></div>
<MdSegmentedButtonSet aria-label="Chart palette preset" @md-change="onPreset">
<MdSegmentedButton value="default" label="Baseline" :selected="preset === 'default'" />
<MdSegmentedButton value="forest" label="Forest" :selected="preset === 'forest'" />
<MdSegmentedButton value="ocean" label="Ocean" :selected="preset === 'ocean'" />
<MdSegmentedButton value="sunset" label="Sunset" :selected="preset === 'sunset'" />
</MdSegmentedButtonSet>
</header>
<MdLineChart label="Weekly active users" subtitle="May – August" height-prop="240px"
:show-marks="true" curve="monotone" :x-axis="{ data: MONTHS, scale: 'category' }"
:y-axis="{ label: 'Users', min: 0 }" :series="lineSeries" />
<div class="row">
<MdAreaChart label="Storage by type" subtitle="Stacked, GB" height-prop="230px"
:x-axis="{ data: MONTHS, scale: 'category' }" :y-axis="{ label: 'GB', min: 0 }"
:series="areaSeries" :value-formatter="fmtGb" />
<MdBarChart label="Revenue by region" subtitle="USD thousands" height-prop="230px"
:corner-radius="4" :x-axis="{ data: ['North America', 'Europe', 'APAC', 'LATAM'] }"
:y-axis="{ label: 'USD (k)', min: 0 }" :series="barSeries" />
</div>
<div class="row">
<MdPieChart label="Traffic by source" subtitle="Last 30 days" :show-labels="false"
inner-radius="45%" legend="bottom" height-prop="300px" :data="pieData">
<div slot="center" style="text-align: center;"><strong>11,540</strong><br />visits</div>
</MdPieChart>
<div class="tiles">
<div class="tile">
<div><div>Deploys per day</div><strong>14</strong></div>
<MdSparkline variant="bar" color="primary" height-prop="36px" style="width: 120px"
:data="deploys" :labels="DAYS" />
</div>
<div class="tile">
<div><div>p95 latency</div><strong>318 ms</strong></div>
<MdSparkline color="tertiary" height-prop="36px" style="width: 120px"
:data="latency" :labels="DAYS" :value-formatter="fmtMs" />
</div>
<div class="tile">
<div class="gauges">
<MdMeter :value="62" label="Storage used" :show-label="true" :show-value="true"
value-text="1.24 TB of 2 TB" />
<div>
<div>Nightly export — 412 of 605 tables</div>
<MdProgressIndicator :value="412" :max="605" label="Nightly export" />
</div>
</div>
<MdMeter variant="circular" :value="84" :size="88" :thickness="8"
label="API quota" :show-label="true" :show-value="true" color="tertiary" />
</div>
</div>
</div>
</div>
</template>
<style scoped>
.gallery { display: grid; gap: 16px; }
header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
.tiles { display: grid; gap: 12px; align-content: start; }
.tile { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-radius: 12px; background: var(--md-sys-color-surface-container-low); }
.gauges { display: grid; gap: 12px; flex: 1; }
</style><script>
// The Svelte package ships the raw md-* elements — register them once on the client.
import { defineCustomElements } from '@awc-ui/svelte';
if (typeof window !== 'undefined') defineCustomElements(window); // SvelteKit: if (browser) ...
import { onMount } from 'svelte';
const MONTHS = ['May', 'Jun', 'Jul', 'Aug'];
const DAYS = ['Aug 17', 'Aug 18', 'Aug 19', 'Aug 20'];
// Palette presets swap the MD3 roles the chart series resolve to. Baseline
// is the absence of overrides — an empty style string lets the site theme
// (dark mode included) show through.
const PRESETS = {
forest: { primary: '#3d6838', secondary: '#53634e', tertiary: '#386569' },
ocean: { primary: '#00658e', secondary: '#4f616e', tertiary: '#635a7c' },
sunset: { primary: '#8f4c38', secondary: '#77574e', tertiary: '#6c5d2f' }
};
let preset = 'default';
let lineEl, areaEl, barEl, pieEl, deploysEl, latencyEl;
$: tokens = PRESETS[preset];
$: styleVars = tokens
? '--md-sys-color-primary:' + tokens.primary +
';--md-sys-color-secondary:' + tokens.secondary +
';--md-sys-color-tertiary:' + tokens.tertiary + ';'
: '';
// Arrays never cross the attribute boundary — assign chart data as element
// properties. The Canvas2D charts also sample the color tokens at paint
// time and only repaint on a data change, so this runs again on every
// preset swap: the fresh references are the repaint nudge.
function applyData() {
if (!lineEl) return;
lineEl.xAxis = { data: MONTHS, scale: 'category' };
lineEl.yAxis = { label: 'Users', min: 0 };
lineEl.series = [
{ label: 'Web', color: 'primary', data: [5580, 5490, 6120, 6740] },
{ label: 'Mobile', color: 'tertiary', data: [2900, 3350, 3610, 4020] }
];
areaEl.xAxis = { data: MONTHS, scale: 'category' };
areaEl.yAxis = { label: 'GB', min: 0 };
areaEl.series = [
{ label: 'Media', color: 'primary', data: [600, 646, 700, 741] },
{ label: 'Documents', color: 'tertiary', data: [352, 380, 401, 428] },
{ label: 'Backups', color: 'secondary', data: [190, 204, 210, 222] }
];
areaEl.valueFormatter = (v) => (v == null ? '' : v + ' GB');
barEl.xAxis = { data: ['North America', 'Europe', 'APAC', 'LATAM'] };
barEl.yAxis = { label: 'USD (k)', min: 0 };
barEl.series = [
{ label: 'FY25', color: 'secondary', data: [640, 520, 310, 190] },
{ label: 'FY26', color: 'primary', data: [720, 590, 405, 238] }
];
pieEl.data = [
{ label: 'Organic', value: 5200 }, { label: 'Direct', value: 3120 },
{ label: 'Referral', value: 1980 }, { label: 'Paid', value: 1240 }
];
deploysEl.data = [8, 12, 10, 14];
deploysEl.labels = DAYS;
latencyEl.data = [362, 340, 331, 318];
latencyEl.labels = DAYS;
latencyEl.valueFormatter = (v) => (v == null ? 0 : v) + ' ms';
}
$: preset, applyData(); // re-runs on every palette swap
onMount(async () => {
await customElements.whenDefined('md-line-chart');
applyData();
});
function onPreset(e) {
preset = e.detail[0];
}
</script>
<div class="gallery" style={styleVars}>
<header>
<div><h2>Northwind Cloud — August 2026</h2><span>Usage, revenue, and platform health</span></div>
<md-segmented-button-set aria-label="Chart palette preset" on:mdChange={onPreset}>
<md-segmented-button value="default" label="Baseline" selected={preset === 'default'}></md-segmented-button>
<md-segmented-button value="forest" label="Forest" selected={preset === 'forest'}></md-segmented-button>
<md-segmented-button value="ocean" label="Ocean" selected={preset === 'ocean'}></md-segmented-button>
<md-segmented-button value="sunset" label="Sunset" selected={preset === 'sunset'}></md-segmented-button>
</md-segmented-button-set>
</header>
<md-line-chart bind:this={lineEl} label="Weekly active users" subtitle="May – August"
height="240px" show-marks curve="monotone"></md-line-chart>
<div class="row">
<md-area-chart bind:this={areaEl} label="Storage by type" subtitle="Stacked, GB" height="230px"></md-area-chart>
<md-bar-chart bind:this={barEl} label="Revenue by region" subtitle="USD thousands" height="230px" corner-radius="4"></md-bar-chart>
</div>
<div class="row">
<md-pie-chart bind:this={pieEl} label="Traffic by source" subtitle="Last 30 days"
show-labels="false" inner-radius="45%" legend="bottom" height="300px">
<div slot="center" style="text-align: center;"><strong>11,540</strong><br />visits</div>
</md-pie-chart>
<div class="tiles">
<div class="tile">
<div><div>Deploys per day</div><strong>14</strong></div>
<md-sparkline bind:this={deploysEl} variant="bar" color="primary" height="36px" style="width: 120px"></md-sparkline>
</div>
<div class="tile">
<div><div>p95 latency</div><strong>318 ms</strong></div>
<md-sparkline bind:this={latencyEl} color="tertiary" height="36px" style="width: 120px"></md-sparkline>
</div>
<div class="tile">
<div class="gauges">
<md-meter value="62" label="Storage used" show-label show-value value-text="1.24 TB of 2 TB"></md-meter>
<div>
<div>Nightly export — 412 of 605 tables</div>
<md-progress-indicator value="412" max="605" label="Nightly export"></md-progress-indicator>
</div>
</div>
<md-meter variant="circular" value="84" size="88" thickness="8"
label="API quota" show-label show-value color="tertiary"></md-meter>
</div>
</div>
</div>
</div>
<style>
.gallery { display: grid; gap: 16px; }
header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
.tiles { display: grid; gap: 12px; align-content: start; }
.tile { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-radius: 12px; background: var(--md-sys-color-surface-container-low); }
.gauges { display: grid; gap: 12px; flex: 1; }
</style>How it’s built
Section titled “How it’s built”| Component | Role in this screen |
|---|---|
md-line-chart | The trend headline. series and xAxis are JS properties (arrays never cross the attribute boundary); curve="monotone" avoids the overshoot a smooth spline adds to monotonic data; show-marks puts a point at each reading. |
md-area-chart | Storage composition — stack="normal" is its default, so three series become a cumulative total with no extra configuration. |
md-bar-chart | Year-over-year comparison across four discrete regions — the case lines must not imply continuity for. |
md-pie-chart | Donut via inner-radius="45%", with the headline total slotted into center — the hole is the layout. |
md-sparkline | Three KPI tiles: bar, line, and area variants, each ~36px tall with no axes or legend. The number stays in the tile as text — the sparkline is the trend, never the value. |
md-meter | Two static readings: a linear storage gauge with value-text="1.24 TB of 2 TB" overriding the percent readout, and a circular quota ring with the value centered inside it. |
md-progress-indicator | The nightly export is an activity, not a state — so it gets a progress bar, fed the raw count (value="412" max="605") rather than a pre-computed percentage. |
md-segmented-button | The palette switcher: a single-select set (role="radiogroup"), whose mdChange reports the selected preset. The handler writes --md-sys-color-* custom properties on the gallery wrapper. |
The details that make it production-grade
Section titled “The details that make it production-grade”- Re-theming works with the grain of the engine. Charts resolve series
colors from
--md-sys-color-*tokens at paint time, but only repaint automatically on aprefers-color-schemechange. Any other token swap needs a nudge: reassignseries(ordata) — a.slice()copy is a new reference and is enough.resize()will not do it; it returns early when the box has not changed. - Every chart carries a screen-reader story. The host is
role="figure"with a generated summary, the plot is a focusable region walkable with arrow keys (announced vialabel-point), and a screen-reader-only data table renders inside the component. The sparklines arerole="img"with a generated “latest / range” label. - Meter vs progress is a semantic split, not a visual one.
md-meterisrole="meter"— a state that sits still, with clamping and locale-aware value text.md-progress-indicatoris an activity; it takes the raw412 of 605count and computes the fraction itself. - The preset switcher is a real radiogroup. The set owns selection —
clicking a segment emits
mdChangewith the full value snapshot, arrow keys move and select per the WAI-ARIA radio pattern, and single-select can never be emptied by a stray click. - Baseline means no overrides, not a fourth palette. The default preset removes every inline token, so the gallery falls back to the site theme — including dark mode, which the charts already track on their own.
Variations
Section titled “Variations”- Zoomable history: add
zoom="slider"to the line chart — anmd-sliderband under the plot with focusable thumbs;mdZoomreports absolute indices you can mirror onto the area chart for linked panning. - Share-of-total storage: switch the area chart to
stack="percentage"when the question is “what fraction is media?” rather than “how many GB?”. - Single-hue pie: add
monochrome(ormonochrome="tertiary") to the donut so slices become tints of one role — useful when the palette is already busy elsewhere on the page. - Comparable sparklines: the three tiles measure different units, so each
auto-scales. If a row of tiles shares a unit, pin the same
min/maxon all of them — otherwise identical-looking trends can hide a 10x difference.
Related
Section titled “Related”- Recipe: KPI overview — sparkline tiles and meters arranged as a scanning-first summary page.
- Recipe: Release health drilldown — charts wired to drill interactions instead of palette swaps.
- Component manuals:
md-line-chart,md-pie-chart,md-meter— full API tables, behavioral contracts, and anti-pattern lists.