D3.js Step by Step

From Zero to Data Viz Hero

Kent English

@kentenglish

 

The deck:

slides.com/zeroviscosity/d3-js-step-by-step 

 

The blog post version: 

zeroviscosity.com (or just zevi.co)

Data-Driven Documents

Examples and docs:

http://d3js.org

 

Created by:

Mike Bostock

D3 can be daunting

The API is extensive

 

It can seem like a lot of code is needed to do anything at all

But don't panic

Take it step by step

We'll go from this:

To this (time permitting):

Steps:

  1. A Basic Pie Chart

  2. A Basic Donut Chart

  3. Adding a Legend

  4. Loading External Data

  5. Adding Tooltips

  6. Animating Interactivty

Step 1: A Basic Pie Chart

<div id="chart">
  <svg width="360" height="360">
    <g transform="translate(180,180)">
      <path d="M0,-180A..." fill="#393b79"></path>
      <path d="M105.801..." fill="#5254a3"></path>
      <path d="M171.190..." fill="#6b6ecf"></path>
      <path d="M-105.80..." fill="#9c9ede"></path>
    </g>
  </svg>
</div>

Our dummy dataset:

var dataset = [
  { label: 'Abulia', count: 10 }, 
  { label: 'Betelgeuse', count: 20 },
  { label: 'Cantaloupe', count: 30 },
  { label: 'Dijkstra', count: 40 }
];

Some dimensions:

var width = 360;
var height = 360;
var radius = Math.min(width, height) / 2;  

Colour Scale:

var color = d3.scale.category20b();
// Alternative
var color = d3.scale.ordinal()
  .range(['#A60F2B', '#648C85', '#B3F2C9', '#528C18']); 

Setting up the SVG:

var svg = d3.select('#chart')
  .append('svg')
  .attr('width', width)
  .attr('height', height)
  .append('g')
  .attr('transform', 'translate(' + 
    (width / 2) +  ',' + (height / 2) + ')');
var svg = d3.select('#chart')
  .append('svg')
  .attr('width', width)
  .attr('height', height)
  .append('g')
  .attr('transform', 'translate(' + 
    (width / 2) +  ',' + (height / 2) + ')');
<div id="chart">
  <svg width="360" height="360">
    <g transform="translate(180,180)">
      <path d="M0,-180A..." fill="#393b79"></path>
      <path d="M105.801..." fill="#5254a3"></path>
      <path d="M171.190..." fill="#6b6ecf"></path>
      <path d="M-105.80..." fill="#9c9ede"></path>
    </g>
  </svg>
</div>

Now for the pie segments

We need two things:

  • The radius

  • The angles

Defining the radius:

var arc = d3.svg.arc()
  .outerRadius(radius);

Defining the angles:

var pie = d3.layout.pie()
  .value(function(d) { return d.count; })
  .sort(null);

And now... drum roll, please...

Adding the path elements:

var path = svg.selectAll('path')
  .data(pie(dataset))
  .enter()
  .append('path')
  .attr('d', arc)
  .attr('fill', function(d, i) { 
    return color(d.data.label);
  });

This looks harder than it is

// Select 'path' elements (these don't currently exist)
var path = svg.selectAll('path')

  // Assign our 'pie' version of the dataset
  .data(pie(dataset))

  // Create placeholder elements for each data point
  .enter()

  // Replace placeholders with 'path' elements          
  .append('path')

  // Assign our 'arc' to the 'd' attribute
  .attr('d', arc)

  // Define how to determine the 'fill'
  .attr('fill', function(d, i) {

    // Use the colour scale we defined earlier
    return color(d.data.label);
  });

This is a pattern you'll see again and again in D3 examples.

svg.selectAll(path)
   .data(dataset)
   .enter()
   .append(path)

Step 2: A Basic Donut Chart

How can we do this?

First define a width:

var donutWidth = 75;

Then define an innerWidth():

var arc = d3.svg.arc()
  .innerRadius(radius - donutWidth)  // NEW
  .outerRadius(radius);

Ta da! We're done.

Easy mode, right?

Step 3: Adding a Legend

We want this:

Each entry in the legend looks like this:

<g class="legend" transform="translate(-36,-44)">
  <rect width="18" height="18" 
      style="fill: ... stroke: ..."></rect>
  <text x="22" y="14">Abulia</text>
</g>

We haven't used CSS yet...

Our first CSS:

.legend {
  font-size: 12px;
}

rect {
  stroke-width: 2;
}

(And this isn't strictly necessary)

Now for some sizes:

var legendRectSize = 18;

var legendSpacing = 4;

We're going to see this

pattern again:

svg.selectAll(path)
   .data(dataset)
   .enter()
   .append(path)

Adding the g elements:

var legend = svg.selectAll('.legend')
  .data(color.domain())
  .enter()
  .append('g')
  .attr('class', 'legend')
  .attr('transform', function(d, i) {
    // Some code to center the legend
  });

Centering the legend:

var legend = svg.selectAll('.legend')
  // ...
  // More code
  // ...
  .attr('transform', function(d, i) {
    var height = legendRectSize + legendSpacing;
    var offset =  height * color.domain().length / 2;
    var horz = -2 * legendRectSize;
    var vert = i * height - offset;
    return 'translate(' + horz + ',' + vert + ')';
  });

Adding the rect elements:

legend.append('rect')
  .attr('width', legendRectSize)
  .attr('height', legendRectSize)
  .style('fill', color)
  .style('stroke', color);

Adding the text elements:

legend.append('text')
  .attr('x', legendRectSize + legendSpacing)
  .attr('y', legendRectSize - legendSpacing)
  .text(function(d) { return d; });

Step 4: Loading External Data

Our dataset is rather contrived.

Time for something more realistic.

Toronto Parking Ticket Data

Our new dataset:

label,count
Monday,379130
Tuesday,424923
Wednesday,430728
Thursday,432138
Friday,428295
Saturday,368239
Sunday,282701

What we'll end up with:

Just wrap any dataset-dependent code in a callback to d3.csv():

// Code that doesn't depend on dataset

d3.csv('weekdays.csv', function(error, dataset) {
  // Code that depends on dataset
  // Like the definition of 'path' and 'legend'
});

There is also:

  • d3.tsv()
  • d3.dsv()

One thing: count needs to be a number

// Code that doesn't depend on dataset

d3.csv('weekdays.csv', function(error, dataset) {
  dataset.forEach(function(d) {
    d.count = +d.count; // Cast to number
  });

  // Code that depends on dataset
});

And that's it.

Step 5: Adding Tooltips

We'll harness mouse events

We'll need a bit of CSS

 

First, for the container:

#chart {
  height: 360px;
  position: relative;
  width: 360px;
}

Some tooltip styles:

.tooltip {
  background: #eee;
  box-shadow: 0 0 5px #999999;
  color: #333;
  display: none;
  font-size: 12px;
  left: 130px;
  padding: 10px;
  position: absolute;
  text-align: center;
  top: 95px;
  width: 80px;
  z-index: 10;
}

Create the tooltip:

var tooltip = d3.select('#chart')
  .append('div')
  .attr('class', 'tooltip');

tooltip.append('div')
  .attr('class', 'label');

tooltip.append('div')
  .attr('class', 'count');

tooltip.append('div')
  .attr('class', 'percent');

Event handlers:

var path = svg.selectAll('path')
  // Blah blah blah

path.on('mouseover', function(d) {
  // Code 
});

path.on('mouseout', function(d) {
  // Code
});
path.on('mouseover', function(d) {
  var total = d3.sum(dataset.map(function(d) {
    return d.count;
  }));
  var percent = Math.round(1000 * d.data.count / total) / 10;
  tooltip.select('.label').html(d.data.label);
  tooltip.select('.count').html(d.data.count); 
  tooltip.select('.percent').html(percent + '%'); 
  tooltip.style('display', 'block');
});

Mouseover:

path.on('mouseout', function() {
  tooltip.style('display', 'none');
});

Mouseout:

// OPTIONAL 
path.on('mousemove', function(d) {
  tooltip.style('top', (d3.event.pageY + 10) + 'px')
    .style('left', (d3.event.pageX + 10) + 'px');
});

(Optionally) mousemove:

Our tooltip will follow the cursor

until we leave the segment

Step 6: Animating Interactivity

What we want:

Animation is hard

D3 is here to help

A bit more CSS:

rect {
  cursor: pointer;
  stroke-width: 2;
}

rect.disabled {
  fill: transparent !important;
}

Add an enabled property:

d3.csv('weekdays.csv', function(error, dataset) {
  dataset.forEach(function(d) {
    d.count = +d.count;
    d.enabled = true;    // NEW
  });
  // ...
  // More code
  // ...

Add a _current property:

var path = svg.selectAll('path')
  .data(pie(dataset))
  .enter()
  .append('path')
  .attr('d', arc)
  .attr('fill', function(d, i) { 
    return color(d.data.label); 
  })
  .each(function(d) { this._current = d; }); // NEW

Change our total calculation:

path.on('mouseover', function(d) {
  var total = d3.sum(dataset.map(function(d) {
    return (d.enabled) ? d.count : 0;        // UPDATED
  }));
  // ...
  // More code
  // ...   

Now that the house

keeping is out of the way

React to click events:

legend.append('rect')
  .attr('width', legendRectSize)
  .attr('height', legendRectSize)                                   
  .style('fill', color)
  .style('stroke', color)
  .on('click', function(label) {  // NEW
    // ...
    // A bunch of code
    // ...
  });                             // NEW
// ...
// More code
// ...
.on('click', function(label) {
  var rect = d3.select(this);
  var enabled = true;
  var totalEnabled = d3.sum(dataset.map(function(d) {
    return (d.enabled) ? 1 : 0;
  }));
  
  if (rect.attr('class') === 'disabled') {
    rect.attr('class', '');
  } else {
    if (totalEnabled < 2) return;
    rect.attr('class', 'disabled');
    enabled = false;
  }

  // ...
  // More code
  // ...
// ...
// More code
// ...

  pie.value(function(d) {
    if (d.label === label) d.enabled = enabled;
    return (d.enabled) ? d.count : 0;
  });

  path = path.data(pie(dataset));

  path.transition()
    .duration(750)
    .attrTween('d', function(d) {
      var interpolate = d3.interpolate(this._current, d);
      this._current = interpolate(0);
      return function(t) {
        return arc(interpolate(t));
      };
    });
});

And we're done!

Thanks!

 

The deck:

slides.com/zeroviscosity/d3-js-step-by-step 

 

The blog post version: 

zeroviscosity.com (or just zevi.co)

D3.js Step by Step

By zeroviscosity

D3.js Step by Step

Presented at Web Unleashed 2014 in Toronto

  • 6,853