Skip to main content

Catalog · Charts

Chart

Draws a line, an area or a small trend line from a table or a list of numbers you already wrote, which stays in the page beside it.

Fits best: any site — a page that shows a trend: a report, a dashboard, a post with a few figures

Example

Parcels per route over six months, as a table you can read and as a line chart drawn from it. The button swaps the drawing for the table; a screen reader always has the table.

Parcels per route, January to June
MonthSevillaCádizJerez
Jan1,204312640
Feb1,310340620
Mar1,180365700
Apr1,420410690
May1,390398760
Jun1,480452810

One column is one series, up to six. Without JavaScript it is the table.

Parcels to Sevilla, January to June
MonthSevilla
Jan1,204
Feb1,310
Mar1,180
Apr1,420
May1,390
Jun1,480

With variant="area".

Parcels a day, last 7 days
  • 41
  • 38
  • 44
  • 52
  • 47
  • 58
  • 63

type="spark": a small line, and its last figure as text. The other six stay in the list for a screen reader.

Interactive

Visitors per day over four weeks, with interactive: each series and its total is a button that turns its line off and on, on the same scale. Point at a day, or Tab to the chart and use the arrow keys, for its figures and a band over it. Without JavaScript it is the table. The data is a sample: replace it with yours.

Visitors per day, April 1 to 28
DayDesktopMobile
Apr 1252148
Apr 2271276
Apr 3182128
Apr 4380247
Apr 5194203
Apr 619174
Apr 717998
Apr 8179132
Apr 9281217
Apr 10187171
Apr 11193251
Apr 12278125
Apr 13228152
Apr 14120100
Apr 15331270
Apr 16319125
Apr 17317259
Apr 18271122
Apr 19226121
Apr 2018786
Apr 21146130
Apr 22206248
Apr 23200256
Apr 24248253
Apr 25378284
Apr 26216136
Apr 27190153
Apr 2819994

Known issues

Four things to know before you rely on it.

  • Without interactive there is no readout on hover or focus: the figures are in the table, which the button shows. With it, the readout follows the pointer and the focus alike.
  • It reads the table when it connects, when the table changes and when the box changes size. A table added to the page later, after the page loaded the script, needs the element to be added with it.
  • The plot is drawn left to right in every language, as a time axis is; the table, the title, the legend and the button follow the page’s direction.
  • It has not been tested with a screen reader.

Code

Copy the markup

Bundler
import "@luz-ds/catalog/components/chart.css";
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/chart.min.css" integrity="sha384-ELc0v0M/zEVoqPmwPSIjtJN98bz6vLurMqk58If0SOBFCqQT+JLI28PND1wWsOfG" crossorigin="anonymous">

Write the table first, as the page a reader lands on without JavaScript. The first column is the X axis, a th of its row; each other column is a series, named by its th. Put each number in a <data value> and write it as you want it read in the cell. Wrap the table in a .luz-table-wrap only when it can be wider than the screen, as the three-series example does, and give each td a data-label so that on a narrow screen its rows stack and still name their cells. A table that always fits needs no wrapper: with one, the group, the region and the table would read the same name three times. The chart hides the wrapper with the table, and it takes no Tab stop meanwhile.

catalog.min.js loads the element’s own file, chart.min.js, when it scans the page; a page without catalog.min.js loads that file on its own. Link components/chart.css, after core’s.

HTML
<script type="module" src="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/catalog.min.js"></script>

<luz-chart type="line" data-luz-label-table="Show table" data-luz-label-chart="Show chart">
  <table class="luz-table">
    <caption>Parcels per route, January to March</caption>
    <thead>
      <tr><th scope="col">Month</th><th scope="col">Sevilla</th><th scope="col">Cádiz</th></tr>
    </thead>
    <tbody>
      <tr><th scope="row">Jan</th><td data-label="Sevilla"><data value="1204">1,204</data></td><td data-label="Cádiz"><data value="312">312</data></td></tr>
      <tr><th scope="row">Feb</th><td data-label="Sevilla"><data value="1310">1,310</data></td><td data-label="Cádiz"><data value="340">340</data></td></tr>
      <tr><th scope="row">Mar</th><td data-label="Sevilla"><data value="1180">1,180</data></td><td data-label="Cádiz"><data value="365">365</data></td></tr>
    </tbody>
  </table>
</luz-chart>

With interactive, a header of the series and their totals, and a readout of a day:

HTML
<luz-chart type="line" interactive data-luz-label-table="Show table" data-luz-label-chart="Show chart">
  <table class="luz-table">
    <caption>Visitors per day, April 1 to 3</caption>
    <thead>
      <tr><th scope="col">Day</th><th scope="col">Desktop</th><th scope="col">Mobile</th></tr>
    </thead>
    <tbody>
      <tr><th scope="row">Apr 1</th><td><data value="252">252</data></td><td><data value="148">148</data></td></tr>
      <tr><th scope="row">Apr 2</th><td><data value="271">271</data></td><td><data value="276">276</data></td></tr>
      <tr><th scope="row">Apr 3</th><td><data value="182">182</data></td><td><data value="128">128</data></td></tr>
    </tbody>
  </table>
</luz-chart>

A small line takes a list of <data> elements, in any markup, and shows the last of them:

HTML
<luz-chart type="spark">
  <span>Parcels a day, last 4 days</span>
  <ul class="luz-chart__values" role="list">
    <li><data value="41">41</data></li>
    <li><data value="38">38</data></li>
    <li><data value="44">44</data></li>
    <li><data value="52">52</data></li>
  </ul>
</luz-chart>

Your scripts can ask what the chart would draw:

JS
import { readChart } from "@luz-ds/catalog/chart";

// What the chart draws: the names of the rows, and one series per column
const { rows, series } = readChart(document.querySelector("luz-chart"));

Variants and options

Class or attributeEffect
<luz-chart>
The custom element the module defines, around the table or the list. It wires itself when it connects
type
line (the default) draws the table’s columns; spark draws one small line from the <data> elements inside. Anything else leaves the table
variant="area"
Fills the area under each line, a light tint of the series colour
interactive
With type="line" (or type="column" on a column chart): a header with a button per series, its name and total, which turns its line off and on, and a readout of the day under the pointer or the focus. It replaces the legend
data-luz-label-table, data-luz-label-chart
The two words of the button: “Show table” and “Show chart”, in the page’s language, on the element or an ancestor (once on <body> serves every chart). LUZ has no English fallback: with either missing there is no button, and the table shows under the chart
--luz-chart-ratio
The shape of the drawing, 2 / 1 by default. Set it on the element: style="--luz-chart-ratio: 3 / 1"
caption
The title: the table’s caption names the group and is copied above the drawing
th scope="col"
The name of a series, in the legend. The first column is the X axis; each other column is a series, up to six, and further ones stay in the table only
td with <data value>
A number: the value of a <data>, or the plain text of the cell. Anything else is a gap in the line
.luz-chart__values
The list of a type="spark": the <data> elements are the points, and the last one shows as text
data-luz-view, data-luz-ready, data-luz-end
Written by the script: chart, table or both for what shows; the element is ready; the last figure of a spark. Style them, never set them
readChart(el)
Exported by @luz-ds/catalog/chart: the names of the rows and the series the table holds, or null

How it works

CSS from @luz-ds/catalog and a file of its own, @luz-ds/catalog/chart (dist/chart.min.js, under 5 kB compressed), which defines <luz-chart>. It uses no library and draws with DOM calls only, so a page that forbids inline scripts and styles runs it as it is. To compare values by length with no script, use the bar list, the column chart, the stacked bar or the heat map.

  • The table: it stays in the page. While the chart shows, the table is moved off screen, not hidden, so a screen reader reads it as it always did; when the button shows the table, the drawing goes. On paper, the table prints and the drawing does not.
  • The drawing: an SVG marked aria-hidden, drawn again when the box or the table changes. It has no animation. Series are --luz-chart-1 to --luz-chart-6, so a change of mode repaints nothing.
  • Telling series apart: each line ends in a mark of its own shape (a circle, a square, a triangle, a diamond, a plus and a cross), repeated in the legend. In forced colors every line is the text colour, each with its own dashes.
  • The numbers: the scale starts at zero, or lower when a figure is negative, in round steps (1, 2 or 5 times a power of ten). The axes are written with Intl.NumberFormat in the page’s language, such as 1.2K.
  • Interactive: the header has a toggle button per series, with its name and the sum of its figures. The total is a sum: it suits counts and amounts, such as visitors or euros, and not rates, shares or averages, whose total says nothing. A line turned off is not drawn, on the same scale, and the last line shown cannot be turned off. The day under the pointer, or the row with the focus, gets a light band and a readout with its name and the figure of each line shown, copied from the cells.
  • The words: the script writes none of its own. The button’s two words are yours, in data-luz-label-*; the title and the legend copy your caption and your column heads.

Without JavaScript

The table, or the list, is all there is, and it is complete: the caption, every figure, every name. The element is a block and the button does not exist. With interactive there is no header and no readout: every figure is in the table. It is the same when the page lacks the table, or type is not one of the two.

Keyboard and ARIA

  • Keys: Tab reaches the “Show table” button; Enter or Space presses it. Without interactive nothing in the picture takes the focus.
  • Interactive: the buttons of the header are toggle buttons (aria-pressed): Enter or Space turns a line off and on, and the last line shown stays on. The picture then takes the focus, one Tab stop. It is a group named by the caption and holds a polite status that, on each key, says the label of the day, the same text as the row of the table. Each row is named by its own cells, from your text, “Apr 1, Desktop 252, Mobile 148”, every series, shown or not. With the focus on the picture, the arrow keys move to the next or the previous day (Right is always the next, as the plot is left to right), and Home and End to the first and the last; the keys of a screen reader’s browse mode are its own. Entering says its name and nothing else, and leaving empties the status.
  • The readout: its title is the name of the day, so the band is never the only sign of where you are. Escape puts it away without moving the pointer or the focus; it comes back on another day. It is hidden from assistive technology, which reads the row. A line turned off is not drawn, but its cells stay in the table, read as before. The script writes the roles of the table (table, rowgroup, row, columnheader, rowheader and cell) so that the rows keep them in every browser.
  • Name: the element is a group named by the table’s caption, and the button is described by it, so “Show table” is heard with what it shows. The drawing, its title and its legend are aria-hidden: the table says all of it.

Contrast and focus are in the Design tab.

Design

When to use

Use it to show how a few series change along an ordered axis, such as months, when the shape of the change is what the reader should see and you already have the numbers in a table. Add interactive for many days and two or more series, when the totals matter and a day is read one at a time. The small line (type="spark") is for a figure that needs its trend beside it.

When not to use

Do not use it when exact values matter more than the shape: a table says them better. For a few values compared on one scale, use a column chart or a bar list, which need no script.

  • Do not use it for parts of a whole: use a stacked bar. There is no pie or donut.
  • Do not use interactive for rates, shares or averages: the total in its header is a sum.
  • Do not use it for more than six series, or for series that cannot share a scale: split them into two charts.

It replaces a charting library that draws a canvas with no text beside it. Nothing in LUZ is removed.

Do and don't

Do Name each column for what it counts: the legend and the table use the same name.
Don't Name them “Series 1” and “Series 2”: the legend then tells the reader nothing, and neither does the table.

Writing the content

The caption is the title of the chart, and says what is counted, where and when: “Parcels per route, January to June”. It is the name a screen reader gives the group.

  • Name each column as the legend should read, with its unit if it has one, and write each figure in its cell as you want it read: 1,204, not 1204, with the plain number in the value.
  • Write the button as the action and the thing: “Show table”, “Show chart”. Set both on the page, once, in its language.
  • With interactive, the head of each column is also the name of its button: a word or two.
  • Keep the names of the rows short: they are the labels of the X axis, and some are dropped when they do not fit.

Contrast and focus

The six series are chart colours, each at 3:1 or more on the page and on a card, in every mode; none of them is red, green or yellow, because those say a status. The grid is a hairline, quieter than every series, because the figures are in the table.

PartColour on backgroundLightDarkHigh contrastNeeds
Series 1, line, mark and legend
--luz-chart-1 on --luz-surface-page
5.6:1
7.7:1
14.1:1
3:1
Series 2, line, mark and legend
--luz-chart-2 on --luz-surface-page
5.3:1
8.9:1
9.3:1
3:1
Series 3, line, mark and legend
--luz-chart-3 on --luz-surface-page
9.6:1
10.6:1
10.9:1
3:1
Series 4, line, mark and legend
--luz-chart-4 on --luz-surface-page
9.8:1
6.1:1
6.6:1
3:1
Series 5, line, mark and legend
--luz-chart-5 on --luz-surface-page
3.9:1
5.0:1
6.2:1
3:1
Series 6, line, mark and legend
--luz-chart-6 on --luz-surface-page
3.7:1
5.4:1
14.7:1
3:1
Series 6, on a card
--luz-chart-6 on --luz-surface
3.8:1
5.0:1
14.7:1
3:1
Figures of the axes and legend
--luz-text-secondary on --luz-surface-page
6.2:1
7.5:1
19.3:1
4.5:1
Title
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5:1
Grid, on the page
--luz-chart-grid on --luz-surface-page
1.4:1
1.7:1
3.2:1
1.3:1
Series 1 on the band of a day
--luz-chart-1 on --luz-surface-hover
5.1:1
6.4:1
13.5:1
3:1
Series 2 on the band of a day
--luz-chart-2 on --luz-surface-hover
4.8:1
7.4:1
8.9:1
3:1
Series shown: its name and total
--luz-text-primary on --luz-surface-muted
15.8:1
15.1:1
20.1:1
4.5:1
Series shown: the line under it
--luz-brand-primary on --luz-surface-muted
5.6:1
7.7:1
13.5:1
3:1
Series turned off: its name and total
--luz-text-secondary on --luz-surface-page
6.2:1
7.5:1
19.3:1
4.5:1
Readout: the figure
--luz-text-primary on --luz-surface-strong
13.5:1
10.9:1
18.0:1
4.5:1
Readout: the series' name
--luz-text-secondary on --luz-surface-strong
5.3:1
5.4:1
16.5:1
4.5:1
Focus ring
--luz-focus-ring on --luz-surface-page
5.6:1
7.7:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for the lines and marks; the grid is decorative and keeps a hairline of 1.3:1 or more.

  • Focus: the “Show table” button is core’s ghost button and takes its focus ring. The picture takes none. With interactive, a button of the header takes the focus ring, --luz-focus-ring, 2px, inside its edge, and so does the picture: the ring is drawn around it, 2px from its edge, because it is the picture that has the focus.
  • Target and focus: each button of the interactive header is more than 24 by 24 px (WCAG 2.2, 2.5.8), and the readout is placed beside the day that has the focus and never over it (2.4.11). Both criteria are new in WCAG 2.2 and not yet required by EN 301 549 v3.2.1.
  • A series is told by its colour, by the mark it ends in and by the legend; in forced colors every line is the text colour, with its own dashes, the area is not filled and the grid is the grey text colour. A series turned off has a hollow dot and a lighter name, and the band of a day is a dashed edge in forced colors.

Tokens it uses

The 33 tokens that components/chart.css reads, by family; the button is core’s. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-space-2xl, --luz-space-2xs, --luz-space-3xs, --luz-space-lg, --luz-space-md, --luz-space-sm, --luz-space-xs
Typography
--luz-font-heading, --luz-font-mono, --luz-font-sans, --luz-text-sm, --luz-text-xl, --luz-text-xs
Radius
--luz-radius-full, --luz-radius-md, --luz-radius-sm
Shadows
--luz-shadow-md
Layers
--luz-z-raised

Keyboard and ARIA are in the Code tab.