Based on Mike Bostock’s coropleth, I used a interpolator instead. Data for unemployment by year is from the Bureau of Labor Statistics. I’m planning to modularize this design and make it slimmer in order to be able to punch in other county-based data, so more to come.
Built with blockbuilder.org. Legend plug-in from Susie Lu (http://d3-legend.susielu.com/).
forked from KingOfCramers‘s block: Unemployment in the United States by County
<!DOCTYPE html>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet">
<style>
text {
font-family: "Raleway"
}
#start{
display: block;
position: absolute;
top: 300px;
left: 400px;
width: 100px;
color: white;
background-color: green;
border: none;
padding: 10px;
font-size: 15px;
}
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
#tooltip{
display: block;
position: absolute;
top: 300px;
left: 400px;
max-width: 150px;
z-index: 1000;
background-color: white;
padding: 5px;
font-family: Raleway;
font-size: 11px;
opacity: 0;
}
.active {
stroke-width: 1px;
stroke: white;
}
.inactive {
stroke: black;
stroke-width: .25px;
}
#tooltip span.positive {
color: red;
}
#tooltip span.negative {
color: green;
}
#title {
font-family: Raleway;
position: absolute;
display: block;
margin: 20px;
fill: white;
font-size: 35px;
box-shadow: 2px solid black;
}
.states{
fill: none;
stroke: lightgrey;
stroke-width: 1px;
}
text.label {
fill: white;
}
g.legendLinear{
background-color: white;
}
</style>
<body>
</body>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.25.4/d3-legend.js"></script>
<script>
var width = 960;
var height = 600;
var margin = {top: 0, bottom: 0, left: 0, right: 0}
var path = d3.geoPath(); // Geopath generator
var startYear = 2007;
var unemployment = new Map();
var year = startYear;
var color = d3.scaleSequential()
.domain([1.0,30.0])
.interpolator(d3.interpolateInferno);
d3.queue()
.defer(d3.json, "https://d3js.org/us-10m.v1.json")
.defer(d3.csv, "unemploymentEdited.csv", function(d){
unemployment.set(d.county, [+d.rate2007,+d.rate2008,+d.rate2009,+d.rate2010,+d.rate2011,+d.rate2012,+d.rate2013,+d.rate2014,+d.rate2015,+d.rate2016,d.area])
})
.await(ready)
function ready(error, us) {
var zoomExtent = d3.zoom().scaleExtent([1, 20]);
function zoom() {
g.attr("transform", d3.event.transform)
}
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("background-color","lightgrey")
.call(zoomExtent
.on("zoom", zoom))
var tooltip = d3.select("body")
.append("div")
.attr("id","tooltip")
.attr("pointer-events","none")
var g = svg.append("g")
.attr("class", "counties")
.attr("transform",`translate(8,${margin.top})`)
.on("mouseleave", function(){
d3.select("#tooltip")
.transition()
.style("opacity",0)
})
.on("mouseover", function(){
d3.select("#tooltip")
.transition()
.style("opacity",1)
})
svg.append("g")
.attr("class", "legendLinear")
.attr("transform", `translate(${width-420},${height-90})`);
var legendLinear = d3.legendColor()
.shapeWidth(35)
.shapePadding(3)
.cells(10)
.orient('horizontal')
.scale(color);
svg.select(".legendLinear")
.call(legendLinear);
d3.select("text.label").text("1.0%")
var paths = g.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("fill", function(d){
return unemployment.get(d.id) ?
color(unemployment.get(d.id)[0]) : "white";
})
.attr("d", path)
.attr("class","inactive")
.on("mouseover",function(d){
var countyName = unemployment.get(d.id)[10]
d3.select(this)
.attr("class","active")
.raise();
d3.selectAll(".states").raise();
d3.select("#tooltip").html(function(){
let isLastYear = d3.select("#year").text() == 2016;
let unemplThatYear = unemployment.get(d.id)[year - startYear]
if(year != 2007 || isLastYear){
var rate = unemployment.get(d.id)[year-startYear] - unemployment.get(d.id)[year-2008] >= 0 ?
"positive" : "negative";
if(isLastYear){
let tempYear = 2016;
var tempRate = unemployment.get(d.id)[tempYear-startYear] - unemployment.get(d.id)[tempYear-2008] >= 0 ?
"positive" : "negative";
let tempUnemplThatYear = unemployment.get(d.id)[tempYear - startYear]
return `${countyName}: ${tempUnemplThatYear}% <span class=${rate}>(${(unemployment.get(d.id)[tempYear-startYear] - unemployment.get(d.id)[2016-2008]).toFixed(2)})</span>`
}
return `${countyName}: ${unemplThatYear}% <span class=${rate}>(${(unemployment.get(d.id)[year-startYear] - unemployment.get(d.id)[year-2008]).toFixed(2)})</span>`
}
else {
return `${countyName}: ${unemplThatYear}%`
}
})
})
.on("mousemove", function(d,event){
let tooltipHeight = $("#tooltip").height();
d3.select("#tooltip").style("left", () => d3.event.x + 15 + "px")
d3.select("#tooltip").style("top", () => d3.event.y - tooltipHeight - 20 + "px")
})
.on("mouseleave",function(){
d3.select(this)
.attr("class","inactive")
})
g.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("class", "states")
.attr("d", path);
svg.append("text")
.text(startYear)
.attr("x",width - 205)
.attr("y",56)
.attr("id","year")
.style("font-size", "45px")
.style("fill","white")
.style("opacity",0)
d3.select("body")
.append("button")
.attr("type","button")
.text("Start")
.attr("id","start")
d3.select("#start").on("click", function(){
d3.select(this).transition().style("opacity",0)
d3.select("#title").transition().style("opacity",0).remove();
d3.select("#year").transition().style("opacity",1)
var runAnimation = setInterval(wrapper, 2500);
function wrapper(){
if(year<2016){
year++
recolor(year);
}
else{
clearInterval(runAnimation)
year = startYear;
d3.select("#start")
.attr("z-index",-1)
.text("Restart")
.style("background-color","orange")
.transition().style("opacity",1)
}
}
})
function recolor(x){
d3.selectAll("path")
.transition()
.duration(500)
.attr("fill", function(d){
return unemployment.get(d.id) ? color(unemployment.get(d.id)[x-startYear]) : "white";
})
.on("end", function(d){
$("#year").text(year)
if(document.getElementsByClassName("active")[0]){
let countyTag = d3.select(".active").data()[0].id;
var rate = unemployment.get(countyTag)[x-startYear] - unemployment.get(countyTag)[x-(startYear+1)] >= 0 ?
"positive" : "negative"
$("#tooltip").html(unemployment.get(countyTag)[10] + ": " + unemployment.get(countyTag)[x-startYear] + "% " +`<span class=${rate}>(` + (unemployment.get(countyTag)[x-startYear] - unemployment.get(countyTag)[x-(startYear+1)]).toFixed(2) +")</span>")
}
})
}
// APPEND THE TITLE
var title = d3.select("svg")
.append("text")
.text("Unemployment in the United States")
.attr("id","title")
.attr("transform",`translate(200,260)`)
}
</script>
import { select } from 'd3-selection'
import { format, formatPrefix } from 'd3-format'
const d3_identity = (d) => d
const d3_reverse = (arr) => {
const mirror = [];
for (let i = 0, l = arr.length; i < l; i++) {
mirror[i] = arr[l-i-1];
}
return mirror;
}
//Text wrapping code adapted from Mike Bostock
const d3_textWrapping = (text, width) => {
text.each(function() {
var text = select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.2, //ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || 0,
tspan = text.text(null)
.append("tspan")
.attr("x", 0)
.attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width && line.length > 1) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", 0)
.attr("dy", lineHeight + dy + "em").text(word);
}
}
});
}
const d3_mergeLabels = (gen=[], labels, domain, range) => {
if (typeof labels === "object"){
if(labels.length === 0) return gen;
let i = labels.length;
for (; i < gen.length; i++) {
labels.push(gen[i]);
}
return labels;
} else if (typeof labels === "function") {
const customLabels = []
const genLength = gen.length
for (let i=0; i < genLength; i++){
customLabels.push(labels({
i,
genLength,
generatedLabels : gen,
domain,
range }))
}
return customLabels
}
return gen;
}
const d3_linearLegend = (scale, cells, labelFormat) => {
let data = [];
if (cells.length > 1){
data = cells;
} else {
const domain = scale.domain(),
increment = (domain[domain.length - 1] - domain[0])/(cells - 1)
let i = 0;
for (; i < cells; i++){
data.push(domain[0] + i*increment);
}
}
const labels = data.map(labelFormat);
return {data: data,
labels: labels,
feature: d => scale(d)};
}
const d3_quantLegend = (scale, labelFormat, labelDelimiter) => {
const labels = scale.range().map( d => {
const invert = scale.invertExtent(d);
return labelFormat(invert[0]) + " " + labelDelimiter + " " + labelFormat(invert[1]);
});
return {data: scale.range(),
labels: labels,
feature: d3_identity
};
}
const d3_ordinalLegend= scale => ({data: scale.domain(),
labels: scale.domain(),
feature: d => scale(d) }
)
const d3_cellOver = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellover", obj, d);
}
const d3_cellOut = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellout", obj, d);
}
const d3_cellClick = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellclick", obj, d);
}
export default {
d3_drawShapes: (shape, shapes, shapeHeight, shapeWidth, shapeRadius, path) => {
if (shape === "rect"){
shapes.attr("height", shapeHeight)
.attr("width", shapeWidth);
} else if (shape === "circle") {
shapes.attr("r", shapeRadius)
} else if (shape === "line") {
shapes.attr("x1", 0).attr("x2", shapeWidth).attr("y1", 0).attr("y2", 0);
} else if (shape === "path") {
shapes.attr("d", path);
}
},
d3_addText: function (svg, enter, labels, classPrefix, labelWidth){
enter.append("text").attr("class", classPrefix + "label");
const text = svg.selectAll(`g.${classPrefix}cell text.${classPrefix}label`)
.data(labels)
.text(d3_identity);
if (labelWidth){
svg.selectAll(`g.${classPrefix}cell text.${classPrefix}label`)
.call(d3_textWrapping, labelWidth)
}
return text
},
d3_calcType: function (scale, ascending, cells, labels, labelFormat, labelDelimiter){
const type = scale.invertExtent ?
d3_quantLegend(scale, labelFormat, labelDelimiter) : scale.ticks ?
d3_linearLegend(scale, cells, labelFormat) : d3_ordinalLegend(scale);
//for d3.scaleSequential that doesn't have a range function
const range = scale.range && scale.range() || scale.domain()
type.labels = d3_mergeLabels(type.labels, labels, scale.domain(), range);
if (ascending) {
type.labels = d3_reverse(type.labels);
type.data = d3_reverse(type.data);
}
return type;
},
d3_filterCells: (type, cellFilter) => {
let filterCells = type.data.map((d, i) => ({ data: d, label: type.labels[i] }))
.filter(cellFilter)
const dataValues = filterCells.map(d => d.data)
const labelValues = filterCells.map(d => d.label)
type.data = type.data.filter(d => dataValues.indexOf(d) !== -1)
type.labels = type.labels.filter(d => labelValues.indexOf(d) !== -1)
return type
},
d3_placement: (orient, cell, cellTrans, text, textTrans, labelAlign) => {
cell.attr("transform", cellTrans);
text.attr("transform", textTrans);
if (orient === "horizontal"){
text.style("text-anchor", labelAlign);
}
},
d3_addEvents: function(cells, dispatcher){
cells.on("mouseover.legend", function (d) { d3_cellOver(dispatcher, d, this); })
.on("mouseout.legend", function (d) { d3_cellOut(dispatcher, d, this); })
.on("click.legend", function (d) { d3_cellClick(dispatcher, d, this); });
},
d3_title: (svg, title, classPrefix, titleWidth) => {
if (title !== ""){
const titleText = svg.selectAll('text.' + classPrefix + 'legendTitle');
titleText.data([title])
.enter()
.append('text')
.attr('class', classPrefix + 'legendTitle');
svg.selectAll('text.' + classPrefix + 'legendTitle')
.text(title)
if (titleWidth){
svg.selectAll('text.' + classPrefix + 'legendTitle')
.call(d3_textWrapping, titleWidth)
}
const cellsSvg = svg.select('.' + classPrefix + 'legendCells')
const yOffset = svg.select('.' + classPrefix + 'legendTitle').nodes()
.map(d => d.getBBox().height)[0],
xOffset = -cellsSvg.nodes().map(function(d) { return d.getBBox().x})[0];
cellsSvg.attr('transform', 'translate(' + xOffset + ',' + (yOffset) + ')');
}
},
d3_defaultLocale: {
format,
formatPrefix
},
d3_defaultFormatSpecifier: '.01f',
d3_defaultDelimiter: 'to'
}
.counties {
fill: none;
}
.states {
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
button{
display: block;
position: absolute;
top: 300px;
left: 400px;
width: 100px;
color: white;
background-color: green;
border: none;
padding: 10px;
font-size: 15px;
}