This chapter covers
Throughout this book, we’ve dealt with D3 components and layouts. In this chapter we’ll write them. After you’ve created your own layout and your own component, you’ll better understand the structure and function of layouts. You’ll be able to use that layout, and other layouts that you create in the same fashion, in the charts and applications that you build with D3 later on.
In this chapter we’ll create a custom layout that places a dataset on a grid. For most of the chapter, we’ll use our people analytics dataset, but the advantage of a layout is that the dataset doesn’t matter. The purpose of this chapter isn’t to create a grid, but rather to help you understand how layouts work. We’ll create a grid layout because it’s simple and allows us to focus on layout structure rather than on the particulars of any data visualization layout. We’ll follow that up by extending the layout so it can have a set size that we can change. You’ll also see how the layout annotates the dataset we send so that individual datapoints can be drawn as circles or rectangles. A grid isn’t the sexiest or most useful layout, but it can teach you the basics of layouts. After that, we’ll build a legend component that tells users the meaning of the color of our elements. We’ll do this by basing the graphical components of the legend on the scale we’ve used to color our chart elements.
Recall from chapter 5 that a layout is a function that modifies a dataset for graphical representation. Here, we’ll build that function. Later, we’ll give it the capacity to modify the settings of the layout in the same manner that built-in D3 layouts operate. This layout will allow us to place discrete datapoints on a grid, allowing for easy comparison. You could use this to show a single circle or rectangle, as we have, or you could use these grids to hold individual charts to generate small multiples charts, as some D3 users have since the first edition of this book was released.
You’ll see this in more detail later, but first we need to create a function that processes our data. After we create this function, we’ll use it to implement the calls that a layout needs. In the following listing, you can see the function and a test where we instantiate it and pass it data.
d3.gridLayout = () => {
function processGrid(data) {
console.log(data)
}
return processGrid
}
var grid = d3.gridLayout()
grid([1,2,3,4,5]) 1
That’s not an exciting layout, but it works. We don’t need to name our layout d3.-layoutX or any other particular name, but using a thoughtful name will make it more readable in the future (and you don’t want to be heckled in a book on the subject in coming years, where you’re asked how your treemap is neither a tree nor a map).
Before we start working on the functions that will create our grid, we have to define what this layout does. We know we want to put the data on a grid, but what else do we want? Here’s a simple spec:
First, we need to initialize all the variables that this grid needs to access to make it happen. We also need to define getter and setter functions to let the user access those variables, because we want to keep them scoped to the d3.gridLayout function. The first thing we can do is update the processGrid function to look like it does in listing 10.2. It takes an array of objects and updates them with x and y data based on grid positions. We derive the size of the grid from the number of data objects sent to processGrid. It turns out this isn’t a difficult mathematical problem. We take the square root of the number of datapoints and round it up to the nearest whole number to get the right number of rows and columns for our grid. This makes sense when you think about how a grid is a set of rows and columns that allows you to place a cell on one of those rows and columns for each datapoint. The number of rows times columns needs to be at least the number of cells (the number of datapoints). If we decide to have the same number of rows as columns, then it’s that number squared.
function processGrid(data) {
var rows = Math.ceil(Math.sqrt(data.length)); 1
var columns = rows;
var cell = 0; 2
for (var rowNumber = 0; rowNumber < rows; rowNumber++) {
for (var cellNumber = 0; cellNumber < columns; cellNumber ++) { 3
if (data[cell]) { cellNumber 4
data[cell].y = rowNumber 5
cell++ 6
}
else {
break
}
}
}
return data
}
To test our nascent grid layout, we can load our people analytics team using nodelist .csv from chapter 7 and then pass that data to the grid. The grid function displays the graphical elements onscreen based on their computed grid position. In the following listing, you can see how we’d pass data from nodelist.csv to our grid layout and size each person by their salary.
d3.csv("nodelist.csv", makeAGrid)
function makeAGrid(data) {
var scale =
d3.scaleLinear().domain([0,5]).range([100,400]); 1
var salaryScale = d3.scaleLinear().domain([0,300000])
.range([1,30]).clamp(true)
var grid = d3.gridLayout();
var griddedData = grid(data);
d3.select("svg").selectAll("circle")
.data(griddedData)
.enter()
.append("circle")
.attr("cx", d => scale(d.x)) 2
.attr("cy", d => scale(d.y))
.attr("r", d => salaryScale(d.salary))
.style("fill", "#93C464");
}
The results in figure 10.1 show how the grid function has correctly appended x and y coordinates to draw the employees as circles on a grid.

The benefit of building this as a layout is that if we add more data to it, it automatically adjusts and allows us to use transitions to animate that adjustment. To do that, we need more data. Listing 10.4 includes a few lines to create a raft of new employees. We also use the .concat() function of an array in native JavaScript that, when given the state shown in figure 10.1, should produce the results in figure 10.2.

var newEmployees = d3.range(14).map(d => {
var newPerson = {id: "New Person " + d, salary: d * 20000} 1
return newPerson
})
var doubledArray = data.concat(newEmployees); 2
var newGriddedData = grid(doubledArray);
d3.select("svg").selectAll("circle")
.data(newGriddedData)
.enter()
.append("circle") 3
.attr("cx", 0)
.attr("cy", 0)
.attr("r", d => salaryScale(d.salary))
.style("fill", "#41A368");
d3.select("svg").selectAll("circle")
.transition() 4
.duration(1000)
.attr("cx", d => scale(d.x))
.attr("cy", d => scale(d.y))
The results in figure 10.2 show snapshots of the animation from the old position to the new position of the circles.
Calculating a scale based on what you know to be the grid size results in an inefficient piece of code. That wouldn’t be useful if someone put in a different dataset. Instead, when designing layouts, you’ll want to provide functionality so that the layout size can be declared, and then any adjustments necessary happen within the code of the layout that processes data. To do this, we need to add a scoped size variable and then add a function to our processGrid function to allow the user to change that size variable. Sending a variable sets the value, and sending no variable returns the value. We achieve this by checking for the presence of arguments using the arguments object in native JavaScript. The updated function is shown in the following listing.
d3.gridLayout = function() {
var gridSize = [0,10]; 1
var gridXScale = d3.scaleLinear(); 2
var gridYScale = d3.scaleLinear();
function processGrid(data) {
var rows = Math.ceil(Math.sqrt(data.length));
var columns = rows;
gridXScale.domain([1,columns]).range([0,gridSize[0]]) 3
gridYScale.domain([1,rows]).range([0,gridSize[1]])
var cell = 0
for (var rowNum = 1; rowNum <= rows; rowNum++) {
for (var cellNum = 1; cellNum <= columns; cellNum++) {
if (data[cell]) {
data[cell].x = gridXScale(cellNum) 4
data[cell].y = gridYScale(rowNum)
cell++
}
else {
break
}
}
}
return data;
}
processGrid.size = (newSize) => { 5
if (!arguments.length) return gridSize
gridSize = newSize
return this
}
return processGrid
}
You can see the updated grid layout in action by slightly changing our code for calling the layout, as shown in the following listing. We set the size, and when we create our circles, we use the x and y values directly instead of using scaled values.
var grid = d3.gridLayout();
grid.size([400,400]); 1
var griddedData = grid(data);
d3.select("svg")
.append("g")
.attr("transform", "translate(50,50)")
.selectAll("circle").data(griddedData)
.enter()
.append("circle")
.attr("cx", d => d.x) 2
.attr("cy", d => d.y)
.attr("r", d => salaryScale(d.salary))
var newEmployees = [];
for (var x = 0;x < 14;x++) {
var newPerson = {id: "New Person " + x, salary: x * 20000};
newEmployees.push(newPerson);
}
var doubledArray = data.concat(newEmployees)
var newGriddedData = grid(doubledArray)
d3.select("g").selectAll("circle").data(newGriddedData)
.enter()
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", d => salaryScale(d.salary))
.style("fill", "#41A368")
d3.select("g").selectAll("circle")
.transition()
.duration(1000)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.on("end", resizeGrid1) 3
This code refers to a resizeGrid1() function, shown in the following listing, that’s chained to a resizeGrid2() function. These functions use the ability to update the size setting on our layout to update the graphical display of the elements created by the layout.
function resizeGrid1() {
grid.size([200,400]); 1
grid(doubledArray);
d3.select("g").selectAll("circle")
.transition()
.duration(1000)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.on("end", resizeGrid2)
};
function resizeGrid2() {
grid.size([400,200]) 2
grid(doubledArray)
d3.select("g").selectAll("circle")
.transition()
.duration(1000)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
}
This creates a grid that fits our defined space perfectly, as shown in figure 10.3, and with no need to create a scale to place the elements.

Figure 10.4 shows a pair of animations where the grid changes in size as we adjust the size setting. The grid changes to fit a smaller or an elongated area. This is done using the transition’s end event. It calls a new function that uses our original grid layout but updates its size and reapplies it to our dataset.

Before we move on, it’s important that we extend our layout a bit more so that you can better understand how layouts work. In D3 a layout isn’t meant to create something as specific as a grid full of circles. Rather, it’s supposed to annotate a dataset so you can represent it using different graphical methods.
Let’s say we want our layout to also handle squares, which would be a desired feature when dealing with grids. To handle squares, or more specifically rectangles (because we want them to stretch out if someone uses our layout and sets the height and width to different values), we need the capacity to calculate height and width values. That’s easy to add to our existing layout function, as shown in the following listing.
var gridCellWidth = gridSize[0] / columns;
var gridCellHeight = gridSize[1] / rows;
//other code
for (var i = 1; i <= rows; i++) {
for (var j = 1; j <= columns; j++) {
if (data[cell]) {
data[cell].x = gridXScale(j);
data[cell].y = gridYScale(i);
data[cell].height = gridCellHeight; 1
data[cell].width = gridCellWidth; 1
cell++;
}
else {
break;
}
}
}
With that in place, we can call our layout and append <rect> elements instead of circle elements. We can update our code, as in the following listing, to offset the x and y attributes (because <rect> elements are drawn from the top left and not from the center like <circle> elements) and also apply the width and height values that our layout computes.
d3.select("svg")
.append("g")
.attr("transform", "translate(50,50)")
.selectAll("circle").data(griddedData)
.enter()
.append("rect")
.attr("x", d => d.x - (d.width / 2)) 1
.attr("y", d => d.y - (d.height / 2)) 1
.attr("width", d => d.width) 1
.attr("height", d => d.height) 1
.style("fill", "#93C464")
...
d3.select("g").selectAll("rect").data(newGriddedData)
.enter()
.append("rect")
.style("fill", "#41A368")
d3.select("g").selectAll("rect")
.transition()
.duration(1000)
.attr("x", d => d.x - (d.width / 2)) 2
.attr("y", d => d.y - (d.height / 2)) 2
.attr("width", d => d.width) 2
.attr("height", d => d.height) 2
.on("end", resizeGrid1); 3
function resizeGrid1() { 4
grid.size([200,400]);
grid(doubledArray);
d3.select("g").selectAll("rect")
.transition()
.duration(1000)
.attr("x", d => d.x - (d.width / 2))
.attr("y", d => d.y - (d.height / 2))
.attr("width", d => d.width)
.attr("height", d => d.height)
.on("end", resizeGrid2);
};
function resizeGrid2() { 4
grid.size([400,200]);
grid(doubledArray);
d3.select("g").selectAll("rect")
.transition()
.duration(1000)
.attr("x", d => d.x - (d.width / 2))
.attr("y", d => d.y - (d.height / 2))
.attr("width", d => d.width)
.attr("height", d => d.height)
};
If we update the rest of our code accordingly, the result is the same animated transition of our layout between different sizes, but now with rectangles that grow and distort based on those sizes, as shown in figure 10.5.

This is a simple example of a layout and doesn’t do nearly as much as the kinds of layouts we’ve used throughout this book, but even a simple layout like this provides reusable, animatable content. Now we’ll look at another reusable pattern in D3—the component—which creates graphical elements automatically.
You’ve seen components in action, particularly the axis component. You can also think of the brush as a component, because it creates graphical elements. But it tends to be described as a “control” because it also loads with built-in interactivity.
The component that we’ll build is a simple legend. Legends are a necessity when working with data visualization, and they all share some things in common. First, we’ll need a more interesting dataset to consider, though we’ll continue to use our grid layout. The legend component that we’ll create will consist eventually of labeled rectangles, each with a color corresponding to the color assigned to our datapoints by a D3 scale. This way our users can tell at a glance which colors correspond to which values in our data visualization.
Instead of the nodelist.csv data, we’ll use world.geojson, except we’ll use the features as datapoints on our custom grid layout from section 10.1 without putting them on a map. Listing 10.10 shows the corresponding code, which produces figure 10.6. You may find it strange to load geodata and represent it not as geographic shapes but in an entirely different way. Presenting data in an untraditional manner can often be a useful technique to draw a user’s attention to the patterns in that data.

d3.json("world.geojson ", data => {
makeAGrid(data);
})
function makeAGrid(data) {
var grid = d3.gridLayout();
grid.size([300,300]);
var griddedData = grid(data.features);
griddedData.forEach(country => {
country.size = d3.geoArea(country); 1
});
d3.select("svg")
.append("g")
.attr("transform", "translate(50,50)")
.selectAll("circle")
.data(griddedData)
.enter()
.append("circle") 2
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 10)
.style("fill", "#75739F")
.style("stroke", "#4F442B")
.style("stroke-width", "1px");
};
We’ll focus on only one attribute of our data: the size of each country. We’ll color the circles according to that size using a quantize scale that puts each country into one of several discrete categories. In our case, we’ll use the colorbrewer.Reds[7] (remember, this means you’ll need to include a link to the colorbrewer.js file) array of light-to-dark reds as our bins. The quantize scale will split the countries into seven different groups. In listing 10.11, you can see how to set that up, and figure 10.7 shows the result of our new color scale.

var griddedData = d3.selectAll("circle").data(); 1
var sizeExtent = d3.extent(griddedData, d => d.size)
var countryColor = d3.scaleQuantize()
.domain(sizeExtent).range(colorbrewer.Reds[7])
d3.selectAll("circle").style("fill", d => countryColor(d.size))
For a more complete data visualization, we’d want to add labels for the countries or other elements to identify the continent or region of the country. But we’ll focus on explaining what the color indicates. We don’t want to get bogged down with other details from the data that could be explained, for example, using modal windows, as we did for our World Cup example in chapter 4, or using other labeling methods discussed throughout this book. For our legend to be useful, it needs to account for the different categories of coloration and indicate which color is associated with which band of values. But before we get to that, let’s build a component that creates graphical elements when we call it. Remember that the d3.select(#something).call (someFunction) function of a selection is the equivalent of someFunction(d3.select (#something)). With that in mind, we’ll create a function that expects a selection and operates on it, as in the following listing.
d3.simpleLegend = () => {
function legend(gSelection) { 1
var testData = [1,2,3,4,5];
gSelection.selectAll("rect") 2
.data(testData)
.enter()
.append("rect")
.attr("height", 20)
.attr("width", 20)
.attr("x", (d,i) => i *25)
.style("fill", "red")
return this;
}
return legend;
};
We can then append a <g> element to our chart and call this component, with the results shown in figure 10.8:

var newLegend = d3.simpleLegend();
d3.select("svg").append("g")
.attr("id","legend")
.attr("transform", "translate(50,400)")
.call(newLegend);
And now that we have the structure of our component, we can add functionality to it, such as allowing the user to define a custom size, as we did with our grid layout. We also need to think about where this legend is going to get its data. Following the pattern of the axis component, it would make the most sense for the legend to refer directly to the scale we’re using and derive, from that scale, the color and values associated with the color of each band in the scale.
To do that, we have to write a new function for our legend that takes a scale and derives the necessary range bands to be useful. The scale we send it will be the same countryColor scale that we use to color our grid circles. Because this is a quantize scale, we’ll make our legend component hardcoded to handle only quantize scales. If we wanted to make this a more robust component, we’d need to make it identify and handle the various scales that D3 uses.
The way all scales have an invert function, they also have the ability to tell you what domain values are mapped to what range values. First, we need to know the range of values of our quantize scale as they appear to the scale. We can easily get that range by using scaleQuantize.range():
countryColor.range() 1
We can pass those values to scaleQuantize.invertExtent to get the numerical domain mapped to each color value:
countryColor.invertExtent("#fee5d9") 1
Armed with these two functions, all we need to do now is give our legend component the capacity to have a scale assigned to it and then update the legend function itself to derive from that scale the dataset necessary for our legend. Listing 10.13 shows both the new d3.simpleLegend.scale() function that uses a quantize scale to create the necessary dataset, and the updated legend() function that uses that data to draw a more meaningful set of <rect> elements.
d3.simpleLegend = function() {
var data = [];
var size = [300,20]; 1
var xScale = d3.scaleLinear(); 2
var scale; 3
function legend(gSelection) {
createLegendData(scale); 4
var xMin = d3.min(data, d => d.domain[0]) 5
var xMax = d3.max(data, d => d.domain[1])
xScale.domain([xMin,xMax]).range([0,size[0]]) 6
gSelection.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("height", size[1]) 7
.attr("width", d => xScale(d.domain[1]) - xScale(d.domain[0]))
.attr("x", d => xScale(d.domain[0]))
.style("fill", d => d.color);
return this;
};
function createLegendData(incScale) { 8
var rangeArray = incScale.range();
data = [];
for (var x in rangeArray) {
var colorValue = rangeArray[x];
var domainValues = incScale.invertExtent(colorValue);
data.push({color: colorValue, domain: domainValues})
}
};
legend.scale = function(newScale) { 9
if (!newScale) return scale;
scale = newScale;
return this;
};
return legend;
};
We call this updated legend and set it up:
var newLegend = d3.simpleLegend().scale(countryColor);
d3.select("svg").append("g")
.attr("transform","translate(50,400)")
.attr("id", "legend").call(newLegend);
This new legend now creates a rect for each band in our scale and colors it accordingly, as shown in figure 10.9.

If we want to add interactivity, it’s a simple process because we know that each rect in the legend corresponds to a two-piece array of values from our quantize scale showing the value of the bands in that cell. The following listing shows that function and the call to make the legend interactive.
d3.select("#legend").selectAll("rect").on("mouseover", legendOver);
function legendOver(d) {
d3.selectAll("circle")
.style("opacity", p => {
if (p.size >= d.domain[0] && p.size <= d.domain[1]) {
return 1;
} else {
return .25;
}
});
};
Notice that this function isn’t defined inside our legend component. Instead, it’s defined and called after the legend is created, because after it’s created our legend component is a set of SVG elements with data bound to it like any other part of our charts. This interactivity allows us to mouseover the legend and see which circles fall in a particular range of values, as shown in figure 10.10.

Finally, before we can call our legend done, we need to add an indication of what those colored bands mean. We can call an axis component and allow that to label the bands, or we can label the break points by appending text elements for each. In our case, because the numbers provided for d3.geo.area are so small, we’ll also need to rotate and shrink those labels quite a bit for them to fit on the page. To do that, we can add the code in the following listing to our legend function in d3.simpleLegend
gSelection.selectAll("text")
.data(data)
.enter()
.append("g") 1
.attr("transform", d => "translate(" + xScale(d.domain[0]) + ","
+ size[1] + ")")
.append("text")
.attr("transform", "rotate(90)")
.text(d => d.domain[0]);
As shown in figure 10.11, they aren’t the prettiest labels. We could adjust their positioning, font, and style to make them more effective. They also need functions like the grid layout has to define size or other elements of the component.

This is usually the point where I say that the purpose of this chapter is to show you that the structure of components and layouts, and that making the most effective layout or component is a long and involved process that we won’t get into. But this is an ugly legend. The break points are hard to read, and it’s missing pieces that the component needs, such as a title and an explanation of units.
Let’s add those features to the legend and create ways to access them, as shown in the following listing. We’re using d3.format, which allows us to set a number-formatting rule based on the popular Python number-formatting mini-language (found at https://docs.python.org/release/3.1.3/library/string.html#formatspec).
var title = "Legend";
var numberFormat = d3.format(".4n");
var units = "Units"; 1
//other code
legend.title = function(newTitle) {
if (!arguments.length) return title;
title = newTitle;
return this;
}; 2
legend.unitLabel = function(newUnits) {
if (!arguments.length) return units;
units = newUnits;
return this;
};
legend.formatter = function(newFormatter) {
if (!arguments.length) return numberFormat;
numberFormat = newFormatter;
return this;
};
We’ll use these new properties in our updated legend drawing code shown in listing 10.17. This new code draws SVG <line> elements at each breakpoint and foregoes the rotated text in favor of more readable, shortened text labels at each breakpoint. It also adds two new <text> elements, one above the legend that corresponds to the value of the title variable and one at the far right of the legend that corresponds to the units variable.
gSelection.selectAll("line") 1
.data(data)
.enter()
.append("line")
.attr("x1", d => xScale(d.domain[0])) 2
.attr("x2", d => xScale(d.domain[0]))
.attr("y1", 0)
.attr("y2", size[1] + 5)
.style("stroke", "black")
.style("stroke-width", "2px");
gSelection.selectAll("text")
.data(data)
.enter()
.append("g")
.attr("transform",
d => `translate(${(xScale(d.domain[0]))},${(size[1] + 20)})`)
.append("text")
.style("text-anchor", "middle")
.text(d => numberFormat(d.domain[0])); 3
gSelection.append("text")
.attr("transform",
d => `translate(${(xScale(xMin))},${(size[1] - 30)})`)
.text(title); 4
gSelection.append("text")
.attr("transform",
d => `translate(${(xScale(xMax))},${(size[1] + 20)})`)
.text(units); 5
This requires that we set these new values using the code in the following listing before we call the legend.
var newLegend = d3.simpleLegend()
.scale(countryColor)
.title("Country Size")
.formatter(d3.format(".2f"))
.unitLabel("Steradians"); 1
d3.select("svg").append("g").attr("transform", "translate(50,400)")
.attr("id", "legend")
.call(newLegend); 2
And now, as shown in figure 10.12, we have a label that’s eminently more readable, still interactive, and useful in any situation where the data visualization uses a similar scale.

By building components and layouts, you understand better how D3 works, but there’s another reason why they’re so valuable: reusability. You’ve built a chart using a layout and component (no matter how simple) that you wrote yourself. You could use either in tandem with another layout or component, or on its own, with any data visualization charts you use elsewhere.
After you’ve worked with components, layouts, and controls in D3, you may start to wonder if there’s a higher level of abstraction available that could combine layouts and controls in a reusable fashion. That level of abstraction has been referred to as a chart, and the creation of reusable charts has been of great interest to the D3 community.
This has led to the development of several APIs on top of D3, such as NVD3, D4 (for generic charts), and my own d3.carto.map (for web mapping, not surprisingly). It’s also led The Miso Project to develop d3.chart, a framework for reusable charts. If you’re interested in using or developing reusable charts, you may want to check these out:
You may also try your hand at building more responsive components that automatically update when you call them again, like the axis and brushes we dealt with in the last chapter. Or you may try creating controls like d3.brush and behaviors like d3.behavior.drag. Regardless of how extensively you follow this pattern, I recommend that you look for instances when your information visualization can be abstracted into layouts and components and try to create those instead of building another one-off visualization. By doing that, you’ll develop a higher level of skill with D3 and fill your toolbox with your own pieces for later work.
When you’re done building your plugin, you probably want to let other people use it. Mike Bostock wrote an excellent tutorial on how to publish your D3 plugins so that they behave like other D3 plug-ins. You can find the tutorial at https://bost.ocks.org/mike/d3-plugin/.
Making legends in D3 was something I had done multiple times and grew tired of implementing in a custom way over and over. After enough repetition, I decided it would be valuable to create a library to solve the use case.
My main priority was to make it as easy to create a legend as possible, something that I’d want to use. The biggest factor to meet this requirement was to provide full documentation including plenty of examples. Using examples was one of the main avenues I used to learn D3 and wanted to also provide those code snippets for users of d3-legend.
