Built with blockbuilder.org
Last active
August 16, 2019 21:15
-
-
Save arnaudsj/52f12983aa5acd7965138e577723f4c8 to your computer and use it in GitHub Desktop.
fresh block
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v5.js"></script> | |
<script src="https://d3js.org/d3-selection.v1.min.js"></script> | |
<script src="https://d3js.org/d3-array.v1.min.js"></script> | |
<script src="https://d3js.org/d3-geo.v1.min.js"></script> | |
<style> | |
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } | |
</style> | |
</head> | |
<body> | |
<script> | |
// // Feel free to change or delete any of the code you see in this editor! | |
var svg = d3.select("body").append("svg") | |
.attr("width", 960) | |
.attr("height", 500) | |
// svg.append("text") | |
// .text("Edit the code below to change me!") | |
// .attr("y", 200) | |
// .attr("x", 120) | |
// .attr("font-size", 36) | |
// .attr("font-family", "monospace") | |
var width = 960, | |
height = 600; | |
var projection = d3.geo.albersUsa() | |
.scale(1100) | |
.translate([width / 2, height / 2]); | |
var path = d3.geo.path() | |
.projection(projection); | |
var svg = d3.select("#map-container").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
var g = svg.append("g"); | |
g.append( "rect" ) | |
.attr("width",width) | |
.attr("height",height) | |
.attr("fill","white") | |
.attr("opacity",0) | |
.on("mouseover",function(){ | |
hoverData = null; | |
if ( probe ) probe.style("display","none"); | |
}) | |
var map = g.append("g") | |
.attr("id","map"); | |
var probe, | |
hoverData; | |
var dateScale, sliderScale, slider; | |
var format = d3.format(","); | |
var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"], | |
months_full = ["January","February","March","April","May","June","July","August","September","October","November","December"], | |
orderedColumns = [], | |
currentFrame = 0, | |
interval, | |
frameLength = 500, | |
isPlaying = false; | |
var sliderMargin = 65; | |
function circleSize(d){ | |
return Math.sqrt( .02 * Math.abs(d) ); | |
}; | |
d3.json("json/states.json", function(error, us) { | |
map.selectAll("path") | |
.data(topojson.feature(us, us.objects.states).features) | |
.enter() | |
.append("path") | |
.attr("vector-effect","non-scaling-stroke") | |
.attr("class","land") | |
.attr("d", path); | |
map.append("path") | |
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; })) | |
.attr("class", "state-boundary") | |
.attr("vector-effect","non-scaling-stroke") | |
.attr("d", path); | |
probe = d3.select("#map-container").append("div") | |
.attr("id","probe"); | |
d3.select("body") | |
.append("div") | |
.attr("id","loader") | |
.style("top",d3.select("#play").node().offsetTop + "px") | |
.style("height",d3.select("#date").node().offsetHeight + d3.select("#map-container").node().offsetHeight + "px") | |
d3.csv("csv/jobs.csv",function(data){ | |
var first = data[0]; | |
// get columns | |
for ( var mug in first ){ | |
if ( mug != "CITY" && mug != "LAT" && mug != "LON" ){ | |
orderedColumns.push(mug); | |
} | |
} | |
orderedColumns.sort( sortColumns ); | |
// draw city points | |
for ( var i in data ){ | |
var projected = projection([ parseFloat(data[i].LON), parseFloat(data[i].LAT) ]) | |
map.append("circle") | |
.datum( data[i] ) | |
.attr("cx",projected[0]) | |
.attr("cy",projected[1]) | |
.attr("r",1) | |
.attr("vector-effect","non-scaling-stroke") | |
.on("mousemove",function(d){ | |
hoverData = d; | |
setProbeContent(d); | |
probe | |
.style( { | |
"display" : "block", | |
"top" : (d3.event.pageY - 80) + "px", | |
"left" : (d3.event.pageX + 10) + "px" | |
}) | |
}) | |
.on("mouseout",function(){ | |
hoverData = null; | |
probe.style("display","none"); | |
}) | |
} | |
createLegend(); | |
dateScale = createDateScale(orderedColumns).range([0,500]); | |
createSlider(); | |
d3.select("#play") | |
.attr("title","Play animation") | |
.on("click",function(){ | |
if ( !isPlaying ){ | |
isPlaying = true; | |
d3.select(this).classed("pause",true).attr("title","Pause animation"); | |
animate(); | |
} else { | |
isPlaying = false; | |
d3.select(this).classed("pause",false).attr("title","Play animation"); | |
clearInterval( interval ); | |
} | |
}); | |
drawMonth( orderedColumns[currentFrame] ); // initial map | |
window.onresize = resize; | |
resize(); | |
d3.select("#loader").remove(); | |
}) | |
}); | |
function drawMonth(m,tween){ | |
var circle = map.selectAll("circle") | |
.sort(function(a,b){ | |
// catch nulls, and sort circles by size (smallest on top) | |
if ( isNaN(a[m]) ) a[m] = 0; | |
if ( isNaN(b[m]) ) b[m] = 0; | |
return Math.abs(b[m]) - Math.abs(a[m]); | |
}) | |
.attr("class",function(d){ | |
return d[m] > 0 ? "gain" : "loss"; | |
}) | |
if ( tween ){ | |
circle | |
.transition() | |
.ease("linear") | |
.duration(frameLength) | |
.attr("r",function(d){ | |
return circleSize(d[m]) | |
}); | |
} else { | |
circle.attr("r",function(d){ | |
return circleSize(d[m]) | |
}); | |
} | |
d3.select("#date p#month").html( monthLabel(m) ); | |
if (hoverData){ | |
setProbeContent(hoverData); | |
} | |
} | |
function animate(){ | |
interval = setInterval( function(){ | |
currentFrame++; | |
if ( currentFrame == orderedColumns.length ) currentFrame = 0; | |
d3.select("#slider-div .d3-slider-handle") | |
.style("left", 100*currentFrame/orderedColumns.length + "%" ); | |
slider.value(currentFrame) | |
drawMonth( orderedColumns[ currentFrame ], true ); | |
if ( currentFrame == orderedColumns.length - 1 ){ | |
isPlaying = false; | |
d3.select("#play").classed("pause",false).attr("title","Play animation"); | |
clearInterval( interval ); | |
return; | |
} | |
},frameLength); | |
} | |
function createSlider(){ | |
sliderScale = d3.scale.linear().domain([0,orderedColumns.length-1]); | |
var val = slider ? slider.value() : 0; | |
slider = d3.slider() | |
.scale( sliderScale ) | |
.on("slide",function(event,value){ | |
if ( isPlaying ){ | |
clearInterval(interval); | |
} | |
currentFrame = value; | |
drawMonth( orderedColumns[value], d3.event.type != "drag" ); | |
}) | |
.on("slideend",function(){ | |
if ( isPlaying ) animate(); | |
d3.select("#slider-div").on("mousemove",sliderProbe) | |
}) | |
.on("slidestart",function(){ | |
d3.select("#slider-div").on("mousemove",null) | |
}) | |
.value(val); | |
d3.select("#slider-div").remove(); | |
d3.select("#slider-container") | |
.append("div") | |
.attr("id","slider-div") | |
.style("width",dateScale.range()[1] + "px") | |
.on("mousemove",sliderProbe) | |
.on("mouseout",function(){ | |
d3.select("#slider-probe").style("display","none"); | |
}) | |
.call( slider ); | |
d3.select("#slider-div a").on("mousemove",function(){ | |
d3.event.stopPropagation(); | |
}) | |
var sliderAxis = d3.svg.axis() | |
.scale( dateScale ) | |
.tickValues( dateScale.ticks(orderedColumns.length).filter(function(d,i){ | |
// ticks only for beginning of each year, plus first and last | |
return d.getMonth() == 0 || i == 0 || i == orderedColumns.length-1; | |
})) | |
.tickFormat(function(d){ | |
// abbreviated year for most, full month/year for the ends | |
if ( d.getMonth() == 0 ) return "'" + d.getFullYear().toString().substr(2); | |
return months[d.getMonth()] + " " + d.getFullYear(); | |
}) | |
.tickSize(10) | |
d3.select("#axis").remove(); | |
d3.select("#slider-container") | |
.append("svg") | |
.attr("id","axis") | |
.attr("width",dateScale.range()[1] + sliderMargin*2 ) | |
.attr("height",25) | |
.append("g") | |
.attr("transform","translate(" + (sliderMargin+1) + ",0)") | |
.call(sliderAxis); | |
d3.select("#axis > g g:first-child text").attr("text-anchor","end").style("text-anchor","end"); | |
d3.select("#axis > g g:last-of-type text").attr("text-anchor","start").style("text-anchor","start"); | |
} | |
function createLegend(){ | |
var legend = g.append("g").attr("id","legend").attr("transform","translate(560,10)"); | |
legend.append("circle").attr("class","gain").attr("r",5).attr("cx",5).attr("cy",10) | |
legend.append("circle").attr("class","loss").attr("r",5).attr("cx",5).attr("cy",30) | |
legend.append("text").text("jobs gained").attr("x",15).attr("y",13); | |
legend.append("text").text("jobs lost").attr("x",15).attr("y",33); | |
var sizes = [ 10000, 100000, 250000 ]; | |
for ( var i in sizes ){ | |
legend.append("circle") | |
.attr( "r", circleSize( sizes[i] ) ) | |
.attr( "cx", 80 + circleSize( sizes[sizes.length-1] ) ) | |
.attr( "cy", 2 * circleSize( sizes[sizes.length-1] ) - circleSize( sizes[i] ) ) | |
.attr("vector-effect","non-scaling-stroke"); | |
legend.append("text") | |
.text( (sizes[i] / 1000) + "K" + (i == sizes.length-1 ? " jobs" : "") ) | |
.attr( "text-anchor", "middle" ) | |
.attr( "x", 80 + circleSize( sizes[sizes.length-1] ) ) | |
.attr( "y", 2 * ( circleSize( sizes[sizes.length-1] ) - circleSize( sizes[i] ) ) + 5 ) | |
.attr( "dy", 13) | |
} | |
} | |
function setProbeContent(d){ | |
var val = d[ orderedColumns[ currentFrame ] ], | |
m_y = getMonthYear( orderedColumns[ currentFrame ] ), | |
month = months_full[ months.indexOf(m_y[0]) ]; | |
var html = "<strong>" + d.CITY + "</strong><br/>" + | |
format( Math.abs( val ) ) + " jobs " + ( val < 0 ? "lost" : "gained" ) + "<br/>" + | |
"<span>" + month + " " + m_y[1] + "</span>"; | |
probe | |
.html( html ); | |
} | |
function sliderProbe(){ | |
var d = dateScale.invert( ( d3.mouse(this)[0] ) ); | |
d3.select("#slider-probe") | |
.style( "left", d3.mouse(this)[0] + sliderMargin + "px" ) | |
.style("display","block") | |
.select("p") | |
.html( months[d.getMonth()] + " " + d.getFullYear() ) | |
} | |
function resize(){ | |
var w = d3.select("#container").node().offsetWidth, | |
h = window.innerHeight - 80; | |
var scale = Math.max( 1, Math.min( w/width, h/height ) ); | |
svg | |
.attr("width",width*scale) | |
.attr("height",height*scale); | |
g.attr("transform","scale(" + scale + "," + scale + ")"); | |
d3.select("#map-container").style("width",width*scale + "px"); | |
dateScale.range([0,500 + w-width]); | |
createSlider(); | |
} | |
function sortColumns(a,b){ | |
// [month,year] | |
var monthA = a.split("-"), | |
monthB = b.split("-"); | |
// Y2K !!! | |
// 99 becomes 9; 2000+ becomes 11+ | |
if ( monthA[1] < 90 ) monthA[1] = parseInt(monthA[1]) + 11; | |
else monthA[1] = parseInt(monthA[1]) - 90; | |
if ( monthB[1] < 90 ) monthB[1] = parseInt(monthB[1]) + 11; | |
else monthB[1] = parseInt(monthB[1]) - 90; | |
// turn year+month into a sortable number | |
return ( 100*parseInt(monthA[1]) + months.indexOf(monthA[0]) ) - ( 100*parseInt(monthB[1]) + months.indexOf(monthB[0]) ); | |
} | |
function createDateScale( columns ){ | |
var start = getMonthYear( columns[0] ), | |
end = getMonthYear( columns[ columns.length-1 ] ); | |
return d3.time.scale() | |
.domain( [ new Date( start[1], months.indexOf( start[0] ) ), new Date( end[1], months.indexOf( end[0] ) ) ] ); | |
} | |
function getMonthYear(column){ | |
var m_y = column.split("-"); | |
var year = parseInt( m_y[1] ); | |
if ( year > 90 ) year += 1900; | |
else year += 2000; | |
return [ m_y[0], year ]; | |
} | |
function monthLabel( m ){ | |
var m_y = getMonthYear(m); | |
return "<span>" + m_y[0].toUpperCase() + "</span> " + m_y[1]; | |
} | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment