Charts
A chart is the table of its numbers, drawn: columns stacked by day, a sparkline of the last runs, a board of lanes over the next hours. The numbers stay in the page as text, so a screen reader reads the table; a small script only hands them to the stylesheet.
A package of the release
Charts are linked apart, on the screens that show them, beside the two files: css/redz-ui-charts.min.css draws, and js/redz-ui-charts.min.js hands it the numbers - every data-v, data-at, data-from, data-to, data-median and data-now becomes a custom property through the CSSOM, which a strict CSP allows where it would refuse a style attribute. A number is a share, 0 to 1: a value over the largest, a time over the window.
<link rel="stylesheet" href="redz-ui/css/redz-ui-charts.min.css">
<script type="module" src="redz-ui/js/redz-ui-charts.min.js"></script>
Columns by day
table class="chart": its caption is the title, the header cells after the first are the series and draw as the legend, and each row is a day - th scope="row" its label, each td a segment whose data-v is its share of the tallest day, stacked from the first up. The series take eight colors in order, --redz-chart-1 to -8, chosen to stay apart for color-blind eyes in both themes; data-tone gives a segment a state's color instead. A row's title is its tooltip, a row with data-empty a stub on the axis, and a label wrapped in visually-hidden is read, not shown - label every few days when there are many. --redz-chart-height sets the height, 10rem by default; ninety days fit a card 700 px wide.
| Day | Free | Paid | Failed |
|---|---|---|---|
| Nov 1 | 120 | 30 | 4 |
| Nov 2 | 132 | 28 | 2 |
| Nov 3 | 98 | 41 | 6 |
| Nov 4 | 0 | 0 | 0 |
| Nov 5 | 144 | 35 | 3 |
| Nov 6 | 160 | 22 | 1 |
| Nov 7 | 110 | 30 | 9 |
| Nov 8 | 90 | 25 | 2 |
| Nov 9 | 0 | 0 | 0 |
| Nov 10 | 150 | 40 | 5 |
| Nov 11 | 170 | 12 | 0 |
| Nov 12 | 125 | 33 | 4 |
| Nov 13 | 140 | 29 | 3 |
| Nov 14 | 155 | 31 | 2 |
<table class="chart">
<caption>Messages sent per day</caption>
<thead><tr><th scope="col">Day</th><th scope="col" data-tone="success">Free</th><th scope="col" data-tone="warning">Paid</th><th scope="col" data-tone="danger">Failed</th></tr></thead>
<tbody>
<tr title="Nov 1: 120 free, 30 paid, 4 failed"><th scope="row">Nov 1</th><td data-tone="success" data-v="0.615">120</td><td data-tone="warning" data-v="0.154">30</td><td data-tone="danger" data-v="0.021">4</td></tr>
<tr title="Nov 2: 132 free, 28 paid, 2 failed"><th scope="row"><span class="visually-hidden">Nov 2</span></th><td data-tone="success" data-v="0.677">132</td><td data-tone="warning" data-v="0.144">28</td><td data-tone="danger" data-v="0.01">2</td></tr>
<tr title="Nov 3: 98 free, 41 paid, 6 failed"><th scope="row"><span class="visually-hidden">Nov 3</span></th><td data-tone="success" data-v="0.503">98</td><td data-tone="warning" data-v="0.21">41</td><td data-tone="danger" data-v="0.031">6</td></tr>
<tr data-empty title="Nov 4: none"><th scope="row">Nov 4</th><td>0</td><td>0</td><td>0</td></tr>
<tr title="Nov 5: 144 free, 35 paid, 3 failed"><th scope="row"><span class="visually-hidden">Nov 5</span></th><td data-tone="success" data-v="0.738">144</td><td data-tone="warning" data-v="0.179">35</td><td data-tone="danger" data-v="0.015">3</td></tr>
<tr title="Nov 6: 160 free, 22 paid, 1 failed"><th scope="row"><span class="visually-hidden">Nov 6</span></th><td data-tone="success" data-v="0.821">160</td><td data-tone="warning" data-v="0.113">22</td><td data-tone="danger" data-v="0.005">1</td></tr>
<tr title="Nov 7: 110 free, 30 paid, 9 failed"><th scope="row">Nov 7</th><td data-tone="success" data-v="0.564">110</td><td data-tone="warning" data-v="0.154">30</td><td data-tone="danger" data-v="0.046">9</td></tr>
<tr title="Nov 8: 90 free, 25 paid, 2 failed"><th scope="row"><span class="visually-hidden">Nov 8</span></th><td data-tone="success" data-v="0.462">90</td><td data-tone="warning" data-v="0.128">25</td><td data-tone="danger" data-v="0.01">2</td></tr>
<tr data-empty title="Nov 9: none"><th scope="row"><span class="visually-hidden">Nov 9</span></th><td>0</td><td>0</td><td>0</td></tr>
<tr title="Nov 10: 150 free, 40 paid, 5 failed"><th scope="row">Nov 10</th><td data-tone="success" data-v="0.769">150</td><td data-tone="warning" data-v="0.205">40</td><td data-tone="danger" data-v="0.026">5</td></tr>
<tr title="Nov 11: 170 free, 12 paid, 0 failed"><th scope="row"><span class="visually-hidden">Nov 11</span></th><td data-tone="success" data-v="0.872">170</td><td data-tone="warning" data-v="0.062">12</td><td data-tone="danger" data-v="0">0</td></tr>
<tr title="Nov 12: 125 free, 33 paid, 4 failed"><th scope="row"><span class="visually-hidden">Nov 12</span></th><td data-tone="success" data-v="0.641">125</td><td data-tone="warning" data-v="0.169">33</td><td data-tone="danger" data-v="0.021">4</td></tr>
<tr title="Nov 13: 140 free, 29 paid, 3 failed"><th scope="row">Nov 13</th><td data-tone="success" data-v="0.718">140</td><td data-tone="warning" data-v="0.149">29</td><td data-tone="danger" data-v="0.015">3</td></tr>
<tr title="Nov 14: 155 free, 31 paid, 2 failed"><th scope="row"><span class="visually-hidden">Nov 14</span></th><td data-tone="success" data-v="0.795">155</td><td data-tone="warning" data-v="0.159">31</td><td data-tone="danger" data-v="0.01">2</td></tr>
</tbody>
</table>Series without a tone take the chart colors by their column.
| Day | Site | Partners | Walk-in |
|---|---|---|---|
| Mon | 12 | 5 | 3 |
| Tue | 15 | 6 | 2 |
| Wed | 9 | 8 | 4 |
| Thu | 18 | 4 | 1 |
| Fri | 22 | 9 | 5 |
| Sat | 30 | 12 | 6 |
| Sun | 26 | 10 | 7 |
<table class="chart">
<caption>Bookings by channel, this week</caption>
<thead><tr><th scope="col">Day</th><th scope="col">Site</th><th scope="col">Partners</th><th scope="col">Walk-in</th></tr></thead>
<tbody>
<tr title="Mon: 12 site, 5 partners, 3 walk-in"><th scope="row">Mon</th><td data-v="0.25">12</td><td data-v="0.104">5</td><td data-v="0.062">3</td></tr>
<tr title="Tue: 15 site, 6 partners, 2 walk-in"><th scope="row">Tue</th><td data-v="0.312">15</td><td data-v="0.125">6</td><td data-v="0.042">2</td></tr>
<tr title="Wed: 9 site, 8 partners, 4 walk-in"><th scope="row">Wed</th><td data-v="0.188">9</td><td data-v="0.167">8</td><td data-v="0.083">4</td></tr>
<tr title="Thu: 18 site, 4 partners, 1 walk-in"><th scope="row">Thu</th><td data-v="0.375">18</td><td data-v="0.083">4</td><td data-v="0.021">1</td></tr>
<tr title="Fri: 22 site, 9 partners, 5 walk-in"><th scope="row">Fri</th><td data-v="0.458">22</td><td data-v="0.188">9</td><td data-v="0.104">5</td></tr>
<tr title="Sat: 30 site, 12 partners, 6 walk-in"><th scope="row">Sat</th><td data-v="0.625">30</td><td data-v="0.25">12</td><td data-v="0.125">6</td></tr>
<tr title="Sun: 26 site, 10 partners, 7 walk-in"><th scope="row">Sun</th><td data-v="0.542">26</td><td data-v="0.208">10</td><td data-v="0.146">7</td></tr>
</tbody>
</table>chart-values writes each column's number above it - for one series and a few columns, where the numbers are what a person reads.
| Month | Guests |
|---|---|
| Jul | 220 |
| Aug | 280 |
| Sep | 180 |
| Oct | 240 |
| Nov | 340 |
| Dec | 400 |
<table class="chart chart-values">
<caption>Guests per month</caption>
<thead><tr><th scope="col">Month</th><th scope="col">Guests</th></tr></thead>
<tbody>
<tr><th scope="row">Jul</th><td data-v="0.55">220</td></tr>
<tr><th scope="row">Aug</th><td data-v="0.7">280</td></tr>
<tr><th scope="row">Sep</th><td data-v="0.45">180</td></tr>
<tr><th scope="row">Oct</th><td data-v="0.6">240</td></tr>
<tr><th scope="row">Nov</th><td data-v="0.85">340</td></tr>
<tr><th scope="row">Dec</th><td data-v="1">400</td></tr>
</tbody>
</table>Shares of a whole
chart-bar with chart draws one row as a single bar across: each segment's data-v is its share of the whole, and the legend under it names the series - write the share in the legend's words too. The bar is 12 px thick; --redz-chart-bar-height sets another.
| Method | Card, 58% | PromptPay, 31% | Cash, 11% |
|---|---|---|---|
| November | 58% | 31% | 11% |
<table class="chart chart-bar">
<caption>Payments in November</caption>
<thead><tr><th scope="col">Method</th><th scope="col">Card, 58%</th><th scope="col">PromptPay, 31%</th><th scope="col">Cash, 11%</th></tr></thead>
<tbody>
<tr><th scope="row">November</th><td data-v="0.58">58%</td><td data-v="0.31">31%</td><td data-v="0.11">11%</td></tr>
</tbody>
</table>table class="chart-rows" breaks a whole into rows, longest first: each tr a label, its count and its share in words, and a bar under them whose length is the row's data-v. The header cells name the columns for a screen reader and are hidden from sight. The bars take the first chart color; a row's data-tone gives it a state's.
| Source | Bookings | Share |
|---|---|---|
| Search | 182 | 52% |
| Partners | 94 | 27% |
| Social networks | 49 | 14% |
| Other | 25 | 7% |
<table class="chart-rows">
<caption>Where bookings came from</caption>
<thead><tr><th scope="col">Source</th><th scope="col">Bookings</th><th scope="col">Share</th></tr></thead>
<tbody>
<tr data-v="0.52"><th scope="row">Search</th><td>182</td><td>52%</td></tr>
<tr data-v="0.27"><th scope="row">Partners</th><td>94</td><td>27%</td></tr>
<tr data-v="0.14"><th scope="row">Social networks</th><td>49</td><td>14%</td></tr>
<tr data-v="0.07" data-tone="secondary"><th scope="row">Other</th><td>25</td><td>7%</td></tr>
</tbody>
</table>Sparklines
span class="spark" with role="img" and its summary in aria-label draws one bar per run, oldest first: data-v its share of the longest, data-state - ok, running, timeout, failed - its color, title its tooltip. 18 px high in a row, spark-md 34 px, spark-lg 64 px; spark-median with data-median draws the median as a dashed line.
| Agent | Last 20 runs |
|---|---|
| backup.nightly |
<table class="table" aria-label="Agents">
<thead><tr><th scope="col">Agent</th><th scope="col">Last 20 runs</th></tr></thead>
<tbody>
<tr><td class="font-monospace">backup.nightly</td><td><span class="spark" role="img" aria-label="20 runs: 18 OK, 1 timeout, 1 failed"><i data-state="ok" data-v="0.31" title="OK · 1.2 s"></i><i data-state="ok" data-v="0.28" title="OK · 1.1 s"></i><i data-state="ok" data-v="0.36" title="OK · 1.4 s"></i><i data-state="ok" data-v="0.33" title="OK · 1.3 s"></i><i data-state="timeout" data-v="1" title="Timeout · 3.9 s"></i><i data-state="ok" data-v="0.31" title="OK · 1.2 s"></i><i data-state="ok" data-v="0.26" title="OK · 1 s"></i><i data-state="ok" data-v="0.38" title="OK · 1.5 s"></i><i data-state="ok" data-v="0.31" title="OK · 1.2 s"></i><i data-state="failed" data-v="0.72" title="Failed · 2.8 s"></i><i data-state="ok" data-v="0.33" title="OK · 1.3 s"></i><i data-state="ok" data-v="0.28" title="OK · 1.1 s"></i><i data-state="ok" data-v="0.31" title="OK · 1.2 s"></i><i data-state="ok" data-v="0.41" title="OK · 1.6 s"></i><i data-state="ok" data-v="0.31" title="OK · 1.2 s"></i><i data-state="ok" data-v="0.33" title="OK · 1.3 s"></i><i data-state="ok" data-v="0.28" title="OK · 1.1 s"></i><i data-state="ok" data-v="0.36" title="OK · 1.4 s"></i><i data-state="ok" data-v="0.31" title="OK · 1.2 s"></i><i data-state="running" data-v="0.15" title="Running · 0.6 s"></i></span></td></tr>
</tbody>
</table>
<figure class="vstack gap-4">
<span class="spark spark-lg spark-median" role="img" aria-label="20 runs, median 1.2 s, longest 3.9 s" data-median="0.31"><i data-state="ok" data-v="0.31" title="Nov 3, 02:00 · 1.2 s"></i><i data-state="ok" data-v="0.28" title="Nov 3, 03:00 · 1.1 s"></i><i data-state="ok" data-v="0.36" title="Nov 3, 04:00 · 1.4 s"></i><i data-state="ok" data-v="0.33" title="Nov 3, 05:00 · 1.3 s"></i><i data-state="timeout" data-v="1" title="Nov 3, 06:00 · 3.9 s"></i><i data-state="ok" data-v="0.31" title="Nov 3, 07:00 · 1.2 s"></i><i data-state="ok" data-v="0.26" title="Nov 3, 08:00 · 1 s"></i><i data-state="ok" data-v="0.38" title="Nov 3, 09:00 · 1.5 s"></i><i data-state="ok" data-v="0.31" title="Nov 3, 10:00 · 1.2 s"></i><i data-state="failed" data-v="0.72" title="Nov 3, 11:00 · 2.8 s"></i><i data-state="ok" data-v="0.33" title="Nov 3, 12:00 · 1.3 s"></i><i data-state="ok" data-v="0.28" title="Nov 3, 13:00 · 1.1 s"></i><i data-state="ok" data-v="0.31" title="Nov 3, 14:00 · 1.2 s"></i><i data-state="ok" data-v="0.41" title="Nov 3, 15:00 · 1.6 s"></i><i data-state="ok" data-v="0.31" title="Nov 3, 16:00 · 1.2 s"></i><i data-state="ok" data-v="0.33" title="Nov 3, 17:00 · 1.3 s"></i><i data-state="ok" data-v="0.28" title="Nov 3, 18:00 · 1.1 s"></i><i data-state="ok" data-v="0.36" title="Nov 3, 19:00 · 1.4 s"></i><i data-state="ok" data-v="0.31" title="Nov 3, 20:00 · 1.2 s"></i><i data-state="running" data-v="0.15" title="Nov 3, 21:00 · 0.6 s"></i></span>
<figcaption class="fs-12 text-muted tabular-nums">Median 1.2 s · longest 3.9 s</figcaption>
</figure>A board of lanes
table class="lanes" lays a time window across: a header cell lanes-scale holds the captions, each at its data-at, and each row is a lane - its name in the th, which may hold a button, and its track in the td. In a track lanes-tick marks a run at data-at, lanes-tick-once a single one, and lanes-span from data-from to data-to a run that goes on, hatched, with its caption. The red line is now, at the table's data-now - 0, the start, by default; the grid has --redz-lanes-steps parts, 8. A caption that would touch the one before it hides, in any language and at any width. Elements that share a data-highlight key light together: a lane, its track included, and its row in a table.
| now18:0021:0000:0003:0006:0009:0012:0015:00 | |
|---|---|
| mail.queue | continuous |
| backup.nightly | |
| fx.rates |
| Agent | Next run |
|---|---|
| mail.queue | Every minute |
| backup.nightly | 02:00 |
| fx.rates | 16:00 |
<table class="lanes">
<caption>The next 24 hours</caption>
<thead><tr><td></td><th class="lanes-scale" scope="col"><span data-at="0">now</span><span data-at="0.125">18:00</span><span data-at="0.25">21:00</span><span data-at="0.375">00:00</span><span data-at="0.5">03:00</span><span data-at="0.625">06:00</span><span data-at="0.75">09:00</span><span data-at="0.875">12:00</span><span data-at="1">15:00</span></th></tr></thead>
<tbody>
<tr data-highlight="mail.queue"><th scope="row">mail.queue</th><td><span class="lanes-span" data-from="0" data-to="1" title="mail.queue - every minute">continuous</span></td></tr>
<tr data-highlight="backup.nightly"><th scope="row">backup.nightly</th><td><i class="lanes-tick-once lanes-tick" data-at="0.458" title="backup.nightly - 02:00"></i></td></tr>
<tr data-highlight="fx.rates"><th scope="row">fx.rates</th><td><i class="lanes-tick" data-at="0.04" title="fx.rates - 16:00"></i><i class="lanes-tick" data-at="0.207" title="fx.rates - 20:00"></i><i class="lanes-tick" data-at="0.373" title="fx.rates - 00:00"></i><i class="lanes-tick" data-at="0.54" title="fx.rates - 04:00"></i><i class="lanes-tick" data-at="0.707" title="fx.rates - 08:00"></i><i class="lanes-tick" data-at="0.873" title="fx.rates - 12:00"></i></td></tr>
</tbody>
</table>
<table class="table" aria-label="Agents">
<thead><tr><th scope="col">Agent</th><th scope="col">Next run</th></tr></thead>
<tbody>
<tr data-highlight="mail.queue"><td class="font-monospace">mail.queue</td><td>Every minute</td></tr>
<tr data-highlight="backup.nightly"><td class="font-monospace">backup.nightly</td><td>02:00</td></tr>
<tr data-highlight="fx.rates"><td class="font-monospace">fx.rates</td><td>16:00</td></tr>
</tbody>
</table>Accessibility
- A chart is its table: the caption names it, the header cells name the series, each row says its day and its numbers.
charts.jsgives achartorchart-rowstable the table's roles, since WebKit drops them once CSS lays the table out otherwise. - A share bar and breakdown rows say their shares in words - the legend's, the rows' - and the bar repeats them.
- A color never speaks alone: the legend names every series, a state's segment is named by its header cell, and a sparkline's summary is its name.
- Three of the light theme's series colors are under 3:1 against the page: the legend and the table are what tell them apart.
- In forced colors every bar is drawn in the text color; the numbers are in the table.