Multiple time series with selectors

Multiple Time Series in D3

Eric Chi Boxer
5 min readOct 27, 2018

--

The D3 library offers such powerful tools for binding data to DOM elements that I found it imposing to create a simple time series with it. Having finally done so, and learned quite a few things along the way, this is my attempt to share that experience and to reinforce what I learned to myself.

Here is the finished product and here is the Github repo for the work.

The work can be broken down into several steps:

HTML Layout

I started out with the idea of comparing three time series, one each for the cancer survival rates of males, females and the total population. To accommodate this format we need a canvas and three selectors to show or hide each time series. The canvas will be a div with id="graph" and the selectors will be checkboxes with id="total", "male" and "female" (these ids will be important later on).

Div

<div id="graph"></div>

Checkboxes

<input class="selector" type="checkbox" value="Total" id="total" checked="true"/><label for="Total">Total</label>                        <input class="selector" type="checkbox" value="Female" id="female" checked="true"/><label for="Total">Female</label>                        <input class="selector" type="checkbox" value="Male" id="male" checked="true"/><label for="Total">Male</label>

Script Part 1 Global

We declare a few things up front: our margins (in accord with Mike Bostock’s conventions here) and the canvas dimensions. Also a few things that I will mention but explain after we have actually loaded in the data: a d3.format and functions to give our checkboxes the power to affect the visualization.

Margins

const margin = {top:50, right:50, bottom:50, left:50};

Dimensions

const width = window.innerWidth - margin.left - margin.right;                         const height = window.innerHeight - margin.top - margin.bottom;

Format

const fsr = d3.format(".2f");

Checkbox Powers

We can take a look at these later, when they will make more sense.

Script Part 2 Data

We will start off with the cancerTotal.csv which contains the survival rates for the total population measure. Start off by loading the desired features SurvRate and Year.

d3.csv("resources/cancerTotal.csv", function (d) {
return {surv: d.SurvRate,
year: d.Year
};
})

Next, we set scales for the x and y axes for year and survival rate, respectively.

var x = d3.scaleLinear()
.domain(d3.extent(data, d => d.year))
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, 1])
.range([height, 0]);

In the domain of x we are using the d3.extent function to get the range of years without having to specify two separate min and max functions for the beginning and ending years.

In the range of y we have inverted height and 0 to make it easier to orient our lines later.

Now we define the path function for our time series:

var line = d3.line()
.x(d => x(d.year))
.y(d => y(d.surv));

line takes two methods x and y for the x- and y-coordinates of our path. Here we have defined anonymous functions => to pass the scaled year to x and the scaled survival rate to y.

Now we define the container using our div from before:

var container = d3.select("#graph")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("class", "container")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

We select the the id graph in which to create our canvas and then append g with the appropriate margin transform.

The next chunks are for the title, axes and y-axis label. We are just taking advantage of the d3 functions and our judicious margins to make these.

//Title
container.append("text")
.attr("x", width * .5)
.attr("y", margin.top * .1)
.text("Cancer Survival Rate (1977-2013)")
.attr("text-anchor", "middle")
.style("font-family", "sans-serif")
.style("font-size", margin.top * .4);
//X axis
container.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.format("d")));
//Y axis
container.append("g")
.attr("class", "axis")
.call(d3.axisLeft(y));
//Y label
container.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -height / 2)
.attr("y", -margin.right * .7)
.style("text-anchor", "middle")
.text("Survival Rate")
.attr("font-family", "sans-serif")
.attr("font-size", margin.right / 4);

Here’s the fun part, path drawing! We create append a g inside container. Then bind the data to a path with this attr("d", line(data))

const path = container.append("g")
.selectAll(".total")
.data(data)
.enter()
.append("path")
.attr("class", "total")
.attr("id", "purpleLine")
.attr("fill", "none")
.attr("stroke", "purple")
.attr("mix-blend-mode", "multiply")
.attr("stroke-width", 1)
.attr("stroke-linejoin", "round")
.attr("d", line(data));

For the sake of transparency I added circles for the data points we have in the csv.

Then it’s basically just rinse and repeat for the female and male survival rate data. We can use the same container and we vary colors, but otherwise the goal is to plot all three lines and the means are very similar for each.

Checkboxes Revisited

Let’s return to the checkboxes, in particular to this:

d3.selectAll(".selector")
.on("click", function (d) {
this.checked ? showCircle(this.id) : hideCircle(this.id);
this.checked ? showPath(this.id) : hidePath(this.id); });

We make a selection of all our inputs, which we have classed as selector when we created them. The event listener "click" is for checking or unchecking and since we want to either show or hide data based on the context we need to refine our choice of action. The line

this.checked ? showCircle(this.id) : hideCircle(this.id);

is a conditional which can be read as “If a checkbox is checked when it is clicked, then pass the id of this checkbox to the function showCircle otherwise pass the id of this checkbox to the function hideCircle. This does what we want as long as we can write the show and hide functions to do what we want using the id of a checkbox. Here’s where the checkbox id and the path class come into play. We used the “total”, “female” and “male” as ids for the checkboxes and “total”, “female” and “male” as classes for the paths. All that is left is to select the correct paths:

function showPath (source) {
d3.selectAll("path").filter("."+source).transition().attr('stroke-width', 1).duration(1000);
}
function hidePath (source) {
d3.selectAll("path").filter("."+source).transition().attr('stroke-width', 0).duration(1000);
}

Here what we have done is first select paths, then filter to paths with the class equal to whatever was passed to showPath or hidePath, which in our case is the class of the checkbox. Checking a box will trigger a transition in which the path takes one second to appear. Unchecking a box will trigger a transition in which the path takes one second to shrink to a width of zero.

That’s all! Looking back there are several redundancies in this code, artifacts of having tried and failed to get things working a certain way and having to rethink how to draw paths or get working checkboxes. I hope this was helpful in some way, that it gave you some idea of how to do (or how not to do) a time series in d3.

--

--