RZREDZ.UI

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.

Messages sent per day
DayFreePaidFailed
Nov 1120304
Nov 2132282
Nov 398416
Nov 4000
Nov 5144353
Nov 6160221
Nov 7110309
Nov 890252
Nov 9000
Nov 10150405
Nov 11170120
Nov 12125334
Nov 13140293
Nov 14155312
<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.

Bookings by channel, this week
DaySitePartnersWalk-in
Mon1253
Tue1562
Wed984
Thu1841
Fri2295
Sat30126
Sun26107
<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.

Guests per month
MonthGuests
Jul220
Aug280
Sep180
Oct240
Nov340
Dec400
<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.

Payments in November
MethodCard, 58%PromptPay, 31%Cash, 11%
November58%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.

Where bookings came from
SourceBookingsShare
Search18252%
Partners9427%
Social networks4914%
Other257%
<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.

AgentLast 20 runs
backup.nightly
Median 1.2 s · longest 3.9 s
<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.

The next 24 hours
now18:0021:0000:0003:0006:0009:0012:0015:00
mail.queuecontinuous
backup.nightly
fx.rates
AgentNext run
mail.queueEvery minute
backup.nightly02:00
fx.rates16: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.js gives a chart or chart-rows table 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.