You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.0 KiB

D3 Build

Lesson Objectives

  1. Add link to d3 library
  2. Add an <svg> tag and size it with D3
  3. Create some fake data for our app
  4. Add SVG circles and style them
  5. Create a linear scale
  6. Attach data to visual elements
  7. Use data attached to a visual element to affect its appearance
  8. Create a time scale
  9. Parse and format times
  10. Set dynamic domains
  11. Dynamically generate svg elements
  12. Create axes

First thing we want to do is create basic index.html file:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
    </body>
</html>

Now add a link to D3 at the bottom of your <body> tag in index.html:

<body>    
    <script src="https://d3js.org/d3.v4.min.js"></script>
</body>

Now create app.js, which will store all of our code:

console.log('this works');

and link to it in index.html at the bottom of the <body> tag:

<body>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="app.js" charset="utf-8"></script>
</body>

Add an <svg> tag and size it with D3

At the top of the <body> tag in index.html, add an <svg> tag:

<body>
    <svg></svg>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="app.js" charset="utf-8"></script>
</body>

In app.js create variables to hold the width and height of the <svg> tag:

var WIDTH = 800;
var HEIGHT = 600;

Next, we can use d3.select() to select a single element, in this case, the <svg> element:

var WIDTH = 800;
var HEIGHT = 600;

d3.select('svg');

The return value of this is a d3 version of the element (just like jQuery), so we "chain" commands onto this. Let's add some styling to adjust the height/width of the element:

d3.select('svg')
    .style('width', WIDTH)
    .style('height', HEIGHT);

Create some fake data for our app

In app.js let's create an array of "run" objects (NOTE I'm storing the date as a string on purpose. Also, it's important that this be an array of objects, in order to work with D3):

var WIDTH = 800;
var HEIGHT = 600;

var runs = [
    {
        id: 1,
        date: 'October 1, 2017 at 4:00PM',
        distance: 5.2
    },
    {
        id: 2,
        date: 'October 2, 2017 at 5:00PM',
        distance: 7.0725
    },
    {
        id: 3,
        date: 'October 3, 2017 at 6:00PM',
        distance: 8.7
    }
];

Add SVG circles and style them

Add three circles to your <svg> element (each one will represent a run):

<svg>
    <circle/>
    <circle/>
    <circle/>
</svg>

Create app.css with some styling for the circles and our svg element:

circle {
    r:5;
    fill: black;
}
svg {
    border: 1px solid black;
}

and link to it in index.html

<head>
    <meta charset="utf-8">
    <title></title>
    <link rel="stylesheet" href="app.css">
</head>

Create a linear scale

  • Let's position the circles vertically, based on the distance run
  • One of the most important things that D3 does is provide the ability to map points in the "domain" of data to points in the visual "range" using what's called a scale.
  • There are lots of different kinds of scales, but for now we're just going to use a linear scale which will map numeric data values to numeric visual values.

In app.js:

d3.select('svg')
    .style('width', WIDTH)
    .style('height', HEIGHT);

var yScale = d3.scaleLinear(); //create the scale
yScale.range([HEIGHT, 0]); //set the visual range (e.g. 600 to 0)
yScale.domain([0, 10]); //set the data domain (e.g. 0 to 10)

console.log(yScale(5)); //get a visual point from a data value
console.log(yScale.invert(450)); //get a data values from a visual point
  • Here we're saying that a data point of 0 to map to a visual height value of 600
  • This is because the lower the distance run (data value), the more we want to move the visual point down the Y axis
    • remember that the Y axis starts at 0 at the top and increases in value

Attach data to visual elements

We can attach each of our "run" objects to one of our circles, so that each circle can access that data:

yScale.range([HEIGHT, 0]);
yScale.domain([0, 10]);

d3.selectAll('circle').data(runs); //selectAll is like select, but selects all elements that match the query string

Use data attached to a visual element to affect its appearance

When setting a value for an element's style, class, id or any other attribute, we can pass that method a callback instead of a static value.

d3.selectAll('circle').data(runs)
    .attr('cy', function(datum, index){
        return yScale(datum.distance);
    });
  • That callback function runs for each visual element selected
  • The result of the function is then assigned to whatever aspect of the element is being set (in this case the cy attribute)
  • The callback function takes two params
    • the individual datum object (from the original runs array of objects) attached to that particular visual element
    • the index of that datum in the original runs array

Create a time scale

  • Let's position the circles horizontally, based on the date that they happened
  • First create a time scale:
var xScale = d3.scaleTime(); //scaleTime maps date values with numeric visual points
xScale.range([0,WIDTH]);
xScale.domain([new Date('2017-10-1'), new Date('2017-10-31')]);

console.log(xScale.domain()); //you can get the domain whenever you want like this
console.log(xScale.range()); //you can get the range whenever you want like this

Parse and format times

  • Note that our date data isn't in the format expected by the xScale domain
  • D3 provides us an easy way to convert strings to dates and vice versa
var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p");
console.log(parseTime('October 3, 2017 at 6:00PM'));

var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p");
console.log(formatTime(new Date()));

Let's use this when calculating cx attributes for our circles:

var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p");
d3.selectAll('circle')
    .attr('cx', function(datum, index){
        return xScale(parseTime(datum.date)); //use parseTime to convert the date string property on the datum object to a Date object
    });

Set dynamic domains

  • At the moment, we're setting up arbitrary min/max values for both distance/date
  • D3 can find the min/max of a data set, so that our graph displays just the data ranges we need:
  • we pass the min/max methods a callback which gets called for each item of data in the array
    • d3 uses the callback to determine which values to compare for min/max
var yMin = d3.min(runs, function(datum, index){
    return datum.distance; //compare distance properties of each item in the data array
})
var yMax = d3.max(runs, function(datum, index){
    return datum.distance; //compare distance properties of each item in the data array
})
yScale.domain([yMin, yMax]);

We can combine both of these functions into one "extent" function that returns both:

var yDomain = d3.extent(runs, function(datum, index){
    return datum.distance; //compare distance properties of each item in the data array
})
yScale.domain(yDomain);

Let's do the same for the xScale's domain:

var parseTime = d3.timeParse("%B%e, %Y");
var xScale = d3.scaleTime();
xScale.range([0,WIDTH]);
xDomain = d3.extent(runs, function(datum, index){
    return parseTime(datum.date);
});
xScale.domain(xDomain);

Dynamically generate svg elements

  • Currently, we have just enough <circle> elements to fit our data. What if we don't want to count how many elements are in the array?
  • D3 Can create elements as needed
  • First, remove all <circle> elements from index.html
<svg></svg>

In app.js add the code to create the circles:

d3.select('svg').selectAll('circle') //since no circles exist, we need to select('svg') so that d3 knows where to append the new circles
    .data(runs) //attach the data as before
    .enter() //find the data objects that have not yet been attached to visual elements
    .append('circle'); //for each data object that hasn't been attached, append a <circle> to the <svg>

Create axes

D3 can automatically generate axes for you:

var bottomAxis = d3.axisBottom(xScale); //pass the appropriate scale in as a parameter
d3.select('svg')
	.append('g') //put everything inside a group
	.call(bottomAxis) //generate the axis within the group
    .attr('transform', 'translate(0,'+HEIGHT+')'); //move it to the bottom

Currently, our SVG clips the axis. Let's change some CSS so it doesn't:

svg {
    overflow: visible;    
}

The left axis is pretty similar:

var leftAxis = d3.axisLeft(yScale);
d3.select('svg')
	.append('g')
	.call(leftAxis); //no need to transform, since it's placed correctly initially

It's a little tough, so let's adding some margin to the body:

body {
    margin: 20px 40px;
}