Painting the Data Picture
with D3.js

Emily Aviva Kapor-Mater

emilyaviva.com
twitter.com/EmilyAviva
github.com/EmilyAviva
linkedin.com/in/emilykapor

What is D3?

Data Serialized (JSON, CSV, etc.)
Driven
Documents Output
  • HTML, CSS, SVG
  • …but not just web!

A complex visualization:
Voronoi Arc Map

The partitioning of a plane with n points into convex polygons such that each polygon contains exactly one generating point and every point in a given polygon is closer to its generating point than to any other.

Wolfram MathWorld

This section is based on Mike Bostock's excellent Voronoi arc map, showing commercial airline flights between cities (GPL v3)

Include some extra libraries


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Voronoi Arc Map</title>
  </head>
  <body>
    <div id="chart"></div>
    <script src="//path/to/d3.js"></script>
    <script src="//path/to/d3/queue.js"></script>
    <script src="//path/to/mbostock/topojson.js"></script>
    <script src="client.js"></script>
  </body>
</html>

Set up D3

var width = 960
var height = 500

var projection = d3.geo.albers()
  .translate([width / 2, height / 2])
  .scale(1080)

var path = d3.geo.path()
  .projection(projection)

var voronoi = d3.geom.voronoi()
  .x((d) => d.x)
  .y((d) => d.y)
  .clipExtent([[0, 0], [width, height]])

var svg = d3
  .select('#chart')
  .append('svg')
    .attr('width', width)
    .attr('height', height)

Asynchronously load data until ready

queue()
  .defer(d3.json, 'us.json')
  .defer(d3.csv, 'airports.csv')
  .defer(d3.csv, 'flights.csv')
  .await(ready)

When ready, start calculating what to draw

function ready(error, us, airports, flights) {
  if (error) throw error;

  var airportById = d3.map()

  airports.forEach((d) => {
    airportById.set(d.iata, d)
    d.outgoing = []
    d.incoming = []
  })

  flights.forEach((flight) => {
    var source = airportById.get(flight.origin)
    var target = airportById.get(flight.destination)
    var link = {source, target}
    source.outgoing.push(link)
    target.incoming.push(link)
  })
}
airports = airports.filter((d) => {
  if (d.count = Math.max(d.incoming.length, d.outgoing.length)) {
    d[0] = +d.longitude
    d[1] = +d.latitude
    var position = projection(d)
    d.x = position[0]
    d.y = position[1]
    return true
  }
})

voronoi(airports).forEach((d) => {
  d.point.cell = d
})

svg.append('path')
  .datum(topojson.feature(us, us.objects.land))
  .attr('class', 'states')
  .attr('d', path)

svg.append('path')
  .datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b))

Draw airports and their Voronoi cells

var airport = svg.append('g')
  .attr('class', 'airports')
  .selectAll('g')
  .data(airports.sort((a, b) => b.count - a.count))
  .enter()
  .append('g')
    .attr('class', 'airport')

airport
  .append('path')
  .attr('class', 'airport-cell')
  .attr('d', (d) => d.cell.length ? 'M' + d.cell.join('L') + 'Z' : null)

Draw arcs for fligths, and circles for airports

airport
  .append('g')
  .attr('class', 'airport-arcs')
  .selectAll('path')
  .data((d) => d.outgoing)
  .enter()
  .append('path')
    .attr('d', (d) => path({type: 'LineString', coordinates: [d.source, d.target]})

airport
  .append('circle')
  .attr('transform', (d) => `translate(${d.x}, ${d.y})`)
  .attr('r', (d, i) => Math.sqrt(d.count))

Prettify with stylesheets

Arcs and cells are not shown by default

.airport-arcs {
  display: none;
  fill: none;
  stroke: #000;
}
.airport-cell {
  fill: none;
  pointer-events: all;
}
.airports circle {
  fill: steelblue;
  stroke: #fff;
  pointer-events: none;
}

Arcs and cells toggle on hover events

.airport:hover .airport-arcs {
  display: inline;
}
svg:not(:hover) .airport-cell {
  stroke: #000;
  stroke-opacity: .2;
}

.states {
  fill: #ccc;
}
.state-borders {
  fill: none;
  stroke: #fff;
  stroke-width: 1.5px;
  stroke-linejoin: round;
  stroke-linecap: round;
}

Result

A simpler start:
A simple donut chart

http://codepen.io/emilyaviva/pen/aZoqBX

Prepare the data

Acreage of Commercial Hop Production in North America, 2015

usahops.org

var hopsData = [{
  name: 'Washington',
  acres: 32205
}, {
  name: 'Oregon',
  acres: 6807
}, {
  name: 'Idaho',
  acres: 4975
}, {
  name: 'Other States',
  acres: 1244
}, {
  name: 'Canada',
  acres: 257
}]

Create an empty <div> for the SVG


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>North American Hops</title>
  </head>
  <body>
    <div id="chart"></div>
    <script src="//path/to/d3.js"></script>
    <script src="client.js"></script>
  </body>
</html>

Set up D3


var width = 400
var height = 400
var totalRadius = Math.min(width, height) / 2
var donutHoleRadius = totalRadius * 0.5
var color = d3.scale.category10()

var svg = d3
  .select('#chart')
  .append('svg')
  .attr('width', width)
  .attr('height', height)
  .append('g')
    .attr('transform', `translate(${width / 2}, ${height / 2})`)

Telling D3 the basic shapes


var arc = d3.svg.arc()
  .innerRadius(totalRadius - donutHoleRadius)
  .outerRadius(totalRadius)

var pie = d3.layout.pie()
  .value((d) => d.acres)
  .sort(null)

...and binding the data


var path = svg
  .selectAll('path')
  .data(pie(hopsData))
  .enter()
  .append('path')
    .attr('d', arc)
    .attr('fill', (d, i) => color(d.data.name))

Behold, a donut chart

Put a legend inside the chart


var legendItemSize = 18
var legendSpacing = 4

var legend = svg
  .selectAll('.legend')
  .data(color.domain())
  .enter()
  .append('g')
    .attr('class', 'legend')
    .attr('transform', (d, i) => {
      var height = legendItemSize + legendSpacing;
      var offset = height * color.domain().length / 2;
      var x = legendItemSize * -2
      var y = (i * height) - offset
      return `translate(${x}, ${y})`
    })

Now let's actually draw some boxes


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

legend
  .append('text')
    .attr('x', legendItemSize + legendSpacing)
    .attr('y', legendItemSize - legendSpacing)
    .text((d) => d);

...and, of course, make it pretty


.legend {
  font-size: 12px;
  font-family: sans-serif;
  rect {
    stroke-width: 2;
  }
  text {
    fill: lightcyan;
  }
}
Washington Oregon Idaho Other States Canada

D3 with Popular Frameworks

D3 + React

React likes to have control over the entire DOM…

…but D3 likes to have control over the SVG DOM

Solution 1: Wrap D3 components


import React from 'react'

export default class D3Component extends React.Component {
  constructor(props) {
    super(props)
  }

  initialize() {}
  update(prevProps, prevState) {}
  destroy() {}

  componentDidMount() {
    this.initialize()
    this.update()
  }

  componentDidUpdate(prevProps, prevState) {
    this.update(prevProps, prevState)
  }

  componentWillUnmount() {
    this.destroy()
  }

  render() {
    return (
      
    )
  }
}

Solution 2: Use a pre-built wrapping solution

...e.g. react-d3-wrap


import d3 from 'd3'
import d3Wrap from 'react-d3-wrap'

const MyChart = d3Wrap({
  initialize(svg, data, options) {},
  update(svg, data, options) {
    const chart = d3
      .select(svg)
      .append('g')
      .attr('transform', `translate(${options.margin.left}, ${options.margin.top})`)
    // etc., etc.
  },
  destroy() {}
})

export default MyChart

Solution 3: Victory from Formidable

http://formidable.com/open-source/victory/docs/victory-scatter


class CatPoint extends React.Component {
  render() {
    const {x, y, datum} = this.props
    const cat = datum.y >= 0 ? '😻' : '😹'
    return <text x={x} y={y} fontSize={30}>{cat}</text>
  }
}

class App extends React.Component {
  render() {
    return (
      <VictoryScatter
        height={500}
        y={(d) => Math.sin(2 * Math.PI * d.x)}
        samples={25}
        dataComponent={<CatPoint />}
      />
    )
  }
}
😻 😻 😻 😻 😻 😻 😻 😻 😻 😻 😻 😻 😻 😹 😹 😹 😹 😹 😹 😹 😹 😹 😹 😹 😹 😹

D3 + Angular

Wrap D3 in an Angular directive

http://codepen.io/emilyaviva/pen/bebQzZ/

Inspired by Mike Bostock's Multi-Series Line Chart

Controller gets data

Fremont Bridge Hourly Bicycle Counts, 1 May 2015

https://dev.socrata.com/foundry/data.seattle.gov/4xy5-26gy


app.controller('BikeCrossingController', ['$scope', ($scope) => {
  $scope.bikeCrossingData = [{
    date: '2015-05-01T00:00:00.000',
    fremont_bridge_nb: '5',
    fremont_bridge_sb: '14'
  }, {
    date: '2015-05-01T01:00:00.000',
    fremont_bridge_nb: '1',
    fremont_bridge_sb: '3'
  }, {
    date: '2015-05-01T02:00:00.000',
    fremont_bridge_nb: '1',
    fremont_bridge_sb: '6'
  }, {
    date: '2015-05-01T03:00:00.000',
    fremont_bridge_nb: '3',
    fremont_bridge_sb: '2'
  }, {
    date: '2015-05-01T04:00:00.000',
    fremont_bridge_nb: '8',
    fremont_bridge_sb: '0'
  }, {
    date: '2015-05-01T05:00:00.000',
    fremont_bridge_nb: '29',
    fremont_bridge_sb: '15'
  }, {
    date: '2015-05-01T06:00:00.000',
    fremont_bridge_nb: '110',
    fremont_bridge_sb: '54'
  }, {
    date: '2015-05-01T07:00:00.000',
    fremont_bridge_nb: '390',
    fremont_bridge_sb: '94'
  }, {
    date: '2015-05-01T08:00:00.000',
    fremont_bridge_nb: '399',
    fremont_bridge_sb: '164'
  }, {
    date: '2015-05-01T09:00:00.000',
    fremont_bridge_nb: '151',
    fremont_bridge_sb: '100'
  }, {
    date: '2015-05-01T10:00:00.000',
    fremont_bridge_nb: '67',
    fremont_bridge_sb: '46'
  }, {
    date: '2015-05-01T11:00:00.000',
    fremont_bridge_nb: '49',
    fremont_bridge_sb: '47'
  }, {
    date: '2015-05-01T12:00:00.000',
    fremont_bridge_nb: '57',
    fremont_bridge_sb: '47'
  }, {
    date: '2015-05-01T13:00:00.000',
    fremont_bridge_nb: '57',
    fremont_bridge_sb: '66'
  }, {
    date: '2015-05-01T14:00:00.000',
    fremont_bridge_nb: '73',
    fremont_bridge_sb: '85'
  }, {
    date: '2015-05-01T15:00:00.000',
    fremont_bridge_nb: '89',
    fremont_bridge_sb: '183'
  }, {
    date: '2015-05-01T16:00:00.000',
    fremont_bridge_nb: '149',
    fremont_bridge_sb: '326'
  }, {
    date: '2015-05-01T17:00:00.000',
    fremont_bridge_nb: '211',
    fremont_bridge_sb: '418'
  }, {
    date: '2015-05-01T18:00:00.000',
    fremont_bridge_nb: '126',
    fremont_bridge_sb: '206'
  }, {
    date: '2015-05-01T19:00:00.000',
    fremont_bridge_nb: '80',
    fremont_bridge_sb: '95'
  }, {
    date: '2015-05-01T20:00:00.000',
    fremont_bridge_nb: '42',
    fremont_bridge_sb: '43'
  }, {
    date: '2015-05-01T21:00:00.000',
    fremont_bridge_nb: '18',
    fremont_bridge_sb: '30'
  }, {
    date: '2015-05-01T22:00:00.000',
    fremont_bridge_nb: '19',
    fremont_bridge_sb: '13'
  }, {
    date: '2015-05-01T23:00:00.000',
    fremont_bridge_nb: '11',
    fremont_bridge_sb: '22'
  }]
}])

Sort out our data, scales, and axes


app.directive('lineChart', ($parse, $window) => {
  return {
    restrict: 'EA',
    template: '',
    link: function(scope, el, attrs) {
      var d3 = $window.d3
      var margin = {top: 20, right: 80, bottom: 30, left: 50}
      var width = 960 - margin.left - margin.right
      var height = 500 - margin.top - margin.bottom

      var data = scope.bikeCrossingData.map((d) => {
        return {
          hour: (new Date(d.date).getHours() + 8) % 24,
          northbound: d.fremont_bridge_nb,
          southbound: d.fremont_bridge_sb
        }
      }).sort((a, b) => d3.ascending(a.hour, b.hour))

      var svg = d3.select(el.find('svg')[0])

      var color = d3.scale.category10()
      color.domain(d3.keys(data[0])
        .filter((key) => key !== 'hour'))

      var curves = color.domain().map((name) => {
        return {
          name,
          values: data.map((d) => {
            return {
              hour: d.hour,
              bikers: +d[name]
            }
          })
        }
      })

      // set scales
      var x = d3.scale.linear().range([0, width])
      var y = d3.scale.linear().range([height, 0])

      x.domain(d3.extent(data, (d) => d.hour))
      y.domain([
        d3.min(curves, (c) => d3.min(c.values, (v) => v.bikers)),
        d3.max(curves, (c) => d3.max(c.values, (v) => v.bikers))
      ])

      // set axes
      var hourLabels = ['12am', '2am', '4am', '6am', '8am', '10am', '12pm', '2pm', '4pm', '6pm', '8pm', '10pm']

      var xAxis = d3.svg.axis()
        .scale(x)
        .orient('bottom')
        .ticks(12)
        .tickFormat((d, i) => hourLabels[i])

      var yAxis = d3.svg.axis()
        .scale(y)
        .orient('left')
        .ticks(8)

      // draw the lines in between the data points
      var line = d3.svg.line()
        .interpolate('basis')
        .x((d) => x(d.hour))
        .y((d) => y(d.bikers))

      svg = svg
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
        .append('g')
        .attr('transform', `translate(${margin.left}, ${margin.top})`)

      svg.append('g')
        .attr('class', 'x axis')
        .attr('transform', `translate(0, ${height})`)
        .call(xAxis)

      svg.append('g')
        .attr('class', 'y axis')
        .call(yAxis)

      // bind the curves to our data
      var curve = svg
        .selectAll('.curve')
        .data(curves)
        .enter()
        .append('g')
        .attr('class', 'curve')

      curve.append('path')
        .attr('class', 'line')
        .attr('d', (d) => line(d.values))
        .style('stroke', (d) => color(d.name))

      // show the label after the curve
      curve.append('text')
        .datum((d) => {
          return {
            name: d.name,
            value: d.values[d.values.length - 1]
          }
        })
        .attr('transform', (d) => `translate(${x(d.value.hour)}, ${y(d.value.bikers)})`)
        .attr('x', 3)
        .attr('dy', '.35em')
        .style('fill', (d) => color(d.name))
        .text((d) => d.name)

      // don't show ticks at the origin
      svg.selectAll('.tick')
        .filter((d) => d === 0)
        .remove()
    }
  }
})

body {
  background: black;
  color: white;
  font-family: sans-serif;
  font-size: 12px;
}
.axis {
  path, line {
    fill: none;
    stroke: white;
    shape-rendering: crispEdges;
  }
}
.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}
text {
  fill: white;
}

<div ng-app="visualizationApp" ng-controller="BikeCrossingController">
  <div line-chart chart-data="bikeCrossingData"></div>
</div>

var app = angular.module('visualizationApp', [])

See the result on CodePen

Where next?

Emily Aviva Kapor-Mater

@EmilyAviva

EmilyAviva

emilykapor