vis4.net

Hi, I'm Gregor, welcome to my blog where I mostly write about data visualization, cartography, colors, data journalism and some of my open source software projects.

Making HTML tables in D3 doesn't need to be a pain

#code#d3js#opensource

tl;dr: Here’s a demo with source code.

D3 is nice, but it also makes some simple things look really complicated. One of them is making a simple HTML table. Let’s say you got a simple dataset, stored as array of objects just as you would get from d3.csv:

var movies = [
	{ title: 'The Godfather', year: 1972, length: 175, budget: 6000000, rating: 9.1 },
	{ title: 'The Shawshank Redemption', year: 1994, length: 142, budget: 25000000, rating: 9.1 },
	{ title: 'The Lord of the Rings 3', year: 2003, length: 251, budget: 94000000, rating: 9 }
	/* ... */
];

To render this in a table you would typically start writing some code like this:

var table = d3.select('body').append('table');

var tr = table.selectAll('tr').data(movies).enter().append('tr');

Now you got a selection of table row elements, each of which is bound to one movie. But how do you make the table columns? What I did a lot was this:

tr.append('td').html(function (m) {
	return m.title;
});
tr.append('td').html(function (m) {
	return m.year;
});
tr.append('td').html(function (m) {
	return m.budget;
});

That looks easy at first, but of course you want more stuff, like a class name depending on the column etc. So the above code turns into this:

tr.append('td')
	.attr('class', 'title')
	.html(function (m) {
		return m.title;
	});

tr.append('td')
	.attr('class', 'center')
	.html(function (m) {
		return m.year;
	});

tr.append('td')
	.attr('class', 'num')
	.html(function (m) {
		return m.budget;
	});

Also you might need a table header, so essentially you copy this entire block to create the th elements. Better make sure you keep them in the same order if you decide to change your code later. To make it short, this is an entire mess. It’s not the right way to do a table.

HTML tables in D3, the right way

To make tables fun again, we simply define a set of columns as an array of objects. Note that some of the attributes of the column objects are functions, these will later be evaluated against the row objects to get the values for each cell.

var columns = [
	{
		head: 'Movie title',
		cl: 'title',
		html: function (row) {
			return r.title;
		}
	},
	{
		head: 'Year',
		cl: 'center',
		html: function (row) {
			return r.year;
		}
	},
	{
		head: 'Length',
		cl: 'center',
		html: function (row) {
			return r.length;
		}
	},
	{
		head: 'Budget',
		cl: 'num',
		html: function (row) {
			return r.budget;
		}
	},
	{
		head: 'Rating',
		cl: 'num',
		html: function (row) {
			return r.rating;
		}
	}
];

Actually, since I really don’t like all these verbose getter functions here, let’s instead use the nice ƒ helper function from the d3-jetpack and compress the code a bit:

var columns = [
	{ head: 'Movie title', cl: 'title', html: ƒ('title') },
	{ head: 'Year', cl: 'center', html: ƒ('year') },
	{ head: 'Length', cl: 'center', html: ƒ('length') },
	{ head: 'Budget', cl: 'num', html: ƒ('budget') },
	{ head: 'Rating', cl: 'num', html: ƒ('rating') }
];

We can now use these column objects in a data join to create the table header. Much more fun than duplicating all the code for each column.

table
	.append('thead')
	.append('tr')
	.selectAll('th')
	.data(columns)
	.enter()
	.append('th')
	.attr('class', ƒ('cl'))
	.text(ƒ('head'));

Finally, we can do the same with the table body. But if we would just pass the column objects here, we would lose the information of the row. So that’s why we evaluate all the function properties of the column objects against the row objects. This way we convert the list of column objects into a list of cell objects to use in the second data join:

table
	.append('tbody')
	.selectAll('tr')
	.data(movies)
	.enter()
	.append('tr')
	.selectAll('td')
	.data(function (row, i) {
		// evaluate column objects against the current row
		return columns.map(function (c) {
			var cell = {};
			d3.keys(c).forEach(function (k) {
				cell[k] = typeof c[k] == 'function' ? c[k](row, i) : c[k];
			});
			return cell;
		});
	})
	.enter()
	.append('td')
	.html(ƒ('html'))
	.attr('class', ƒ('cl'));

And that’s it. Again, here’s a link to the demo with full source code.

Comments

Dheepan (Jul 21, 2015)

Hi Gregor,

How do I access the values inside the rows to use in functions? In my case I want to use the values in my class td.num to determine the background fill based on variable sentcolor.

My return function for .style selected on (“td.num”) can only access the index number but not the actual value.

See example here, link

Nagarajan Chinnasamy (Apr 29, 2015)

In case if you need an SVG based Grid (Table with adjustable columns, sorting etc.), you can look at: https://github.com/PMSI-AlignAlytics/scrollgrid

This is based on D3.

keith (May 10, 2015)

including datatables.js is very simple to add to your example and makes for nice user enabled sorting/filtering.

i was working on a project where the data drawn by d3 was to be controlled by datatables but never did implement that part of it. i think that would be pretty slick.

Will Morris (Apr 24, 2015)

This is a nice solution. However, for an HTML table, why not use something like handlebars.js? Just curious about your experience with one vs the other.

Gregor Aisch (Apr 24, 2015)

In the context where I am using this code, D3 is part of the default project setup, but handlebars isn’t. We do a lot of graphics so it kind of makes sense to stay in one framework..

Pat (Jul 20, 2015)

The array-of-columns is a very cool approach.

It seems like the column definitions would be a good place to put a sorting function, which would enable a simple version of the kinds of sorting the other commenters are suggesting.