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
129785
313.5173
133456
<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">
<script src="https://cdn.jsdelivr.net/npm/chartist@0.11.4/dist/chartist.min.js" integrity="sha384-bf6CtCfLXLDSLve8PkWwKSMdpwsiwVijot4kH/wDChhARuTElQyXxF6Ki9fa9oE2" crossorigin="anonymous"></script>
<table id="table-source">
    <thead>
        <tr>
            <th>Monday</th>
            <th>Tuesday</th>
            <th>Wednesday</th>
            <th>Thursday</th>
            <th>Friday</th>
        </tr>
    </thead>
    <tbody>
        <tr><td>12</td><td>9</td><td>7</td><td>8</td><td>5</td></tr>
        <tr><td>3</td><td>1</td><td>3.5</td><td>17</td><td>3</td></tr>
        <tr><td>13</td><td>3</td><td>4</td><td>5</td><td>6</td></tr>
    </tbody>
</table>

<div class="ct-chart"></div>

<script>
var base_design = {
        fullWidth: false,
        width: 400,
        height: 300,
        chartPadding: {
            right: 40
        }
    };

tableToChart(document.getElementById('table-source'));

function tableToChart(table) {
    // Handle if we cannot find the target
    if (table != null) {
        // Get data
        var labels = Array.from(table.getElementsByTagName('th'), th => th.textContent.trim()));
        var series = Array.from(table.querySelectorAll("tbody tr"), tr => Array.from(tr.getElementsByTagName('td'), td => parseFloat(td.textContent, 10)));

        new Chartist.Line('.ct-chart',
            {"labels": labels, "series": series},
            base_design
        );
        return true;
    } else {
        return false;
    }
}
</script>
<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 in script. 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 and tbody, as well as using th and td as appropriate. I've given this the id of table-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:
    1. [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.
    2. [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'). The table-source is the id from the table 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 of table-source, as ID is unique2.
    3. [Line 34-52] Here's that function.
      1. [Line 36] The first thing it does is stop processing if it didn't find a matching table element for the id search.
      2. [Line 38] It then creates the labels value for passing through to the Chartist.Line function. The Array.from3 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 the th elements in the selected table as an HTMLCollection. 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. An HTMLCollection 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 each th's textContent so I get rid of any leading or trailing whitespace.
      3. [Line 39-42] We've got labels, now we need values. The Array.from and mapping should be familiar - in this case we also parseFloat each value to make them numbers. But this time we do this twice:
        • First, we get all the tr in the tbody to get each individual row via querySelectorAll. 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 first Array.from we look in the table variable (DOM section) for all tbody tr matches; but then we look in each found tr. The various getElementByTagName/ 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 the document master start point.
      4. [Line 44-47] I pass these labels and series through to the Chartist.Line function as per spec, with the base_design to keep all charts looking the same and we now have a chart.
  1. [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.

function splitIntoChunk(arr, chunk) {
    var to_return = [];
    while(arr.length > 0) {
        to_return.push(arr.splice(0, chunk));
    }

    return to_return;
}

  1. With big thanks to jQuery leading the way in what JavaScript (ECMAScript) should do and now does. Thanks, jQuery. ↩︎

  2. 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. ↩︎

  3. I'd originally used the ... function as it's short hand and looks cute, but Array.from has map as a second parameter, and I'd need to go [...].map() to use the ... function so... use the best for purpose. ↩︎