Skip to content

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
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>
ComponentRole in this screen
md-line-chartThe 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-chartStorage composition — stack="normal" is its default, so three series become a cumulative total with no extra configuration.
md-bar-chartYear-over-year comparison across four discrete regions — the case lines must not imply continuity for.
md-pie-chartDonut via inner-radius="45%", with the headline total slotted into center — the hole is the layout.
md-sparklineThree 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-meterTwo 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-indicatorThe 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-buttonThe 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.
  • 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 a prefers-color-scheme change. Any other token swap needs a nudge: reassign series (or data) — 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 via label-point), and a screen-reader-only data table renders inside the component. The sparklines are role="img" with a generated “latest / range” label.
  • Meter vs progress is a semantic split, not a visual one. md-meter is role="meter" — a state that sits still, with clamping and locale-aware value text. md-progress-indicator is an activity; it takes the raw 412 of 605 count and computes the fraction itself.
  • The preset switcher is a real radiogroup. The set owns selection — clicking a segment emits mdChange with 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.
  • Zoomable history: add zoom="slider" to the line chart — an md-slider band under the plot with focusable thumbs; mdZoom reports 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 (or monochrome="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/max on all of them — otherwise identical-looking trends can hide a 10x difference.