Let's Make a Grid with D3.js

August 17, 2016
Category: TIL
Tags: D3.js and Javascript

I’ve been on a mission to relearn the fundamentals of D3.js from the ground up. This tutorial is a way to apply what I learned about data joins, click events, and selections. Along the way I learned about building arrays.

I also wrote this up as a block for those interested.

Basics

We want to make a 10x10 grid using D3.js. D3’s strength is transforming DOM elements using data. This means we’ll need some data and we’ll want to use SVG and rect elements.

Data

We could write an array of data for the grid by hand, but we wouldn’t learn anything then, would we? Let’s generate one with Javascript.

Picture a grid in your head. It is made up of rows and columns of squares. Since this is ultimately going to be represented by an SVG, let’s think about how an SVG is structured:

<svg>
	<g>
		<rect></rect>
		<rect></rect>
		<rect></rect>
	</g>
	<g>
		<rect></rect>
		<rect></rect>
		<rect></rect>
	</g>
	<g>
		<rect></rect>
		<rect></rect>
		<rect></rect>
	</g>
</svg>

What you see here is a basic structure of rows and columns. That means that when we make our data array, we want to make a nested array of rows and cells/columns inside those rows. We’ll need to use iteration to do this. Easy-peasy.

The other question we’ll have when making these arrays is, “What attributes will this grid need?”. Think about how you’d draw a grid: You start in the upper right corner of a piece of paper, draw a 1x1 square, move over the width of 1 square and draw another, and repeat until you get to the end of the row. Then you’d go back to the first square, draw one underneath it, and repeat the process. Here we’ve described positions, widths, and heights. In SVG world these are x, y, width, and height.

Here is the function I’m using to create the underlying data for the upcoming grid. It makes an array that holds 10 other arrays, which each hold 10 values:

function gridData() {
	var data = new Array();
	var xpos = 1; //starting xpos and ypos at 1 so the stroke will show when we make the grid below
	var ypos = 1;
	var width = 50;
	var height = 50;
	
	// iterate for rows	
	for (var row = 0; row < 10; row++) {
		data.push( new Array() );
		
		// iterate for cells/columns inside rows
		for (var column = 0; column < 10; column++) {
			data[row].push({
				x: xpos,
				y: ypos,
				width: width,
				height: height
			})
			// increment the x position. I.e. move it over by 50 (width variable)
			xpos += width;
		}
		// reset the x position after a row is complete
		xpos = 1;
		// increment the y position for the next row. Move it down 50 (height variable)
		ypos += height;	
	}
	return data;
}

Making a Grid with D3 Data Joins

We made a cool array above and now we’ll make the data correspond to svg:rect objects to make our grid. First we’ll need to make a div to append everything to (and, of course, don’t forget to include the latest version of D3 in your header):

<div id="grid"></div>

Now we need to assign our data to a variable so we can access it:

var gridData = gridData();	
// I like to log the data to the console for quick debugging
console.log(gridData);

Next, let’s append an SVG to the div we made and set its width and height attributes:

var grid = d3.select("#grid")
	.append("svg")
	.attr("width","510px")
	.attr("height","510px");

Next, we can apply what we learned in Mike Bostock’s Thinking With Joins to make our rows:

var row = grid.selectAll(".row")
	.data(gridData)
	.enter().append("g")
	.attr("class", "row");

And finally we make the individual cells/columns. Translating the data is a bit trickier, but the key is understanding that we are doing a selectAll on the rows, which means that any reference to data is to the contents of the single array that is bound to that row. We’ll then use a key function to access the attributes we defined (x, y, width, height):

var column = row.selectAll(".square")
	.data(function(d) { return d; })
	.enter().append("rect")
	.attr("class","square")
	.attr("x", function(d) { return d.x; })
	.attr("y", function(d) { return d.y; })
	.attr("width", function(d) { return d.width; })
	.attr("height", function(d) { return d.height; })
	.style("fill", "#fff")
	.style("stroke", "#222");

You’ll note that I added style fill and stroke attributes to make the grid visible.

When we put it all together, here is what we get:

Note: If you are viewing this on your phone, you might want to switch over to a tablet or desktop. I haven’t optimized this example for mobile because that will needlessly complicate it.

Cool, huh? Go ahead and inspect the element and marvel at your find handiwork. Then change the fill, stroke, width, and height attributes and see how it changes.

Adding Click Functions

Let’s have some fun and add click events to the individual cells. I want to have cells turn blue on the first click, orange on the second, grey on the third, and white again on the fourth. Since D3 is data-driven, we’ll need to add some click data to the arrays and then add functions to change it and set colors based on the number of clicks.

//add this to the gridData function
var click: 0;

//add this to the cell/column iteration data.push
click: click

//add this to var column = row.selectAll(".square")
.on('click', function(d) {
       d.click ++;
       if ((d.click)%4 == 0 ) { d3.select(this).style("fill","#fff"); }
	   if ((d.click)%4 == 1 ) { d3.select(this).style("fill","#2C93E8"); }
	   if ((d.click)%4 == 2 ) { d3.select(this).style("fill","#F56C4E"); }
	   if ((d.click)%4 == 3 ) { d3.select(this).style("fill","#838690"); }
    })

Let’s break down that on('click') function:

  • When you click on a cell, it increases the click variable (originally set at 0) by 1.
  • The if statements set the color based on how many times it has been clicked mod 4. This satisfies the UI of only having four states: white, blue, orange, and grey. If you go to your console and call up the data for a certain cell, you’ll see that the full number of clicks is available.

Here it is. Click away!

Randomized click counts

What happens when we randomize click counts when we create the data array?

//add this to the gridData function
var click: 0;

//add this to the cell/column for loop, just above the data.push line
click = Math.round(Math.random() * 100);

//add this to var column = row.selectAll(".square")
.style("fill", function(d) {
		if ((d.click)%4 == 0 ) { return "#fff"; }
		if ((d.click)%4 == 1 ) { return "#2C93E8"; }
		if ((d.click)%4 == 2 ) { return "#F56C4E"; }
		if ((d.click)%4 == 3 ) { return "#838690"; }
	})

Note that Math.random() returns a number between 0 and 1, inclusive. Multiple that by 100 if you want a number between 1 and 100.

It changes when you refresh!

Mouseovers: Even more fun with a bigger grid

What happens when we change the click event to a mouseover event and make a bigger grid? It becomes a lot more fun. I’ll leave the implementation as an exercise to the reader. If you’ve been following along and writing this yourself instead of copying and pasting, you probably already know which variables and events to change:

Find this post useful?

Buy me a coffeeBuy me a coffee