-------------------------------------------------------
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.
- Add some CSS
- Add some table building d3.js code
- 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.
Great tutorial, this is really helpful.
ReplyDeleteIs there a way to define a table on either the left or right hand side of the graph?
Something like div tags in HTML.
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!
ReplyDeleteWould someone please point me to an example of adding multiple rows in a thead where the cells have mixed colspan widths?
ReplyDeleteOoo.... 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?
DeleteDone! http://stackoverflow.com/questions/18026947/using-d3js-to-build-table-with-multiple-rows-in-thead-where-some-cells-have-mixe
ReplyDeleteExcellent! It will be interesting to see what comes up.
DeleteThis is sooo useful!! Thank you very much!!
ReplyDeleteI just built this one using components on vida.io with Bootstrap theme and SlickGrid table. https://vida.io/documents/WHZXPPE6p2Tbtsd49
ReplyDeleteHonestly I cannot interpret these lines of code. Can you simplify, or define functions externally to simplify it?
ReplyDelete// 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]};
});
})
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
DeleteHi, just searching on D3 and find your code, can you explain to me what is // Add the Y Axis
ReplyDeletesvg.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
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