On Github emilyaviva / painting-data-d3
emilyaviva.com twitter.com/EmilyAviva github.com/EmilyAviva linkedin.com/in/emilykapor
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.
This section is based on Mike Bostock's excellent Voronoi arc map, showing commercial airline flights between cities (GPL v3)
<!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>
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)
queue() .defer(d3.json, 'us.json') .defer(d3.csv, 'airports.csv') .defer(d3.csv, 'flights.csv') .await(ready)
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))
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)
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))
.airport-arcs { display: none; fill: none; stroke: #000; } .airport-cell { fill: none; pointer-events: all; } .airports circle { fill: steelblue; stroke: #fff; pointer-events: none; }
.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; }
Acreage of Commercial Hop Production in North America, 2015
var hopsData = [{ name: 'Washington', acres: 32205 }, { name: 'Oregon', acres: 6807 }, { name: 'Idaho', acres: 4975 }, { name: 'Other States', acres: 1244 }, { name: 'Canada', acres: 257 }]
<!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>
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})`)
var arc = d3.svg.arc() .innerRadius(totalRadius - donutHoleRadius) .outerRadius(totalRadius) var pie = d3.layout.pie() .value((d) => d.acres) .sort(null)
var path = svg .selectAll('path') .data(pie(hopsData)) .enter() .append('path') .attr('d', arc) .attr('fill', (d, i) => color(d.data.name))
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})` })
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);
.legend { font-size: 12px; font-family: sans-serif; rect { stroke-width: 2; } text { fill: lightcyan; } }
React likes to have control over the entire DOM…
…but D3 likes to have control over the SVG DOM
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 ( ) } }
...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
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 />} /> ) } }
http://codepen.io/emilyaviva/pen/bebQzZ/
Inspired by Mike Bostock's Multi-Series Line Chart
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' }] }])
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', [])