slider map of nyc auto smash-and-grabs
slider update and start/pause button based on officeofjane's timeline example
license: mit |
slider map of nyc auto smash-and-grabs
slider update and start/pause button based on officeofjane's timeline example
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width"> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
<script src="https://d3js.org/d3-time-format.v2.min.js"></script> | |
<title>nyc auto smash-and-grabs</title> | |
<style> | |
h1 { | |
font: 100 22px "Montserrat", sans-serif; | |
color: #969696; | |
} | |
h2 { | |
font: 100 16px "Montserrat", sans-serif; | |
color: #969696; | |
} | |
p, | |
.ticks { | |
font: 100 12px "Montserrat", sans-serif; | |
color: #969696; | |
} | |
.label { | |
font: 500 16px "Montserrat", sans-serif; | |
fill: #ddd; | |
} | |
.pct { | |
stroke-width: 0.2; | |
} | |
.track { | |
stroke: #000; | |
stroke-opacity: 0.3; | |
stroke-width: 10px; | |
stroke-linecap: round; | |
} | |
.track-inset { | |
stroke: #dcdcdc; | |
stroke-width: 8px; | |
stroke-linecap: round; | |
} | |
.track-overlay { | |
pointer-events: stroke; | |
stroke-width: 25px; | |
stroke: transparent; | |
cursor: crosshair; | |
} | |
.handle { | |
fill: #fff; | |
stroke: #000; | |
stroke-opacity: 0.5; | |
stroke-width: 1px; | |
} | |
#button { | |
position: absolute; | |
top: 140px; | |
left: 20px; | |
background: #5e81a3; | |
border: none; | |
color: white; | |
padding: 0px 5px; | |
text-align: center; | |
text-decoration: none; | |
display: inline-block; | |
font-size: 14px; | |
cursor: pointer; | |
width: 80px; | |
height: 30px; | |
} | |
#button:hover { | |
background-color: #00528b; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>NYC auto smash-and-grabs over the past year</h1> | |
<h2></h2> | |
<button id="button" style="vertical-align:middle"><span>start </span></button> | |
<div id="chart"> | |
</div> | |
<script> | |
var pctUrl = "https://data.cityofnewyork.us/resource/5rqd-h5ci.geojson"; | |
//use app token to avoid throttling:"https://data.cityofnewyork.us/resource/qgea-i56i.json?$$app_token=YOUR_TOKEN&$where=pd_cd IN('321.0','421.0') AND rpt_dt>'2019-3-31' AND crm_atpt_cptd_cd='COMPLETED' AND lat_lon IS NOT NULL&$limit=100000" | |
var histUrl = "https://data.cityofnewyork.us/resource/qgea-i56i.json?$where=pd_cd IN('321.0','421.0') AND rpt_dt>'2019-4-30' AND crm_atpt_cptd_cd='COMPLETED' AND lat_lon IS NOT NULL&$limit=100000"; | |
var ytdUrl = "https://data.cityofnewyork.us/resource/5uac-w243.json?$where=pd_cd IN('321.0','421.0')AND crm_atpt_cptd_cd='COMPLETED' AND lat_lon IS NOT NULL&$limit=100000"; | |
var formatDateYear = d3.timeFormat("%b %Y") | |
Promise.all([d3.json(pctUrl), d3.json(histUrl), d3.json(ytdUrl)]) | |
.then(function(allSets) { | |
var precincts = allSets[0]; | |
var histCrimes = allSets[1]; | |
var ytdCrimes = allSets[2]; | |
var combinedData = d3.merge([histCrimes, ytdCrimes]); | |
var crimeData = combinedData.map( | |
row => [{ | |
number: row.cmplnt_num, | |
reportDate: new Date(row.rpt_dt), | |
offenseCode: row.pd_cd, | |
offense: row.pd_desc, | |
lat: +Number(row.latitude), | |
long: +Number(row.longitude) | |
}]).flat().sort((a, b) => a.reportDate - b.reportDate);; | |
var startDate = crimeData.map(d => d.reportDate)[0], | |
endDate = crimeData.map(d => d.reportDate).reverse()[0]; | |
var margin = { | |
top: 0, | |
right: 10, | |
bottom: 5, | |
left: 25 | |
}, | |
width = 800 - margin.left - margin.right, | |
height = 650 - margin.top - margin.bottom; | |
//create top-level SVG | |
var svg = d3.select("#chart").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom); | |
var projection = d3.geoMercator() | |
.scale(60000) | |
.translate([width / 1.5, height]) | |
.center([-73.94, 40.50]); | |
//nypd pct outlines | |
var path = d3.geoPath() | |
.projection(projection); | |
var targetValue = width - 25, | |
inputValue = 0; | |
var x = d3.scaleTime() | |
.domain([startDate, endDate]) | |
.range([0, targetValue]) | |
.clamp(true); | |
var slider = svg.append("g") | |
.attr("class", "slider") | |
.attr("transform", "translate(" + margin.left + "," + height / 20 + ")"); | |
var startButton = d3.select("#button"), | |
moving = false; | |
slider.append("line") | |
.attr("class", "track") | |
.attr("x1", x.range()[0]) | |
.attr("x2", x.range()[1]) | |
.select(function() { | |
return this.parentNode.appendChild(this.cloneNode(true)); | |
}) | |
.attr("class", "track-inset") | |
.select(function() { | |
return this.parentNode.appendChild(this.cloneNode(true)); | |
}) | |
.attr("class", "track-overlay") | |
.call(d3.drag() | |
.on("start.interrupt", function() { slider.interrupt(); }) | |
.on("start drag", function() { | |
inputValue = d3.event.x; | |
update(x.invert(inputValue)); | |
}) | |
); | |
slider.insert("g", ".track-overlay") | |
.attr("class", "ticks") | |
.attr("transform", "translate(30," + margin.left + ")") | |
.selectAll("text") | |
.data(x.ticks(9)) | |
.enter() | |
.append("text") | |
.attr("x", x) | |
.attr("y", 10) | |
.attr("text-anchor", "middle") | |
.text(d => formatDateYear(d)); | |
startButton.on("click", function() { | |
var button = d3.select(this); | |
if (button.text() == "pause") { | |
moving = false; | |
clearInterval(timer); | |
button.text("start"); | |
} else { | |
moving = true; | |
timer = setInterval(step, 100); | |
button.text("pause"); | |
} | |
}) | |
function step() { | |
update(x.invert(inputValue)); | |
inputValue = inputValue + (targetValue / 11); | |
if (inputValue > targetValue) { | |
moving = false; | |
inputValue = 0; | |
clearInterval(timer); | |
playButton.text("start"); | |
} | |
} | |
var handle = slider.insert("circle", ".track-overlay") | |
.attr("class", "handle") | |
.attr("r", 9); | |
var label = slider.append("text") | |
.attr("class", "label") | |
.attr("text-anchor", "middle") | |
.text(formatDateYear(startDate)) | |
.attr("transform", "translate(0," + (-20) + ")"); | |
svg.selectAll(".pct") | |
.data(precincts.features) | |
.enter().append("path") | |
.attr("class", "pct") | |
.style("fill", "#ddd") | |
.style("stroke", "#969696") | |
.attr("d", path); | |
draw(crimeData, startDate); | |
function draw(data, date) { | |
svg.selectAll("locations").remove(); | |
var locations = svg.selectAll("locations") | |
.data(data) | |
.enter().append("circle") | |
.attr("cx", d => projection([d.long, d.lat])[0]) | |
.attr("cy", d => projection([d.long, d.lat])[1]) | |
.attr("r", 2); | |
locations.style("fill", function(d, i) { | |
var d = formatDateYear(d.reportDate); | |
if (d === formatDateYear(date)) { | |
this.parentElement.appendChild(this); | |
return "#ff0000"; | |
} else { | |
return "lightgrey"; | |
}; | |
}) | |
.style("stroke", "#9696") | |
.style("opacity", 0.8); | |
} | |
function update(h) { | |
handle.attr("cx", x(h)); | |
label.attr("x", x(h)) | |
.text(formatDateYear(h)); | |
draw(crimeData, h); | |
} | |
}); | |
</script> | |
</body> | |
<footer> | |
<p>data <a href="https://data.cityofnewyork.us/resource/qgea-i56i" style="color:#969;text-decoration:none">nyc open data </a> | |
created by <a href="https://ursulams.github.io/" style="color:#969;text-decoration:none">ursula kaczmarek</a></p> | |
</footer> | |
</html> |