Lecture 1 - iloughman/D3-Lecture-Project GitHub Wiki
Selections
d3 uses CSS3 to select elements from the DOM. Naturally, once you select an element, you want to use it in some way. d3 provides a variety of operators to set attributes, styles, properties, HTML, and text content on selections. It also provides a way to bind data to elements.
Consider the following code snippet.
<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
<script src="d3.min.js"></script>
</head>
<body>
<h1>An h1 header</h1>
<h2>An h2 header</h1>
<h6>I've never used one of these before</h6>
<script>
var h1 = d3.selectAll('h1');
h1.text("BAM! I've replaced you. There's no turning back.");
h1.style('color','magenta');
</script>
</body>
</html>
The script in the header gives us global access to the d3 object, which is what allows us to select DOM elements and manipulate them. Let's move line by line through the script tag in the html body.
We first select all <h1>
elements and assign them to the variable h1. We can now perform opereations on this <h1>
element, namely setting the text and style.
Data Binding - A Toy Example
One we select elements from the DOM we can bind data to them using selection.data(dataValues)
. Here's an example:
<body>
<script>
var data = [2, 4, 6];
d3.select('body').selectAll('div')
.data(data)
.enter().append('div')
.style("width", function(d){
return d*100+"px"
})
.style("background-color","blue")
</script>
</body>
This renders in the browser as:
But wait, how did we select all <div>
elements when there weren't any to begin with inside the body of our html. This is one of the nuances of d3 selections. More on that to come.
Rendering with an SVG
Ideally, we want to create data visualizations using DOM elements that are versatile. SVG has a number of built in shapes that are flexible and easy to control, which will help enhance our visualizations.
Lets begin by appending an <svg>
element with minimal styling to the body of our html.
var svg = d3.select('body').append('svg')
.style('background','#ccc')
.attr('width', 400)
.attr('height', 150)
Next we select all rectangles within the <svg>
element and bind our data to these elements. Remember, even though no <rect>
elements exist yet, we can still bind data to placeholder <rect>
elements.
var chart = svg.selectAll('rect')
.data(dataArray)
What this returns is a selection of placeholder rectangle elements. Since no rectangle elements existed previously, we must enter and append this selection to the DOM. We do this by chaining off the existing selection.
var chart = svg.selectAll('rect')
.data(dataArray)
.enter().append('rect')
Finally, to get our rectangles to actually appear we need to adjust some of their attributes. We do this using the data bound to each element. That these attribute methods have access to the bound data is one of the most powerful aspects of D3!
chart.attr('width', function(d){return d})
.attr('height', 20)
.attr('y', function(d,i) {return i*25})
.style('fill','yellow')
Finally, this will render in our browser as:
Grouping SVG Elements
All SVG shapes can be transformed using the transform attribute. Often, we group SVG shapes using the <g>
container element. Let's see how this would alter the previous example:
<script>
var width = 400;
var height = 150;
var dataArray = [300, 125, 100, 175, 225];
var svg = d3.select('body').append('svg')
.style('background','#ccc')
.attr('width', width)
.attr('height', height)
// Now bind data to the SVG 'g' element
var chart = svg.selectAll('g')
.data(dataArray)
.enter().append('g')
.attr('transform', function(d,i) {return 'translate(0,'+i*25+')'})
chart.append('rect')
.attr('width', function(d){return d})
.attr('height', 20)
.style('fill','yellow')
chart.append('text')
.attr('x', function(d){ return d - 30})
.attr('y', 16)
.text(function(d){return d})
</script>
Notice that we can now append a <text>
element to each <g>
container, allowing us to label each bar with its corresponding piece of data.
Scales
Almost always, data from the real world will not be scaled appropriately for viewing in the browser. D3 provides functionality to scale data.
Consider how our chart above changes if we are given a different array of data: dataArray = [3, 1.25, 1, 1.75, 2.25]
. At these widths, the rectangles representing each bar will barely be visible. Let's fix this using d3.scale
.
var scaleFunc = d3.scale.linear()
.domain([0,d3.max(dataArray)])
.range([0,width])
d3.scale.linear()
returns a function that will linearly map the domain interval into the range interval. In this example we have provided a domain interval of [0, d3.max(dataArray)]
and a range interval of [0,width]
. The maximum value in our data will map to the width of the SVG, and all values between 0 and the maximum data value will scale accordingly (linearly).
Now we can apply this function to the bound data, which determines the size of each bar.
chart.append('rect')
.attr('width', function(d){return scale(d)})
There are many ways to use the scale function. Read more about them here
Paths
Most SVG elements are formed from an SVG <path>
element. We can manipulate the path element directly, using D3's path data generators. This allows for greater flexibility in our visualizations.
A path can be thought of as a line drawn onto an SVG, where the line is constructed by connecting a sequence of points. The way points are connected can be linear or curved. Read more about that here.
The shape of a <path>
is controlled primarily through its d
attribute.
<path d=" M 10 25
L 10 75
L 60 75
L 10 25"
stroke = "red" stroke-width = "2" fill = "none" />
When defined inside an <svg>
this path will render as
Let's use this example to review the coordinate system of an SVG. The path begins at the coordinate (10,25), which is the upper left corner of the triangle. It then moves linearly to the bottom left corner, which corresponds to the coordinate (10,175). Next, the path moves to the right corner: the coordinate (100,175). Finally, the path moves (again, linearly) to the point (10,25), where it began.
Path Generation
Hard coding a path is impractical. D3 provides a wonderful tool that lets us define paths according to data, the D3 line generator: d3.svg.line()
Let's see an example:
var lineFunction = d3.svg.line()
.x(function(d) {return d.x})
.y(function(d) {return d.y})
.interpolate('linear')
lineFunction
is both a function and an object. We can apply the function to an array of data or we can call methods on it to alter it's behavior. Notice that we call the methods x
and y
in order to specify how the line function will transform data into coordinates. Typically, we will use the line generator when setting a path's d
attribute. Assume we have defined a <g>
element and bound data to it. Then
g.append('path')
.attr('d', function(d) {return lineFunction(d)})
will set the path's d
attribute to the appropriate string corresponding to a linear interpolation of coordinates, which are derived from the bound data.
Before we go through a more detailed example involving the line generator, let's discuss how we can import more complicated data sets into our visualizations.
Loading Data
D3 loads data asynchronously through an XMLHttpRequest or XHR. When loading data in this way, logic that depends on the loaded data must be coded in a callback function. For the example below, we will make use of d3.csv
to load a table of comma-separated values. A full discussion of D3's data loading can be found here.
Creating a Line Plot
Let's go through an example that makes use of everything we've covered so far. We're going to create a line plot representing the change in reservoir capacities over time for several reservoirs in California. Let's assume we have this data in a local file capacities.csv
. We can load it simply by running d3.csv('/capacities.csv', callback)
. The callback function will take two arguments, an error and the loaded file. The loaded file is an array of objects, with each array entry representing a row, and the column names represented as keys on the row object.
Were we to view capacities.csv
in Excel, we would see
A three letter code is used to represent each reservoir, and it's capacities over time appear below it. For simplicity, let's filter our data so that we only deal with one reservoir at a time.
d3.csv('capacities.csv')
.row(function(d) {return {capacity: d.ATN , date. d.date}})
.get(function(err, table){
// Create the plot using table data
})
Rather than passing a callback function as the second argument to d3.csv
we instead chain a row method onto the function, which allows us to filter the loaded data into a smaller object for each row. For simplicity, let's only look at data for Alpine Lake, which has the code APN. After the row method, we call the get method, which contains our callback function, giving us access to the data.
Let's define a few things before we start working inside the callback function.
// SVG
var width = 1200, height = 800;
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height+50)
.append('g')
.attr('transform', 'translate(80,20)')
// Scale Functions
var xScale = d3.time.scale()
.range([0,600])
var yScale = d3.scale.linear()
.range([height, 0])
// Axis Functions
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
// Line Generator
var lineFunction = d3.svg.line()
.x(function(d){return xScale(d.date)})
.y(function(d){return yScale(d.capacity)})
// Date Parse Function
var parseDate = d3.time.format('%Y-%m').parse
Note that we haven't defined the domains for our scale functions. We have to do this after we load the data. Also, consider the order of the array values in the range for the yScale
function. Why is that?
With the data attached, we can draw the appropriate path, which will serve as the line plot. Let's first make some simple adjustments.
d3.csv('capacities.csv')
.row(function(d) {return {capacity: d.APN , date: d.date}})
.get(function(err, table){
table.forEach(function(row){
row.capacity = +row.capacity/8900;
row.date = parseDate(row.date);
})
})
The capacity of Alpine Lake is 8,900 acre-ft, so dividing by this amount gives our data values the units of percent of total capacity. Additionally, applying the parseDate function to each date converts each string value into a date that JavaScript understands.
Next, we set the domain on our scale functions.
xScale.domain(d3.extent(table, function(d) {return d.date}))
yScale.domain([0,d3.max(table, function(d) {return d.capacity})])
Then we add the axes.
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,'+height+')')
.call(xAxis)
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
Finally, we append our data and define the d
attribute of the path element.
svg.append('path')
.datum(table)
.attr('d', function(d) {return lineFunction(d)})
.attr('class', 'line')