These are the materials for my workshop on creating interactive data visualizations with D3! We will be using the following two tools to work through the exercises:
And please do not hesitate to reach out to me directly via email at jondinu@gmail.com or over twitter @clearspandex
Throughout this workshop, you will learn how to make an interactive map of AirBnB listings in SF to better understand the companies impact on the city.
The data comes from Inside AirBnB and it is available below under a Creative Commons CC0 1.0 Universal (CC0 1.0) "Public Domain Dedication" license.
Inside AirBnB is an independent, non-commercial set of tools and data that allows you to explore how Airbnb is really being used in cities around the world.
Github repo: https://github.com/Jay-Oh-eN/interactive-data-viz
Archival event links:
forked from Jay-Oh-eN's block: Civic Impact through Data Visualization: Final
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.8/d3.min.js"></script>
<style>
body, html {
width: 960px;
height: 100%;
}
​
svg {
width:50%;
height:100%;
float: left;
}
​
circle.airbnb {
fill: #e00007;
opacity: 0.6;
}
​
.axis {
font-family: arial;
font-size: 0.7em;
}
text {
fill: black;
stroke: none;
}
.label {
font-size: 1.5em;
}
path {
fill: none;
stroke: black;
stroke-width: 2px;
}
.tick {
fill: none;
stroke: black;
}
circle {
opacity: 0.9;
stroke: none;
fill: red;
}
.line {
fill: none;
stroke: #e00007;
stroke-width: 1px;
}
</style>
<script>
function draw(geo_data) {
"use strict";
​
/*
D3.js setup code
*/
​
var margin = 50,
width = 450 - margin,
height = 500 - margin;
​
// https://github.com/mbostock/d3/wiki/Time-Formatting
var format = d3.time.format("%Y-%m-%d");
// create a projection properly scaled for SF
var projection = d3.geo.mercator()
.center([-122.433701, 37.767683])
.scale(150000)
.translate([width / 1.5, height / 1.74]);
// create a path to draw the neighborhoods
var path = d3.geo.path()
.projection(projection);
​
// create and append the map of SF neighborhoods
var map = d3.select('#map').selectAll('path')
.data(geo_data.features)
.enter()
.append('path')
.attr('d', path)
.style('fill', '#eee')
.style('stroke', 'black')
.style('stroke-width', 1);
​
// normalize neighborhood names
map.datum(function(d) {
var normalized = d.properties.neighbourhood
.replace(/ /g, '_')
.replace(/\//g, '_');
​
d.properties.neighbourhood = normalized;
return d;
});
​
// add the neighborhood name as its class
map.attr('class', function(d) {
return d.properties.neighbourhood;
});
​
// load the listing per neighborhood dataset
d3.json("https://jay-oh-en.github.io/interactive-data-viz/data/airbnb/listing_count.json", function(data) {
// find the min/max of listing per neighborhood
var listings_extent = d3.extent(d3.values(data));
​
// append a bubble to each neighborhood
var bubbles = d3.select('#map').append("g")
.attr("class", "bubble")
.selectAll("circle")
.data(geo_data.features)
.enter()
.append("circle")
.attr('class', 'airbnb');
​
// add the listing data to each neighborhood datum
bubbles.datum(function(d) {
d.count = data[d.properties.neighbourhood];
return d;
});
​
// scale each bubble with a sqrt scale
var radius = d3.scale.pow().exponent(0.5)
.domain(listings_extent)
.range([3, 20]);
​
// transform each bubbles' attributes according to the data
bubbles
.attr("cx", function(d) { return path.centroid(d.geometry)[0]; })
.attr("cy", function(d) { return path.centroid(d.geometry)[1]; })
.attr("r", function(d) { return radius(d.count); });
​
// load the review timeseries data for each neighborhood
d3.csv('https://jay-oh-en.github.io/interactive-data-viz/data/airbnb/neighborhood_reviews_timeseries.csv',
function(timeseries) {
// initialize the Mission as the default neighborhood
var field = "Mission";
​
// maximum reviews
var max_y = d3.max(timeseries, function(d) {
var max = 0;
​
d3.values(d).forEach(function(i) {
if (+i && (+i > max)) {
max = +i;
}
});
​
return max;
});
​
// Create y-axis scale mapping price -> pixels
var measure_scale = d3.scale.linear()
.range([height, 100])
.domain([0, max_y]);
​
// Create D3 axis object from measure_scale for the y-axis
var measure_axis = d3.svg.axis()
.scale(measure_scale)
.orient("right");
​
// Append SVG to page corresponding to the D3 y-axis
d3.select('#chart').append('g')
.attr('class', 'y axis')
.attr("transform", "translate(" + width + " , -15)")
.call(measure_axis);
​
// add label to y-axis
d3.select(".y.axis")
.append("text")
.attr('class', 'label')
.text("Reviews per week")
.attr("transform", "translate(45,215) rotate(90)");
​
// create a function to draw the timeseries for each neighborhood
var drawChart = function(field) {
// remove the previous chart
d3.select('#chart').select('.x.axis').remove();
d3.select('#chart').select('path').remove();
​
// update the title
d3.select('#heading')
.text(field.replace(/_/g, ' '));
​
// remove missing values
var neigh_data = timeseries.filter(function(d) {
return d[field];
});
​
// get min/max dates
var time_extent = d3.extent(neigh_data, function(d){
return format.parse(d['timestamp']);
});
​
// Create x-axis scale mapping dates -> pixels
var time_scale = d3.time.scale()
.range([0, width - margin])
.domain(time_extent);
​
// Create D3 axis object from time_scale for the x-axis
var time_axis = d3.svg.axis()
.scale(time_scale)
.tickFormat(d3.time.format("%b '%y"));
​
// Append SVG to page corresponding to the D3 x-axis
d3.select('#chart').append('g')
.attr('class', 'x axis')
.attr('transform', "translate(" + margin + ',' + (height - 15) + ")")
.call(time_axis)
.selectAll("text")
.attr("y", 0)
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(90)")
.style("text-anchor", "start");
​
// define the values to map for x and y position of the line
var line = d3.svg.line()
.x(function(d) { return time_scale(format.parse(d['timestamp'])); })
.y(function(d) { return measure_scale(+d[field]); });
​
// append a SVG path that corresponds to the line chart
d3.select('#chart').append("path")
.datum(neigh_data)
.attr("class", "line")
.attr("d", line)
.attr('transform', 'translate(' + margin + ', -15)');
};
​
drawChart(field);
​
// create a callback for the neighborhood hover
var mover = function(d) {
var neigh = d.properties.neighbourhood;
d3.select('#map path.' + neigh).style('fill', 'black');
drawChart(neigh);
};
​
// create a callback for the neighborhood hover
var mout = function(d) {
var neigh = d.properties.neighbourhood;
d3.select('path.' + neigh).style('fill', '#eee');
}
​
// attach events to neighborhoods in map
map.on("mouseover", mover);
map.on("mouseout", mout);
​
// attach events to bubbles on map
bubbles.on('mouseover', mover);
bubbles.on('mouseout', mout);
});
});
}
</script>
</head>
<body>
<svg id="map"></svg>
<svg id="chart">
<text x="50%" y="50" id="heading" font-size="1.5em" text-anchor="middle" font-family="futura">SF</text>
</svg>
<script>
// load neighborhood data
d3.json("https://jay-oh-en.github.io/interactive-data-viz/data/airbnb/neighbourhoods.geojson", draw);
</script>
</body>
</html>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.8/d3.min.js