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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more