Published
Edited
Apr 17, 2019
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// import d3 library
d3 = require("d3@5")
Insert cell
Insert cell
// bring in basemap data
boston = d3.json('https://gist.githubusercontent.com/cesandoval/09b2e39263c748fbcb84b927cecc7c46/raw/ab71d3638efd2545ec99c2651c6f2ddcea9d2a07/boston.json')

Insert cell
// define SVG dimensions, width
width = 960;
Insert cell
// define SVG dimensions, height
height = 600;
Insert cell
// project using Albers
bosAlbers = d3.geoAlbers()
.scale( 190000 )
.rotate( [71.057,0] )
.center( [0, 42.313] )
.translate( [width/2,height/2] )
Insert cell
// use D3 function to turn lat-long into positions on a screen
bos_geoPath = d3.geoPath()
.projection( bosAlbers );
Insert cell
//import 311 csv file
twitter311 = d3.csv('https://gist.githubusercontent.com/cesandoval/046a91586ae76889aeb5b3e9db53016e/raw/ffb0c410ce8503c8c839cde01235bafb39cb14ad/bostosn_311.csv')
Insert cell
// record tweeted 311s by neighborhood in a format that can be joined to the topojson base layer
tweeted311 = {
// Create empty object for holding dataset
const tweeted311ByNeigh = {};
// take log to address right skew. Add 1 to avoid log(0) = -infinity
// create column showing, for each neighborhood, what % of all 311 reports were made by tweets
// note that this depended on how I interpreted the question: ie, for each neighborhood, what is the % of complaints made by Twitter?
twitter311.forEach(d =>
tweeted311ByNeigh[d.id] = {tweets: +Math.log(d.twitter+1),
tweetsShare: (100.0*(parseFloat(d.twitter)/parseFloat(d['tot_count ']))).toFixed(2)}
)

return tweeted311ByNeigh;
}
Insert cell
// create appropriate purple color scale for chloropleth
color = d3.scaleQuantize()
.domain([1, 8])
.range(d3.schemePurples[9]);
Insert cell
{
// Create SVG
let svg = d3.select(DOM.svg(width, height));
let g = svg.append("g");
// Bind TopoJSON data
g.selectAll("path")
.data(topojson.feature(boston, boston.objects.boston_neigh).features) // Bind TopoJSON data elements
.enter().append("path")
.attr("d", bos_geoPath)
.style("fill", d => color(tweeted311[d.properties.OBJECTID]['tweets'])) // Color according to number of tweeted 311s. we use neighborhood ID to join that statistic to the neighorhood geometries in the baselayer map
.style("stroke", "black")

return svg.node();
}
Insert cell
Insert cell
// tooltip stylizing. Include here requirement for 5px padding, white background, 60% opacity

tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("font-family", "'Open Sans', sans-serif")
.style("font-size", "12px")
.style("background", "white")
.style("padding", "5px")
.style("z-index", "10")
.style("opacity",0.6)
.style("visibility", "hidden");
Insert cell

{
// Create SVG
let svg = d3.select(DOM.svg(width, height));
let g = svg.append("g");

tooltip
// Bind TopoJSON data
g.selectAll("path")
.data(topojson.feature(boston, boston.objects.boston_neigh).features) // Bind TopoJSON data elements
.enter().append("path")
.attr("d", bos_geoPath)
.style("fill", d => color(tweeted311[d.properties.OBJECTID]['tweets'])) // Color according to number of tweeted 311s. we use neighborhood ID to join that statistic to the neighorhood geometries in the baselayer map
.style("stroke", "black")
//mouseover popup will show neighborhood name: % 311 reports there made by Twitter
.on("mouseover", d => tooltip.style("visibility", "visible").text(d.properties.Name))
.on("mousemove", d => tooltip.style("top", (d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px").text(d.properties.Name + ": " + tweeted311[d.properties.OBJECTID]['tweetsShare'] + "%"))
.on("mouseout", d => tooltip.style("visibility", "hidden"));
return svg.node();
}
Insert cell
Insert cell
{
// Create SVG
let svg = d3.select(DOM.svg(width, height));
let g = svg.append("g");

tooltip
// add legend
g.call(legend);
// Bind TopoJSON data
g.selectAll("path")
.data(topojson.feature(boston, boston.objects.boston_neigh).features) // Bind TopoJSON data elements
.enter().append("path")
.attr("d", bos_geoPath)
.style("fill", d => color(tweeted311[d.properties.OBJECTID]['tweets']))// Color according to number of tweeted 311s. we use neighborhood ID to join that statistic to the neighorhood geometries in the baselayer map
.style("stroke", "black")
//mouseover effect: shows name of neighborhood name: % 311 reports there made by Twitter
.on("mouseover", d => tooltip.style("visibility", "visible").text(d.properties.Name))
.on("mousemove", d => tooltip.style("top", (d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px").text(d.properties.Name + ": " + tweeted311[d.properties.OBJECTID]['tweetsShare'] + "%"))
.on("mouseout", d => tooltip.style("visibility", "hidden"));
return svg.node();
}
Insert cell

legend = g => {
//scale for size of legend box
const x = d3.scaleLinear()
.domain(d3.extent(color.domain()))
.rangeRound([0, 250]);

// colored gradient box for the legend
g.selectAll("rect")
.data(color.range().map(d => color.invertExtent(d)))
.join("rect")
.attr("height", 8)
.attr("x", d => x(d[0]))
.attr("width", d => x(d[1]) - x(d[0]))
.attr("fill", d => color(d[0]));

// legend title; shifted down along y axis so that it shows up on the SVG
g.append("text")
.attr("x", x.range()[0])
.attr("y", 50)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Tweeted 311s per neighborhood (log scale)");

// add on labeled tickmarks
g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(d => d3.format(".2n")(d))
.tickValues(color.range().slice(1).map(d => color.invertExtent(d)[0])))
.select(".domain")
.remove();
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more