D3 Tips and Tricks v4

Saturday, 2 February 2013

Add an HTML table to your d3.js graph

The following post is a portion of the D3 Tips and Tricks document which is free to download. To use this post in context, consider it with the others in the blog or just download the pdf  and / or the examples from the downloads page:-)
-------------------------------------------------------
So graphs and graphics are D3's bread and butter you'd think. Hmm...

Well yes and no.

Yes D3 has extraordinary powers for presenting and manipulating images in a web page. But if you've read through the entirety of the d3.js main site (haven't we all) you will recall that D3 actually stands for Data Driven Documents. It's not necessarily about the pretty pictures and the swirling cascade of colour. It's about generating something in a web browser based on data.

This transitions nicely into consideration of adding a table of information that can accompany your graph (it could just as easily (or easier) stand alone, but for the sake of continuity, we'll use the graph).

What we'll do is add the data that we've used to make our graph under the graph itself. To make sure that it's all nicely aligned, we'll place it in a table.

It should end up looking a little like this (and this has been cropped slightly at the bottom to avoid expanding the page with rows of numbers / dates).

The code was drawn from and example provided by Shawn Allen (http://jsfiddle.net/7WQjr/) on Google Groups (http://stackoverflow.com/questions/9268645/d3-creating-a-table-linked-to-a-csv-file). In fact, the post itself is an excellent one if you are considering creating a table straight from a csv file.

HTML Tables

I'm walking a fine line here since I have a remarkably small amount of knowledge on HTML tables. So I'll try to provide a brief overview as I understand it and as I see it represented in the code below, but for a far fuller explanation, let Google be your friend.

Tables are made up of rows, columns and the data (that goes in each cell). All you need to do to successfully place a table on a web page is to lay out the rows and columns in a logical sequence using the appropriate HTML tags and you're away.

For example here's the total HTML code for a web page to display a simple table;
<!DOCTYPE html>
<body>
<table border="1">
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr>
<td>row 1, cell 1</td>
<td>row 1, cell 2</td>
</tr>
<tr>
<td>row 2, cell 1</td>
<td>row 2, cell 2</td>
</tr>
</table>
</body>
This will result in a table that looks a little like this in a web browser;

Header 1 Header 2
row 1, cell 1 row 1, cell 2
row 2, cell 1 row 2, cell 2

The entire table itself is enclosed in <table> tags. Each row is enclosed in <tr> tags. Each row has two items which equates to the two columns. Each piece of data for each cell is enclosed in a <td> tag except for the first row, which is a header and therefore has a special tag <th> that denotes it as a header making it bold and centred. For the sake of ease of viewing we have told the table to place a border around each cell and we do this in the first <table> tag with the border="1" statement.

The good news is that you don't need to fully understand all this, but it will help with the explanation of what we're doing in the code below.

There are three main things you need to do to the basic line graph to get your table to display.
  1. Add some CSS
  2. Add some table building d3.js code
  3. Make a small but cunning change...

First the CSS

This just helps the table with formatting and making sure the individual cells are spaced appropriately;
td, th {
    padding: 1px 4px;
}
This sets a padding of 1 px around each cell and 4 px between each column.

Feel free to play with the figures to suit your application, I've just set them there because I thought they looked appropriate.

I've placed this portion of CSS at the end of our <style> section.

Now the d3.js code

Oki doki... Hopefully you have a loose understanding of the html layout of a table as explained above, but if not you can always go with the “it just works” approach.

Here's what we should add into our simple graph example;
function tabulate(data, columns) {
    var table = d3.select("body").append("table")
            .attr("style", "margin-left: 250px"),
        thead = table.append("thead"),
        tbody = table.append("tbody");

    // append the header row
    thead.append("tr")
        .selectAll("th")
        .data(columns)
        .enter()
        .append("th")
            .text(function(column) { return column; });

    // create a row for each object in the data
    var rows = tbody.selectAll("tr")
        .data(data)
        .enter()
        .append("tr");

    // create a cell in each row for each column
    var cells = rows.selectAll("td")
        .data(function(row) {
            return columns.map(function(column) {
                return {column: column, value: row[column]};
            });
        })
        .enter()
        .append("td")
        .attr("style", "font-family: Courier")
            .html(function(d) { return d.value; });
    
    return table;
}

// render the table
 var peopleTable = tabulate(data, ["date", "close"]);
And we should take care to add it into the code at the end of the portion where we've finished drawing the graph, but before the enclosing curly and regular brackets that complete the portion of the graph that has loaded our data.tsv file. This is because we want our new piece of code to have access to that data and if we place it after those brackets it won't know what data to display.

So, right about here;
        // Add the Y Axis
        svg.append("g")
            .attr("class", "y axis")
            .call(yAxis);
                                 // <= Add the code right here!
});
Now, we're going to break with tradition a bit here and examine what our current state of code produces. Then we're going to explain something different. THEN we're going to come back and explain the code...

Check it out...


Not quite as we has originally envisaged?

Indeed, the date has taken it upon itself to expand from a relatively modest format of day-abbreviated month-two digit year (30-Apr-12) to a behemoth of a thing (Mon Apr 30 2012 00:00:00 GMT+1200 (New Zealand Standard Time)) that we certainly didn't intend, let alone have in our data.tsv file.

What's going on here?

Well, To be perfectly frank, I'm not entirely sure. But this is what I'm going to propose. The JavaScript code recognises and deals with the 'date' variable as being a date/time. So that when we proceed to display the variable on the screen, the browser says, “this is a date / time formatted piece of data, therefore it must be formatted in the following way”. I had a play with a few ideas to correct it via an HTML formatting instruction, but drew a blank and then I stumbled on another way to solve the problem. Hence the third small but cunning change to our original code.

A small but cunning change...

So... Our table has decided to develop a mind of it's own and format the date time as it sees fit. Well fair enough (I for one welcome our web time formatting overlords). So how do we convince it to display the values in their natural form?

Well, one solution that we could employ is to not tell the JavaScript that our 'date' value in the data is actually time. In that condition, the code should treat the values as an ordinary string and print it directly as it appears.
The good news is that this is pretty easy to do. Where originally we had a block of data that consisted of `date` and `close`, all at different times, we will now add a new variable called `date1` which will be the variable that we convert to a time and draw the graph with. Leaving `date` to be the text string that will be printed in our table.

How to do it?

It's actually remarkably easy. Just change the following lines in the basic line graph code to amend `date` to `date1` and you're good to go.
    .x(function(d) { return x(d.date1); })

    d.date1 = parseDate(d.date);

    x.domain(d3.extent(data, function(d) { return d.date1; }));
The middle line is probably the most significant, since this is the point where we declare `date1`, assign a time format and bring a new column of data into being. The others simply refer to the data.

So we'll make those small changes and now we can return to explain the d3.js code...

Explaining the d3.js code (reloaded).

So back we come to explain what is going on in the d3.js code that we presented a page or two back. Obviously it's a fairly large chunk, and we can first break it down into two chunks.

The first chunk we'll look at is in fact the last part of the code that look like this;
// render the table
 var peopleTable = tabulate(data, ["date", "close"]);
This portion simply calls the `tabulate` function using the `date` and `close` columns of our `data` array. Simply add or remove whichever columns you want to appear in your table (so long as they are in your data.tsv file) and they will be in your table. The tabulate function makes up all of the other part of the added code.

 So we come to the first block of the tabulate function;
function tabulate(data, columns) {
    var table = d3.select("body").append("table")
            .attr("style", "margin-left: 250px"),
        thead = table.append("thead"),
        tbody = table.append("tbody");
Here the tabulate function is declared (`function tabulate`) and the variables that the function will be using are specified(`(data, columns) `). In or case `data` is of course our `data` array and columns` refers to `["date", "close"]`.

The next line appends the table to the `body` of the web page (so it will occur just under the graph in this case). The I do something just slightly sneaky. The line `.attr("style", "margin-left: 250px"),` is actually not the not that was used to produce the table with the hugh date/ time formatted info on. I deliberately used `.attr("style", "margin-left: 0px"),` for the huge date / time table since it's job is to indent the table by a specified amount from the left hand side of the page. And since the huge date time values would have pushed the table severely to the right, I cheated and used `0` instead of `250`. For the purposes of the final example where the date / time values are formatted as expected, `250` is a good value.

The next two lines declare the functions we will use to add in the header cells (since they use the `<th>` tags for content) and the cells for the main body of the table (they use `<td>`).

The next block of code adds in the header row;
    thead.append("tr")
        .selectAll("th")
        .data(columns)
        .enter()
        .append("th")
            .text(function(column) { return column; });
Here we first append a tow tag (`<tr>`), then we gather all the `columns` that we have in our function (remember they were ` ["date", "close"]` and add them to our row using header tags (`th`).

The next block of code assigns the `row` variable to return (append) a row tag (<`tr`) whenever its called ...
    var rows = tbody.selectAll("tr")
        .data(data)
        .enter()
        .append("tr");
… and it is in the following block of code...
    var cells = rows.selectAll("td")
        .data(function(row) {
            return columns.map(function(column) {
                return {column: column, value: row[column]};
            });
        })
        .enter()
        .append("td")
        .attr("style", "font-family: Courier")
            .html(function(d) { return d.value; });

Where we select each row that we've added (`var cells = rows.selectAll("td")`). Then the following five lines works out from the intersection of the row and column which piece of data we're looking at for each cell.

Then the last four lines take that piece of data (`d.value`) and wrap it in table data tags (`<td>`) and place it in the correct cell as HTML.

It's a very neat piece of code and I struggle to get my head around it, but that doesn't mean that I can appreciate the cleverness of it :-)

Wrap up

So there we have it. Hopefully enough to explain what is going on and perhaps also enough to convince ourselves that D3 is indeed more than just pretty pictures. It's all about the Data Driven Documents.
This file has been saved as table-plus-graph.html and has been added into the downloads section on d3noob.org with the general examples files.

The above description (and heaps of other stuff) is in the D3 Tips and Tricks document that can be accessed from the downloads page of d3noob.org.

12 comments:

  1. Great tutorial, this is really helpful.
    Is there a way to define a table on either the left or right hand side of the graph?
    Something like div tags in HTML.

    ReplyDelete
  2. Ahh... What great timing!! As chance would have it I have just (8 hours ago) published an update to the D3 Tips and Tricks book (https://leanpub.com/D3-Tips-and-Tricks) that includes a section on setting up different sections on a web page for 'stuff' using Bootstrap. Have a read and I think that you're right, adding in an ID selector using Bootstrap (in the HTML) and then anchor the table to the ID. Have fun!

    ReplyDelete
  3. Would someone please point me to an example of adding multiple rows in a thead where the cells have mixed colspan widths?

    ReplyDelete
    Replies
    1. Ooo.... Good question... I don't know of an answer (:-)) but i think that a good place to start would be some HTML grounding and then see if that's compatible with what we're trying to do with d3. Failing that, it may be worth having a look to see if boostrap can bring anything to the table (pun intended). Perhaps worth asking on Stack Overflow?

      Delete
  4. Done! http://stackoverflow.com/questions/18026947/using-d3js-to-build-table-with-multiple-rows-in-thead-where-some-cells-have-mixe

    ReplyDelete
    Replies
    1. Excellent! It will be interesting to see what comes up.

      Delete
  5. This is sooo useful!! Thank you very much!!

    ReplyDelete
  6. I just built this one using components on vida.io with Bootstrap theme and SlickGrid table. https://vida.io/documents/WHZXPPE6p2Tbtsd49

    ReplyDelete
  7. Honestly I cannot interpret these lines of code. Can you simplify, or define functions externally to simplify it?
    // create a cell in each row for each column
    var cells = rows.selectAll("td")
    .data(function(row) {
    return columns.map(function(column) {
    return {column: column, value: row[column]};
    });
    })

    ReplyDelete
    Replies
    1. I found them really tricky as well, so I feel your pain. Unfortunately I wrote the explanation to suit how I thought it worked and to assist in recall for myself and I fully expect that it will not suit everyone (probably far from it). I will have to revisit the script at some time in the future, but that will be a while away. In the mean time check out some of the other work that folks have done with tables and d3.js (I make the assumption that Google will be our friend). The book has had some editing so it is slightly different to the original post that appears here. I might be worth downloading that (it's free after all). https://leanpub.com/D3-Tips-and-Tricks

      Delete
  8. Hi, just searching on D3 and find your code, can you explain to me what is // Add the Y Axis
    svg.append("g")
    .attr("class", "y axis")
    .call(yAxis);
    // <= Add the code right here!
    });

    What code // <= Add the code right here! to be there ? then the table is in the right position

    ReplyDelete
    Replies
    1. The 'Add the code' lineis pointing to the space in the file that draws the line graph where you add the code that will draw the table. The table portion is described immediately above that paragraph. Have another read of the post and see if that helps make sense of it. check out this example to help see how it goes together (sorry the example was developed long after the post). Good luck.

      Delete