Skip to content

Instantly share code, notes, and snippets.

@cagrimmett
Last active July 31, 2023 18:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save cagrimmett/07f8c8daea00946b9e704e3efcbd5739 to your computer and use it in GitHub Desktop.
Save cagrimmett/07f8c8daea00946b9e704e3efcbd5739 to your computer and use it in GitHub Desktop.
Let's Make a Grid with D3.js
license: gpl-3.0
height: 510

This tutorial is a way to apply the D3.js basics of data joins, click events, and selections. Along the we'll learn about building arrays.

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. 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.

You are done! Your handiwork should now match the example at the top of this block.

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;
var click = 0;
// 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,
click: click
})
// 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;
}
var gridData = gridData();
// I like to log the data to the console for quick debugging
console.log(gridData);
var grid = d3.select("#grid")
.append("svg")
.attr("width","510px")
.attr("height","510px");
var row = grid.selectAll(".row")
.data(gridData)
.enter().append("g")
.attr("class", "row");
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")
.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"); }
});
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<div id="grid"></div>
<script src="grid.js" type="text/javascript"></script>
</body>
</html>
@tonyxqing
Copy link

hey cool tutorial :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment