emilyaviva.com
twitter.com/EmilyAviva
github.com/EmilyAviva
linkedin.com/in/emilykapor
Data | Serialized (JSON, CSV, etc.) |
Driven | |
Documents |
Output
|
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
}]
<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>
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', [])