Skip to main content

Catalog · Charts

Column chart

Compares a few values as columns of one height scale, drawn from a native table that keeps every figure as text.

Fits best: app — a report that compares a few values with an order, such as days or months

Example

Parcels per day this week: the figure, its column, then the day.

Parcels per day, this week
MonTueWedThuFriSatSun
1,2048309011,4201,5003770

Several series

Visitors per month, by device, for three months: a row per month and a column per series, with no script. Each month is a group of columns, the head row the legend. The data is a sample: replace it with yours.

Visitors per month, by device, January to March
MonthDesktopMobile
Jan18680
Feb305200
Mar237120

Interactive

Visitors per day over four weeks, in <luz-chart type="column" interactive>. Each series and its total is a button that turns its columns off and on (the last one shown stays on); point at a day, or Tab to the chart and use the arrow keys, for its figures. 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

None known.

Code

Copy the markup

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

A stylesheet of @luz-ds/catalog: link components/column-chart.css after luz.css.

HTML
<table class="luz-column-chart">
  <caption>Parcels per day, this week</caption>
  <thead>
    <tr><th scope="col">Mon</th><th scope="col">Tue</th><th scope="col">Wed</th><th scope="col">Thu</th></tr>
  </thead>
  <tbody>
    <tr><td style="--v: .8">1,204</td><td style="--v: .55">830</td><td style="--v: .6">901</td><td style="--v: .95">1,420</td></tr>
  </tbody>
</table>

With several series, with no script: one row per category, its name in a <th scope="row">, and one cell per series, up to six. The head row names the series. The rows are laid out side by side, and some browsers (Safari with VoiceOver) then stop telling the table from a pile of boxes: write its roles, as here. <luz-chart interactive> writes them for you.

HTML
<table class="luz-column-chart" role="table">
  <caption>Visitors per month, by device, January and February</caption>
  <thead role="rowgroup">
    <tr role="row"><th scope="col" role="columnheader">Month</th><th scope="col" role="columnheader">Desktop</th><th scope="col" role="columnheader">Mobile</th></tr>
  </thead>
  <tbody role="rowgroup">
    <tr role="row"><th scope="row" role="rowheader">Jan</th><td role="cell" style="--v: .58">186</td><td role="cell" style="--v: .25">80</td></tr>
    <tr role="row"><th scope="row" role="rowheader">Feb</th><td role="cell" style="--v: .95">305</td><td role="cell" style="--v: .62">200</td></tr>
  </tbody>
</table>

Interactive, with the header of the series: link components/chart.css too, and load catalog.js, which loads chart.js when the page has a <luz-chart>. One row per day, its name in a <th scope="row">, and one column per series. The table has no class: the script gives it luz-column-chart when it draws it, so without the script it fits any width.

HTML
<luz-chart type="column" interactive>
  <div class="luz-table-wrap" role="region" aria-label="Visitors per day, April 1 to 3" tabindex="0">
    <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 style="--v: .5"><data value="252">252</data></td><td style="--v: .3"><data value="148">148</data></td></tr>
        <tr><th scope="row">Apr 2</th><td style="--v: .54"><data value="271">271</data></td><td style="--v: .55"><data value="276">276</data></td></tr>
        <tr><th scope="row">Apr 3</th><td style="--v: .36"><data value="182">182</data></td><td style="--v: .26"><data value="128">128</data></td></tr>
      </tbody>
    </table>
  </div>
</luz-chart>

Variants and options

OptionEffect
--v
On each cell, from 0 to 1: its value over the top of the scale, the same for every column
--luz-column-chart-color
Another chart colour for a table or a cell, such as var(--luz-chart-2)
--luz-column-chart-h
The height of the columns
--luz-column-chart-bar
The width of a column, var(--luz-space-lg) by default; never wider than its share of the table
A row per category
Several series: a <th scope="row"> then a cell per series in each row, up to six. Each row is a group of columns, series n in --luz-chart-n, its name below it, and the head row is the legend
--luz-column-chart-gap
In the CSS-only several-series chart, the space between the columns of a group, var(--luz-space-xs) by default; the groups take the space that is left. <luz-chart interactive> shares the width of its days and has no gap to set
<luz-chart type="column" interactive>
Around a table of several series with no class: drawn as columns by the script, with a button per series and its total, which turns its columns off and on, and a readout of the day under the pointer or the focus

How it works

A table of one head row of names and one row of figures. Each figure is the text of its cell, and the cell draws its column under it, from the foot up to --v, in --luz-chart-1, with the track in --luz-chart-track. The columns share the width evenly.

Work out --v as the value over the top of the scale, the same top for every cell. The inline style sets only that. With a strict style-src, set it in a rule of your own, such as .sat { --v: .25 }.

At the default text size, up to seven columns, or three groups of two, fit a 320 px screen. For more, or with larger text, put the table in a luz-table-wrap: it scrolls inside it.

The figure of each cell is above its column and the name below it, as an axis. With several series, each row is a group of columns, --luz-column-chart-gap apart (in the CSS-only chart), and the head row is the legend: each series' name after a dot of its colour.

Inside <luz-chart type="column" interactive>, the script adds a header above the table: a button per series, its name and the sum of its figures, written with the page's language. A sum suits counts and amounts, such as visitors or euros; it does not suit rates, shares or averages, whose total says nothing: do not use interactive for them, or leave their cells out of it. The caption becomes the title. The columns lose their track: the day under the pointer or the focus gets a light band, and the figures leave the columns for a readout beside it. The columns keep the scale of their --v when a series is turned off, and the last series shown cannot be turned off. The script writes no word of its own: the names, the figures and the caption are yours.

Without JavaScript

It needs none. Without the stylesheet it reads as a table: a head row of names, then the figures. The interactive chart is, without its script, a plain table: a row per day with every figure, which fits the narrowest screen. A table of a few categories that should be columns with no script takes the class luz-column-chart itself.

Keyboard and ARIA

The plain chart takes no focus. A table keeps its roles, so a screen reader reads each figure with its name. The caption names the table and says the unit. With several series the rows are laid out side by side: write the roles of the table in its markup, as the snippet does, so that Safari with VoiceOver still reads a table.

The interactive chart is a group named by the caption, and the script writes the roles of its table (table, rowgroup, row, columnheader, rowheader and cell). Its buttons are toggle buttons (aria-pressed): Enter or Space turns a series off and on, and the last series shown stays on. Its rows take the focus, one Tab stop, and each row is named by its own cells, from your text: “Apr 1, Desktop 252, Mobile 148”, with every series, shown or not. When the focus is in a row, the arrow keys move to the next or the previous day (Left and Right follow the direction of the page), and Home and End to the first and the last; the keys of a screen reader’s browse mode are its own.

The readout has the name of the day as its title, so the band is never the only sign of where you are; it is hidden from assistive technology. Escape puts it away without moving the pointer or the focus (it comes back on another day). A series turned off loses its columns but keeps its cells in the table, so a screen reader still reads every figure.

Do not hide the table: it is the data. The columns are a picture of it and carry no text of their own.

Contrast and focus are in the Design tab.

Design

When to use

Use it to compare a few values that have an order or a name, such as the days of a week or the months of a year, on one scale. With several series, it compares a few series category by category, such as two devices month by month. Use the interactive chart for two or more series over many days, when the totals matter and each day is read one at a time.

When not to use

Do not use the interactive chart when every figure must be seen at once: its figures are in the readout, nor for rates, shares or averages: the total of a series is a sum. Do not use it for one value (use a meter), for long names (use a bar list), or for a trend over many points. Keep a table when exact values matter most.

Do and don't

Do Work every --v from one top of the scale, and write each figure in its cell: the height compares, the number says how much.
Don't Give each column a scale of its own: 312 fills its column as 1,204 does.

Writing the content

Name the unit and the period in the caption (“Parcels per day, this week”), keep each name to a word or two, and write the figure with its unit when it is not obvious. Name each series in a word or two: its head cell is the name of its button. The data and the labels are yours: the component only draws them.

Contrast and focus

The column is a chart colour; the track is quieter on purpose, because the figure carries the value. The interactive chart has no track: the day under the pointer or the focus has a light band.

PartColour on backgroundLightDarkHigh contrastNeeds
Column against its track
--luz-chart-1 on --luz-chart-track
3.4:1
3.8:1
6.8:1
2:1
Column on the page
--luz-chart-1 on --luz-surface-page
5.6:1
7.7:1
14.1:1
3:1
Column on a card
--luz-chart-1 on --luz-surface
5.9:1
7.2:1
14.1:1
3:1
Second series against its track
--luz-chart-2 on --luz-chart-track
3.2:1
4.4:1
4.4:1
2:1
Second series on the page
--luz-chart-2 on --luz-surface-page
5.3:1
8.9:1
9.3:1
3:1
Column on the band of the day under the pointer
--luz-chart-1 on --luz-surface-hover
5.1:1
6.4:1
13.5:1
3:1
Second series on the band
--luz-chart-2 on --luz-surface-hover
4.8:1
7.4:1
8.9:1
3:1
Figure
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5:1
Name of a column or a group
--luz-text-secondary on --luz-surface-page
6.2:1
7.5:1
19.3:1
4.5:1
Name of a series in the legend
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5: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
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 focus rings, borders and icons; a quiet decorative part keeps 2:1 against what it is drawn on.

  • Focus: the plain chart takes none. In the interactive chart, a button and a row take the focus ring, --luz-focus-ring, 2px: inside a button's edge, and around the group of columns of a row.
  • 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.
  • Not colour alone: every column has its figure in text, and in forced colors the column is drawn in the system highlight with an edge around its track, a second series in the text colour; in the interactive chart, with no track, the day under the pointer or the focus has a dashed edge for its band. A series turned off has a hollow dot and a lighter name, not only another colour.

Tokens it uses

The 26 tokens that components/column-chart.css reads, by family. 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-mono, --luz-font-sans, --luz-text-sm, --luz-text-xs
Radius
--luz-radius-full, --luz-radius-sm
Layers
--luz-z-raised

Keyboard and ARIA are in the Code tab.