~ read.

A multi-line plot in d3.js

In this post, we are going to create a multi-line plot visualizing the relationship between age and height of boys from Oxford, England. The data set is described in Goldstein (1987) and ships with the R package nlme for mixed-effects hierarchical models. We wish to show the increase in height as the boys get older by using a multi-line plot, in which a single line is associated with each boy, allowing us to get a sense of how the two variables are connected and whether there are differences in development between the subjects.

We will be using the d3.js library for visualization, which allows to create interactive visualizations using web standard technology. We start by creating an empty html document as follows:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title> INSERT TITLE </title>
</head>
<body>
INSERT BODY
</body>
</html>

In the head tag, we now load the d3.js library as well as an extension which provides nice tooltip functionality:

<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>

Finally, we include the following CSS statements in order to customize the appearance of the elements of the plot:

<style>		
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}

.axis text {
font: 10px sans-serif;
}

.axis .grid-line{
stroke: black;
shape-rendering: crispEdges;
stroke-opacity: .2;
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}

/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\\25BC";
position: absolute;
text-align: center;
}

/* Style northward tooltips differently */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}

</style>

We now include the following JavaScript code in which we define our data set and pass it to the render function, which we will gradually build up from scratch
in the remainder of this post.

<script type="text/javascript">
function render(records){
INSERT FUNCTION DEFINITION
}

var oxboys = {
"Subject": [ "1", "1", "1", "1", "1", "1", "1", "1", "1", "2", "2", "2", "2", "2", "2", "2", "2", "2", "3", "3", "3", "3", "3", "3", "3", "3", "3", "4", "4", "4", "4", "4", "4", "4", "4", "4", "5", "5", "5", "5", "5", "5", "5", "5", "5", "6", "6", "6", "6", "6", "6", "6", "6", "6", "7", "7", "7", "7", "7", "7", "7", "7", "7", "8", "8", "8", "8", "8", "8", "8", "8", "8", "9", "9", "9", "9", "9", "9", "9", "9", "9", "10", "10", "10", "10", "10", "10", "10", "10", "10", "11", "11", "11", "11", "11", "11", "11", "11", "11", "12", "12", "12", "12", "12", "12", "12", "12", "12", "13", "13", "13", "13", "13", "13", "13", "13", "13", "14", "14", "14", "14", "14", "14", "14", "14", "14", "15", "15", "15", "15", "15", "15", "15", "15", "15", "16", "16", "16", "16", "16", "16", "16", "16", "16", "17", "17", "17", "17", "17", "17", "17", "17", "17", "18", "18", "18", "18", "18", "18", "18", "18", "18", "19", "19", "19", "19", "19", "19", "19", "19", "19", "20", "20", "20", "20", "20", "20", "20", "20", "20", "21", "21", "21", "21", "21", "21", "21", "21", "21", "22", "22", "22", "22", "22", "22", "22", "22", "22", "23", "23", "23", "23", "23", "23", "23", "23", "23", "24", "24", "24", "24", "24", "24", "24", "24", "24", "25", "25", "25", "25", "25", "25", "25", "25", "25", "26", "26", "26", "26", "26", "26", "26", "26", "26" ],
"age": [ -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.4493, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9973, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.4493, -0.1643, 0, 0.2466, 0.5562, 0.7945, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7945, 1.0055, -1, -0.7479, -0.4493, -0.1643, -0.0027, 0.2466, 0.5562, 0.7945, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, 0, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.715, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, 0, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, 0, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7808, 0.9945, -1, -0.7479, -0.463, -0.1643, -0.0027, 0.2466, 0.5562, 0.7781, 1.0055 ],
"height": [ 140.5, 143.4, 144.8, 147.1, 147.7, 150.2, 151.7, 153.3, 155.8, 136.9, 139.1, 140.1, 142.6, 143.2, 144, 145.8, 146.8, 148.3, 150, 152.1, 153.9, 155.8, 156, 156.9, 157.4, 159.1, 160.6, 155.7, 158.7, 160.6, 163.3, 164.4, 167.3, 170.7, 172, 174.8, 145.8, 147.3, 148.7, 149.78, 150.22, 152.5, 154.8, 156.4, 158.7, 142.4, 143.8, 145.2, 146.3, 147.1, 148.1, 148.9, 149.1, 151, 141.3, 142.4, 144.3, 145.2, 146.1, 146.8, 147.9, 150.5, 151.8, 141.7, 143.7, 145.1, 147.9, 148.1, 149.6, 150.99, 154.1, 154.9, 132.7, 134.1, 135.3, 136.6, 137.5, 139.1, 140.9, 143.7, 144.7, 126.2, 128.2, 129, 129.4, 129.59, 130.6, 132.5, 133.4, 134.2, 142.5, 143.8, 145.6, 148.3, 149.4, 151.6, 154.8, 156.9, 159.2, 149.9, 151.7, 153.3, 156.1, 156.7, 157.8, 160.7, 162.7, 163.8, 148.9, 149.8, 151.7, 154.4, 155.5, 156.4, 161.4, 163.9, 164.6, 151.6, 153.2, 155.2, 157.3, 159.1, 160.9, 164.4, 166.9, 168.4, 137.5, 139.3, 140.9, 142.7, 144.2, 145.7, 147.09, 150.2, 152.3, 142.8, 144.9, 145, 146.7, 147.2, 148.9, 150.1, 151, 152.2, 134.9, 137.4, 138.2, 140.2, 143.6, 144.2, 147.9, 150.3, 151.8, 145.5, 146.2, 148.2, 150.3, 152, 152.3, 154.3, 156.2, 156.8, 156.9, 157.9, 160.3, 161.9, 163.8, 165.5, 169.9, 172.4, 174.4, 146.5, 148.4, 149.3, 151.2, 152.1, 152.4, 153.9, 154.9, 155.4, 143.9, 145.1, 147, 149.2, 149.8, 151.5, 153.17, 156.9, 159.6, 147.4, 148.8, 150.1, 152.5, 154.7, 156, 158.4, 161.5, 163.3, 144.5, 146, 147.4, 149.2, 150.8, 152.5, 155, 156.8, 158.8, 147.8, 148.2, 150.2, 151, 152.2, 153.6, 155.8, 159.2, 161.6, 135.5, 136.6, 137.3, 138.2, 139, 139.5, 141, 142.7, 143.9, 132.2, 134.3, 135.1, 136.7, 138.4, 138.9, 141.8, 142.6, 143.1 ],
"Occasion": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9" ] }
render(oxboys)

</script>

In a real application, one would almost always load data from files, and d3 provides convenient methods to load data via XMLHttpRequest's,
which allows to load data living in JSON files, .txt or .csv asynchronously.
For further information, have a look at at the documentation:

https://github.com/mbostock/d3/wiki/Requests

data = [];
            for(var i = 0; i < records.age.length; i++)
                {
                    var obj = {};
                obj.age = records.age[i];
                obj.height = records.height[i];
                obj.Subject = records.Subject[i];
                data.push(obj)
                }
                
            var seriesData = d3.nest()
                            .key(function(d){return d.Subject})
                            .entries(data);

Then,

var height = 600,
                width = 600,
                margins = {top: 10, right: 20, bottom: 50, left: 60},
                offset = 40,
                axisWidth = width - margins.left - margins.right,
                axisHeight = height - margins.top - margins.bottom,
                svg;
                
                svg = d3.select("body")
                    .append("svg")
                        .attr("class","axis")
                        .attr("width",width)
                        .attr("height",height) 
                    .append("g")    
                        .attr("transform", "translate(" + margins.left + "," + margins.top + ")");

Setting up the axes:

 var xScale = d3.scale.linear()
            .domain(d3.extent(records.age))
            .range([0,axisWidth]);
            
 var xAxis = d3.svg.axis()
     .scale(xScale)
     .orient("bottom")
     .ticks(10);

svg.append("g")
  .classed("x-axis",true)
  .attr("transform","translate(0," + (height - margins.top - margins.bottom) +  ")")
  .call(xAxis);

var yScale = d3.scale.linear()
  .domain(d3.extent(records.height))
  .range([axisHeight,0]);

var yAxis = d3.svg.axis()
  .scale(yScale)
  .orient("left")
  .ticks(10);

svg.append("g")
  .classed("y-axis",true)
  .attr("transform","translate(0,0)")
  .call(yAxis);

Adding axis labels:

svg.select(".y-axis")
                .append("text")
                .attr("text-anchor","middle")
                .text("height in cm")
                .attr("transform","rotate(-270,0,"+(-offset)+")")
                .attr("x", (height - margins.top) / 2)
                .style("font-size","12px");
                
svg.select(".x-axis")
                .append("text")
                .attr("text-anchor","middle")
                .text("age (normalized)")
                .style("font-size","12px")
                .attr("transform","translate(" + ((width - margins.left) / 2)  + "," + offset + ")");

Plotting the time series:

	var colorScale = d3.scale.ordinal()
    .range(['#e62e65', '#e62e9c', '#e62ed3', '#c12ee6', '#8a2ee6', '#532ee6', '#2e40e6', '#2e77e6', '#2eaee6', '#2ee6e6', '#2ee6ae', '#2ee677', '#2ee640', '#53e62e', '#8ae62e', '#c1e62e', '#e6d32e', '#e69c2e', '#e6652e', '#e62e2e']);
            
    var line = d3.svg.line()        
             .x(function(d){return xScale(d.age)})        
             .y(function(d){return yScale(d.height)})  
             .interpolate("linear"); 
                                
    var series = svg.selectAll(".series")
              .data(seriesData)
              .enter().append("g")
              .attr("class", "series");

    series.append("path")
        .attr("class", "line")
        .attr("d", function (d) { return line(d.values); })
        .style("stroke", function (d) { return colorScale(d.key); })
        .style("stroke-width", "3px")
        .style("fill", "none")  
        .on('mouseover', tip.show)
          .on('mouseout', tip.hide)

For the tooltips to work, we define them as follows:

   var tip = d3.tip()
       .attr('class', 'd3-tip')
       .offset([0, 180])
       .html(function(d) {
             return "Subject " + d.key + "";
       }						    
     svg.call(tip);