# Line chart

> Trends over time drawn from a data table, with a tooltip, a legend and a sparkline size.

## Anatomy

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.

```
<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.

```
<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.

```
<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()`.

```
<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.

```
<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.

```
<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.

```
<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:

1.  Put the switch beside the stat.
2.  On a change, update the stat and swap the table rows.
3.  With `data-watch` on the figure, the chart redraws itself. Without it, call `update()`.

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.

```
<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.

```
<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.

```
<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`.
