Chartist.js data from table
I like JavaScript. Out of the box JavaScript can do great things1. One of my favourite things is progressive enhancement - getting the brass tacks, minimal viable functionality for everyone running and then making it better.
I also love the indieweb. I frequently dance around the indieweb's webring to find interesting ideas and people. On one such journey I saw Jeremy Cherfas wanting to do nifting things with JavaScript. The initial example - provide a table of data in pure HTML for ease of reading to everyone; and then provide a nifty graph of said data next to it as an enhanced display. Graphs are great for visual thinkers and spotting trends. He'd got a great JavaScript charting library in chartist.js. But the chartist library requires a JSON object to make a graph from, and Jeremy rightly noted the data is already on the page, why provide it again.
Using this as a learning experience for myself as well, diving into the newer ECMAScript constructs, I have the below:
Monday | Tuesday | Wednesday | Thursday | Friday |
---|---|---|---|---|
12 | 9 | 7 | 8 | 5 |
3 | 1 | 3.5 | 17 | 3 |
13 | 3 | 4 | 5 | 6 |
1<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chartist@0.11.4/dist/chartist.min.css" integrity="sha384-0WKi5n+SSFA5+YHZzrjn5EA3EsoI6skyCK7W5geFP9qNIaA+ZLiQFdHLCRfXedLj" crossorigin="anonymous">
2<script src="https://cdn.jsdelivr.net/npm/chartist@0.11.4/dist/chartist.min.js" integrity="sha384-bf6CtCfLXLDSLve8PkWwKSMdpwsiwVijot4kH/wDChhARuTElQyXxF6Ki9fa9oE2" crossorigin="anonymous"></script>
3<table id="table-source">
4 <thead>
5 <tr>
6 <th>Monday</th>
7 <th>Tuesday</th>
8 <th>Wednesday</th>
9 <th>Thursday</th>
10 <th>Friday</th>
11 </tr>
12 </thead>
13 <tbody>
14 <tr><td>12</td><td>9</td><td>7</td><td>8</td><td>5</td></tr>
15 <tr><td>3</td><td>1</td><td>3.5</td><td>17</td><td>3</td></tr>
16 <tr><td>13</td><td>3</td><td>4</td><td>5</td><td>6</td></tr>
17 </tbody>
18</table>
19
20<div class="ct-chart"></div>
21
22<script>
23var base_design = {
24 fullWidth: false,
25 width: 400,
26 height: 300,
27 chartPadding: {
28 right: 40
29 }
30 };
31
32tableToChart(document.getElementById('table-source'));
33
34function tableToChart(table) {
35 // Handle if we cannot find the target
36 if (table != null) {
37 // Get data
38 var labels = Array.from(table.getElementsByTagName('th'), th => th.textContent.trim()));
39 var series = Array.from(table.querySelectorAll("tbody tr"), tr => Array.from(tr.getElementsByTagName('td'), td => parseFloat(td.textContent, 10)));
40
41 new Chartist.Line('.ct-chart',
42 {"labels": labels, "series": series},
43 base_design
44 );
45 return true;
46 } else {
47 return false;
48 }
49}
50</script>
51<style> .ct-series * { transform: translate(30px,0); }</style>
Now for the fun part
Unless you can explain something, can you really understand it? So here goes.
- [Line 1-2] I've included the two library bits for chartist.js, the css in
link
and the javascript inscript
. Note I've version locked and added an SRI integrity checker. - [Line 3-18] One table of data, nicked from the example page. Note that I've marked up the table with
thead
andtbody
, as well as usingth
andtd
as appropriate. I've given this the id oftable-source
to find it later. - [Line 20] One div element, with the class identifying where the final graph goes. You include that as the first parameter to the
Chartist.Line
function. - My script:
- [Line 23-30] I've moved the design object into a variable so that if there are multiple charts on the page, they can share the same configuration. You include that as the third parameter to the
Chartist.Line
function. - [Line 32] Call the tableToChart function. The first parameter is the html Element of the table. We find the table using
document.getElementById('table-source')
. Thetable-source
is the id from thetable
tag above above.document.getElementById
goes through the Document Object Model of the HTML page (basically a tree of tags, and children of tags, and children of children of tags etc.) to find the ONE element with the id oftable-source
, as ID is unique2. - [Line 34-52] Here's that function.
- [Line 36] The first thing it does is stop processing if it didn't find a matching table element for the id search.
- [Line 38] It then creates the labels value for passing through to the
Chartist.Line
function. TheArray.from
3 function uses the Array constructor to build an array from any other sort of Iterable object. It takes two parameters.- First parameter is the object to iterate through.
table.getElementsByTagName('th')
function gets me all theth
elements in the selected table as anHTMLCollection
.th
stands for table header and with the markup I used, they only appear in the actual header of the table of data. Meaning I can grab all of those and know they are the labels for the values. AnHTMLCollection
isn't an array though, so it doesn't have all the nice array functions, plus it could have spacing around the labels like newlines or just spaces before the closing tag, so converting to an array and trimming makes sense. Therefor we use the second parameter. - Second parameter is a mapping function. A mapping is a function that runs over each element in the object you're iterating through. In this instance I'm running a
trim
over eachth
'stextContent
so I get rid of any leading or trailing whitespace.
- First parameter is the object to iterate through.
- [Line 39-42] We've got labels, now we need values. The
Array.from
and mapping should be familiar - in this case we alsoparseFloat
each value to make them numbers. But this time we do this twice:- First, we get all the
tr
in thetbody
to get each individual row viaquerySelectorAll
. This works by giving a css-style identification string, and the function finds all matching elements. - Then, for each found row, we then get the
td
from each and convert that into a number. Note that in the firstArray.from
we look in thetable
variable (DOM section) for alltbody tr
matches; but then we look in each foundtr
. The variousgetElementByTagName
/querySelector
etc. are given a node to start looking in, and then they look through all that node's branches and leaves. To look through everything in the page, you start with thedocument
master start point.
- First, we get all the
- [Line 44-47] I pass these
labels
andseries
through to theChartist.Line
function as per spec, with thebase_design
to keep all charts looking the same and we now have a chart.
- [Line 23-30] I've moved the design object into a variable so that if there are multiple charts on the page, they can share the same configuration. You include that as the third parameter to the
- [Line 48] Add in a stylesheet to shift the points to the middle of the column as it's driving me spare.
So this is how I tackle re-using the table data by converting it into some arrays for Chartist.js
to parse. There's some rudimentary error checking and cleaning up, but nothing super fancy. But in terms of exploring how to do this thing... I think it is understandable. Please let me know where I've been confusing or if anything needs more (or less?) detail.
Oh, exercise for the reader. Note the chart above how the labels are overlapping? How would you alter the script to only take the first 3 characters of the days so they don't overlap?
History
The first version I wrote of the above populated the series
variable using a simplistic splitIntoChunk(Array.from(table.getElementsByTagName('td'), td => parseFloat(td.textContent.trim(), 10)),labels.length);
to get all td elements, then split them into sub arrays based on the number of headers (ie. number of elements per array) via splitIntoChunk
. More efficient to get all the individual tr in the body then split those into the individual arrays required. I think. I should benchmark before getting all huffy about my code.
1function splitIntoChunk(arr, chunk) {
2 var to_return = [];
3 while(arr.length > 0) {
4 to_return.push(arr.splice(0, chunk));
5 }
6
7 return to_return;
8}
-
With big thanks to jQuery leading the way in what JavaScript (ECMAScript) should do and now does. Thanks, jQuery. ↩︎
-
At least, it's supposed to be. Of course people can make mistakes and HTML is very forgiving. It'll just ignore any ID after the first one. ↩︎
-
I'd originally used the
...
function as it's short hand and looks cute, butArray.from
has map as a second parameter, and I'd need to go[...].map()
to use the...
function so... use the best for purpose. ↩︎