d3 Shapes and Layouts — What’s It All About?
By: Sabahat Iqbal
Tags: Data Visualization, D3js

This is a portion of the dataset:

First 5 rows of dataset; shows the number of individuals per age category in an imaginary town.

First, the data has to be read in to the file. I like to save all datasets into one variable, promises (although in this case only one dataset is being loaded in):

const promises = [d3.csv(“data/population-by-age.csv”)]; Promise.all(promises).then(function (data) { const dataToUse = data[0]; //more code

.

.

.

}

Now, the data, dataToUse , can be inputted to d3-shape library, specifically pie() , to put out some of the basic elements of a pie chart. This is what the data looks like before:

Before d3.pie()

And this is what it should look like after, the original data with extra fields:

After d3.pie()

Original field:

data — the input datum; the corresponding element in the input data array.

Additional fields:

value — the numeric value of the arc.

index — the zero-based sorted index of the arc.

startAngle — the start angle of the arc.

endAngle — the end angle of the arc.

padAngle — the pad angle of the arc.

In it’s most simple form, pie() needs to know the dataset to use. In this case, that is dataToUse . But pie() expects the dataset to be an array of numbers and, unfortunately, dataToUse is an array of objects. So, in this case, pie() needs additional information, i.e. within dataToUse , where is the actual number and how can it be accessed? pie() allows users to specify this by using the value method of pie() :

const myArcs = d3.pie().value(function (d) {

return d.value;

})(dataToUse);

If dataToUse had been an array of numbers, there would have been no need to use the value() method and the code would have looked like this:

const myArcs = d3.pie()(dataToUse);

The next step is to set other important values, innerRadius and outerRadius , of the pie chart using another part of the d3-shape API, d3.arc() :

const allMyArcs = d3

.arc()

.innerRadius(100)

.outerRadius(240);

Setting the innerRadius to a value greater than 0 makes it a donut instead of a pie chart.

Now, the two functions, pie() and arc(), are combined to build a whole donut:

//Setting up the space on which the data viz will be "printed":

const svg = d3

.select("#my_dataviz")

.append("svg")

.attr("width", width)

.attr("height", height)

.append("g")

.attr("transform", "translate(" + 260 + "," + 240 + ")"); //Putting the components together that make up the donut:

svg

.append("g")

.attr("stroke", "white")

.selectAll("path") //We're expecting a bunch of paths to be created; see more below about paths.

.data(myArcs) //The paths will use the data from myArcs to set the start angle and end angle of each slice.

.enter()

.append("path")

.attr("fill", (d) => color(d.data.name))

.attr("d", allMyArcs); //The paths will use the data from allMyArcs to set the same innerRadius and outerRadius for each slice.

The d3-path takes the parameters set by arc() and pie() to create the commands that define the shape that is required. If arc() and pie() did not exist, drawing a circle would require coding each and every line in each and every slice in the pie chart with moveto , lineto etc etc. Tedious, to say the least.

Without labels, this pie chart is dead to me.

Next, a group is created to hold all the labels ( name , value ) that will be displayed over each slice:

svg

.append("g")

.attr("font-family", "sans-serif")

.attr("font-size", 12)

.attr("text-anchor", "middle")

.selectAll("text") //We're expecting a bunch of texts to be created

.data(myArcs) //One text per item in myArcs dataset

.enter()

.append("text")

.attr("transform", (d) => `translate(${arcLabel().centroid(d)})`) //See full explanation below

.call((text) => //The function inside call() runs on the entire selection, i.e. all the "texts" created above.

text

.append("tspan")

.attr("y", "-0.4em")

.attr("font-weight", "bold")

.text((d) => d.data.name)

)

.call((text) => //The function inside call() runs on the entire selection, i.e. all the "texts" created above.

text

.filter((d) => d.endAngle - d.startAngle > 0.25)

.append("tspan")

.attr("x", 0)

.attr("y", "0.7em")

.attr("fill-opacity", 0.7)

.text((d) => d.value.toLocaleString())

);

Each slice gets a label made up by the name and value fields of the data key in the myArcs dataset:

What is going on with the transform attribute?

The transform attribute is used to shift items, in this case labels for each slice of the donut, around the page laterally, vertically, or rotationally (is that a word?) or a combination of all three.

The labels have to be placed carefully so they lay over exactly the right slice. This is done by creating another d3.arc() function with innerRadius equal to the outerRadius (so nothing actually shows up on the page but the circle is “there”).

const arcLabel = function () {

return d3.arc().innerRadius(200).outerRadius(200);

};

This function runs — with the centroid method chained to it — as a callback to the transform attribute. It returns the exact position of where to place the label. The concept of centroid is best capture by the images in the API documentation:

Black dots mark the spot returned by centroid().

That’s it for pie charts for now. I will be uploading a new one about how to make it interactive as that is slightly (read: very) different from making a scatter-plot or bar chart interactive.

Please let me know if you have any trouble with this tutorial or find a typo.