A <figure class="line-chart"> draws a
<table> of values as smooth lines. The first column
labels the x axis. Each further column is a series named by its
header cell.
JavaScript draws the chart from the table. It hides the table visually, so a screen reader still reads the data as a table. Without JavaScript, the table shows. Points, a cursor and a tooltip follow the pointer, and the arrow keys move them when the chart has focus.
- Total revenue
- $54,500
- +6.2% from last year
| Month | Revenue |
|---|---|
| Jan | $4,400 |
| Feb | $3,400 |
| Mar | $3,700 |
| Apr | $4,900 |
| May | $4,600 |
| Jun | $5,000 |
| Jul | $5,500 |
| Aug | $4,000 |
| Sep | $4,500 |
| Oct | $5,000 |
| Nov | $4,000 |
| Dec | $5,500 |
<figure class="line-chart fade">
<figcaption>Monthly revenue, 2026</figcaption>
<table>
<thead>
<tr><th>Month</th><th>Revenue</th></tr>
</thead>
<tbody>
<tr><th>Jan</th><td>$4,400</td></tr>
<tr><th>Feb</th><td>$3,400</td></tr>
<tr><th>Mar</th><td>$3,700</td></tr>
<!-- One row per month -->
</tbody>
</table>
</figure>
fade fades the ends of each line. The tooltip shows each
cell's text as written, so format values in the table.
Area
area fills under each line with a gradient in the series color.
| Month | Revenue |
|---|---|
| Jan | $4,400 |
| Feb | $3,400 |
| Mar | $3,700 |
| Apr | $4,900 |
| May | $4,600 |
| Jun | $5,000 |
<figure class="line-chart area">
<figcaption>Monthly revenue, first half</figcaption>
<table><!-- … --></table>
</figure>
Several series
Each value column after the first is another series. A legend appears
when there is more than one. Series take primary,
tertiary, secondary and
on-surface-variant in turn. Classes on a series header
cell apply to its line, so <th class="dashed"> draws
a target or forecast as a dotted line.
| Month | This year | Last year | Target |
|---|---|---|---|
| Jan | $4,400 | $4,000 | $5,200 |
| Feb | $3,400 | $4,400 | $5,400 |
| Mar | $3,700 | $3,100 | $5,100 |
| Apr | $4,900 | $4,500 | $5,000 |
| May | $4,600 | $4,900 | $5,700 |
| Jun | $5,000 | $4,000 | $5,500 |
| Jul | $5,500 | $4,000 | $6,200 |
| Aug | $4,000 | $4,400 | $6,400 |
| Sep | $4,500 | $3,100 | $6,100 |
| Oct | $5,000 | $4,500 | $5,000 |
| Nov | $4,000 | $4,900 | $6,700 |
| Dec | $5,500 | $5,500 | $6,500 |
<figure class="line-chart">
<figcaption>Revenue against last year and target</figcaption>
<table>
<thead>
<tr><th>Month</th><th>This year</th><th>Last year</th><th class="dashed">Target</th></tr>
</thead>
<tbody>
<tr><th>Jan</th><td>$4,400</td><td>$4,000</td><td>$5,200</td></tr>
<!-- One row per month -->
</tbody>
</table>
</figure>
Legend toggle
Add toggle to let readers hide a series. Each legend
item becomes a button, pressed while its series shows. Pressing one
hides that series from the plot and the tooltip, and the scale refits
to the series left. The others keep their colors. The last series
shown cannot be hidden. A hidden series is struck through, with a
hollow swatch, and stays hidden through update().
| Month | This year | Last year | Target |
|---|---|---|---|
| Jan | $4,400 | $4,000 | $5,200 |
| Feb | $3,400 | $4,400 | $5,400 |
| Mar | $3,700 | $3,100 | $5,100 |
| Apr | $4,900 | $4,500 | $5,000 |
| May | $4,600 | $4,900 | $5,700 |
| Jun | $5,000 | $4,000 | $5,500 |
| Jul | $5,500 | $4,000 | $6,200 |
| Aug | $4,000 | $4,400 | $6,400 |
| Sep | $4,500 | $3,100 | $6,100 |
| Oct | $5,000 | $4,500 | $5,000 |
| Nov | $4,000 | $4,900 | $6,700 |
| Dec | $5,500 | $5,500 | $6,500 |
<figure class="line-chart axis toggle">
<figcaption>Revenue against last year and target</figcaption>
<table><!-- As above --></table>
</figure>
Points and straight lines
Add points to mark every value with a dot, so a reader
can see where the measurements are. Add straight to
draw straight lines between them instead of a smooth curve. Both suit
a few values read one by one, such as a week of visits.
| Day | Visitors |
|---|---|
| Mon | 8,200 |
| Tue | 9,400 |
| Wed | 10,100 |
| Thu | 9,800 |
| Fri | 11,600 |
| Sat | 12,400 |
| Sun | 11,900 |
<figure class="line-chart axis points straight">
<figcaption>Visitors this week</figcaption>
<table>
<thead>
<tr><th>Day</th><th>Visitors</th></tr>
</thead>
<tbody>
<tr><th>Mon</th><td>8,200</td></tr>
<!-- … -->
</tbody>
</table>
</figure>
Stacked
Add stacked to draw each series on top of the ones
before it and fill the band between them. The top line is the total.
The y axis starts at zero, and the tooltip still shows each cell.
Stack values that add up, such as sales by channel, and keep them
positive. An empty cell leaves the total unknown, so it breaks that series and
the ones above it at that row.
| Month | Online | Retail | Wholesale |
|---|---|---|---|
| Jan | $2,100 | $1,800 | $900 |
| Feb | $2,300 | $1,700 | $1,000 |
| Mar | $2,200 | $1,900 | $950 |
| Apr | $2,600 | $1,800 | $1,100 |
| May | $2,800 | $2,000 | $1,050 |
| Jun | $3,100 | $2,100 | $1,200 |
<figure class="line-chart stacked">
<figcaption>Sales by channel, first half</figcaption>
<table>
<thead>
<tr><th>Month</th><th>Online</th><th>Retail</th><th>Wholesale</th></tr>
</thead>
<tbody>
<tr><th>Jan</th><td>$2,100</td><td>$1,800</td><td>$900</td></tr>
<!-- One row per month -->
</tbody>
</table>
</figure>
Sparkline
Use sparkline for a trend beside a figure. It is a 48px
chart with no grid, labels, legend or tooltip. Hide its caption with
visually-hidden when the tile already names it.
- Balance
- $5,000
- +25% over 14 days
-
Balance over 14 days Day Balance Day 1 $4,000 Day 2 $4,500 Day 3 $4,300 Day 4 $4,400 Day 5 $4,900 Day 6 $4,600 Day 7 $5,000 Day 8 $4,900 Day 9 $4,500 Day 10 $4,300 Day 11 $4,400 Day 12 $4,900 Day 13 $4,600 Day 14 $5,000 - Orders
- 452
- +46% over 14 days
-
Orders over 14 days Day Orders Day 1 310 Day 2 290 Day 3 335 Day 4 320 Day 5 360 Day 6 340 Day 7 380 Day 8 372 Day 9 395 Day 10 410 Day 11 398 Day 12 430 Day 13 425 Day 14 452
<dl class="stats">
<div>
<dt>Balance</dt>
<dd>$5,000</dd>
<dd class="stat-change up">+25% over 14 days</dd>
<dd>
<figure class="line-chart sparkline area my-0">
<figcaption class="visually-hidden">Balance over 14 days</figcaption>
<table><!-- … --></table>
</figure>
</dd>
</div>
</dl>
Chart cards
Put a chart in a card with the figures it explains. To add a period switch:
- Put the switch beside the stat.
- On a change, update the stat and swap the table rows.
- With
data-watchon the figure, the chart redraws itself. Without it, callupdate().
The card adds no chart styles, so size a chart with its tokens. A
card pads only its headings and paragraphs. Wrap the stat and chart
in <div class="p-4"> to keep them off the border. A
connected button group fills its container, so give it a
<div> of its own to keep it beside the stat.
- Revenue
- $54,500
- +6.2% from last year
| Month | This year | Last year |
|---|---|---|
| Jan | $4,400 | $4,000 |
| Feb | $3,400 | $4,400 |
| Mar | $3,700 | $3,100 |
| Apr | $4,900 | $4,500 |
| May | $4,600 | $4,900 |
| Jun | $5,000 | $4,000 |
| Jul | $5,500 | $4,000 |
| Aug | $4,000 | $4,400 |
| Sep | $4,500 | $3,100 |
| Oct | $5,000 | $4,500 |
| Nov | $4,000 | $4,900 |
| Dec | $5,500 | $5,500 |
<article class="outlined" id="revenue-card">
<div class="p-4">
<div class="flex flex-wrap justify-between align-start gap-3">
<dl class="stats my-0">
<div>
<dt>Revenue</dt>
<dd>$54,500</dd>
<dd class="stat-change up">+6.2% from last year</dd>
</div>
</dl>
<div>
<div class="button-group connected xsmall" data-selection="single"
data-selection-required role="group" aria-label="Period">
<button type="button" class="button tonal" aria-pressed="true"
data-period="12" data-total="$54,500"
data-change="+6.2% from last year">Year</button>
<button type="button" class="button tonal" aria-pressed="false"
data-period="6" data-total="$28,500"
data-change="+8.0% from last year">6 months</button>
</div>
</div>
</div>
<figure class="line-chart area" data-watch>
<figcaption class="visually-hidden">Revenue this year and last year</figcaption>
<table><!-- … --></table>
</figure>
</div>
</article>
<script>
const card = document.getElementById('revenue-card');
// The chart has data-watch, so it redraws itself when the rows change.
const body = card.querySelector('.line-chart tbody');
const rows = [...body.rows];
card.querySelectorAll('[data-period]').forEach((button) => {
button.addEventListener('click', () => {
body.replaceChildren(...rows.slice(-Number(button.dataset.period)));
card.querySelector('.stats dd').textContent = button.dataset.total;
card.querySelector('.stat-change').textContent = button.dataset.change;
});
});
</script>
For a compact card, set the height and clear the grid. A sparkline
with flex-1 beside the figures fills the rest of a short
tile.
- Visitors this week
- 73,400
- +14.5% from last week
| Day | Visitors |
|---|---|
| Mon | 8,200 |
| Tue | 9,400 |
| Wed | 10,100 |
| Thu | 9,800 |
| Fri | 11,600 |
| Sat | 12,400 |
| Sun | 11,900 |
- Balance
- $5,000
- +25% over 14 days
| Day | Balance |
|---|---|
| Day 1 | $4,000 |
| Day 2 | $4,500 |
| Day 3 | $4,300 |
| Day 4 | $4,400 |
| Day 5 | $4,900 |
| Day 6 | $4,600 |
| Day 7 | $5,000 |
| Day 8 | $4,900 |
| Day 9 | $4,500 |
| Day 10 | $4,300 |
| Day 11 | $4,400 |
| Day 12 | $4,900 |
| Day 13 | $4,600 |
| Day 14 | $5,000 |
<article class="outlined">
<div class="p-4">
<dl class="stats my-0">…</dl>
<figure class="line-chart area" style="
--md-comp-line-chart-height: 120px;
--md-comp-line-chart-line-width: 2px;
--md-comp-line-chart-grid-color: transparent">
<figcaption class="visually-hidden">Visitors per day this week</figcaption>
<table><!-- … --></table>
</figure>
</div>
</article>
<article class="outlined">
<div class="p-4">
<div class="flex align-center justify-between gap-3">
<dl class="stats my-0">…</dl>
<figure class="line-chart sparkline area flex-1 my-0"
style="--md-comp-line-chart-height: 72px">
<figcaption class="visually-hidden">Balance over 14 days</figcaption>
<table><!-- … --></table>
</figure>
</div>
</div>
</article>
Values and scale
A cell's number is its text without currency signs, percent signs
or grouping commas, so $4,400 reads as 4400. Text that
is not a single number, such as 4.4k or
(1,200), is a gap in the line, as is an empty cell. Put
the number in data-value for those, and for
4.400 €, which would read as 4.4.
The y axis spans the data with 10% to spare above and below. Set
data-min or data-max on the figure to fix
either end, such as data-min="0" when the change should
be read against zero.
Add axis to label the y axis at round values, with a
grid line at each. The chart widens an end it chose to the next round
value, so the labels start and end on the plot's edges. An end set
with data-min or data-max stays where it
is. The labels repeat any currency or percent sign the cells are
written with, and shorten large numbers, such as $1.5K.
They are hidden from assistive technology, and the table still
carries the values.
| Month | Revenue |
|---|---|
| Jan | $4,400 |
| Feb | $3,400 |
| Mar | $3,700 |
| Apr | $4,900 |
| May | $4,600 |
| Jun | $5,000 |
<figure class="line-chart axis">
<figcaption>Monthly revenue, first half</figcaption>
<table><!-- … --></table>
</figure>
<figure class="line-chart" data-min="0">
<figcaption>Signups</figcaption>
<table>
<thead><tr><th>Week</th><th>Signups</th></tr></thead>
<tbody>
<tr><th>Week 1</th><td data-value="4400">4.4k</td></tr>
<tr><th>Week 2</th><td></td></tr>
<tr><th>Week 3</th><td data-value="5100">5.1k</td></tr>
</tbody>
</table>
</figure>
Accessibility
The table is the chart's text alternative. Give it a header row and
a header cell per row, and name the chart with a
<figcaption>. The drawn chart, its labels and its
legend are hidden from assistive technology. The exception is a
toggle legend. Its buttons follow the plot in the tab
order and say whether each series shows.
The plot is one Tab stop named by the caption. Left and right arrows move between rows. Home and End jump to the ends, and Escape hides the tooltip. In a right-to-left page, the x axis runs from right to left, and the arrows still move the cursor the way they point. The tooltip is a polite live region, so a screen reader hears each row's values as it moves. A sparkline is not focusable.
JavaScript
AutoInit() starts every .line-chart. To
start one yourself, call LineChart.init. After you
change its table, call update() to redraw it in place.
The lines, areas and points move from their old values to the new
ones. A series or value new to the chart rises from the plot bottom,
or in a stack from the series below. With reduced motion, the new
shape appears at once.
Add data-watch to the figure, or set the
watch option, and the chart calls update()
itself whenever its table changes. That covers rows or cells added
or removed, cell text, and a data-value,
class or heatmap data-label inside the
table. A replaced table is watched too. Every chart type takes
data-watch.
The edits a script makes in one run redraw the chart once. Edits on
either side of an await redraw it twice. The chart reads
the attribute when it starts, and any value turns it on. Changes to
the figure itself, such as data-min, its classes or its
caption, still need update().
const chart = Expressive.LineChart.init(document.querySelector('.line-chart'), {
min: 0
});
chart.show(2); // highlight the third row; -1 hides the tooltip
chart.update(); // redraw from the table; focus and the highlighted row stay
chart.destroy(); // remove the drawing and show the table again
| Option | Default | Description |
|---|---|---|
min | null | Lowest y value. data-min overrides it. |
max | null | Highest y value. data-max overrides it. |
watch | false | Redraw when the table changes. data-watch turns it on too. |
Tokens
Override these on the .line-chart.
| Token | Default |
|---|---|
--md-comp-line-chart-height | 240px, 48px for a sparkline |
--md-comp-line-chart-line-width | 3px, 2px for a sparkline |
--md-comp-line-chart-grid-color | --md-sys-color-outline-variant |
--md-comp-line-chart-axis-width | 48px, the room beside the plot for axis labels |
--md-comp-line-chart-color-1 | --md-sys-color-primary |
--md-comp-line-chart-color-2 | --md-sys-color-tertiary |
--md-comp-line-chart-color-3 | --md-sys-color-secondary |
--md-comp-line-chart-color-4 | --md-sys-color-on-surface-variant |
For a selective Sass build, include "line-chart" in $components.