Last active
October 26, 2016 12:04
-
-
Save zross/d6433afbb55ddceb77f6 to your computer and use it in GitHub Desktop.
Animate path (with pollution values) on Leaflet map using D3
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
/*jshint browser:true, indent:2, globalstrict: true, laxcomma: true, laxbreak: true */ | |
/*global d3:true */ | |
/* | |
* colorlegend | |
* | |
* This script can be used to draw a color legend for a | |
* [d3.js scale](https://github.com/mbostock/d3/wiki/Scales) | |
* on a specified html div element. | |
* [d3.js](http://mbostock.github.com/d3/) is required. | |
* | |
*/ | |
'use strict'; | |
var colorlegend = function (target, scale, type, options) { | |
var scaleTypes = ['linear', 'quantile', 'ordinal'] | |
, found = false | |
, opts = options || {} | |
, boxWidth = opts.boxWidth || 20 // width of each box (int) | |
, boxHeight = opts.boxHeight || 20 // height of each box (int) | |
, title = opts.title || null // draw title (string) | |
, fill = opts.fill || false // fill the element (boolean) | |
, linearBoxes = opts.linearBoxes || 9 // number of boxes for linear scales (int) | |
, htmlElement = document.getElementById(target.substring(0, 1) === '#' ? target.substring(1, target.length) : target) // target container element - strip the prefix # | |
, w = htmlElement.offsetWidth // width of container element | |
, h = htmlElement.offsetHeight // height of container element | |
, colors = [] | |
, padding = [2, 4, 10, 4] // top, right, bottom, left | |
, boxSpacing = type === 'ordinal' ? 3 : 0 // spacing between boxes | |
, titlePadding = title ? 11 : 0 | |
, domain = scale.domain() | |
, range = scale.range() | |
, i = 0; | |
// check for valid input - 'quantize' not included | |
for (i = 0 ; i < scaleTypes.length ; i++) { | |
if (scaleTypes[i] === type) { | |
found = true; | |
break; | |
} | |
} | |
if (! found) | |
throw new Error('Scale type, ' + type + ', is not suported.'); | |
// setup the colors to use | |
if (type === 'quantile') { | |
colors = range; | |
} | |
else if (type === 'ordinal') { | |
for (i = 0 ; i < domain.length ; i++) { | |
colors[i] = range[i]; | |
} | |
} | |
else if (type === 'linear') { | |
var min = domain[0]; | |
var max = domain[domain.length - 1]; | |
for (i = 0; i < linearBoxes ; i++) { | |
colors[i] = scale(min + i * ((max - min) / linearBoxes)); | |
} | |
} | |
// check the width and height and adjust if necessary to fit in the element | |
// use the range if quantile | |
if (fill || w < (boxWidth + boxSpacing) * colors.length + padding[1] + padding[3]) { | |
boxWidth = (w - padding[1] - padding[3] - (boxSpacing * colors.length)) / colors.length; | |
} | |
if (fill || h < boxHeight + padding[0] + padding[2] + titlePadding) { | |
boxHeight = h - padding[0] - padding[2] - titlePadding; | |
} | |
// set up the legend graphics context | |
var legend = d3.select(target) | |
.append('svg') | |
.attr('width', w) | |
.attr('height', h) | |
.append('g') | |
.attr('class', 'colorlegend') | |
.attr('transform', 'translate(' + padding[3] + ',' + padding[0] + ')') | |
.style('font-size', '13px') | |
.style('fill', '#000') | |
.style('font-family', '"Trebuchet MS", Helvetica, sans-serif'); | |
var legendBoxes = legend.selectAll('g.legend') | |
.data(colors) | |
.enter().append('g'); | |
// value labels | |
legendBoxes.append('text') | |
.attr('class', 'colorlegend-labels') | |
.attr('dy', '.71em') | |
.attr('x', function (d, i) { | |
return i * (boxWidth + boxSpacing) + (type !== 'ordinal' ? (boxWidth / 2) : 0); | |
}) | |
.attr('y', function () { | |
return boxHeight + 2; | |
}) | |
.style('text-anchor', function () { | |
return type === 'ordinal' ? 'start' : 'middle'; | |
}) | |
.style('pointer-events', 'none') | |
.text(function (d, i) { | |
// show label for all ordinal values | |
if (type === 'ordinal') { | |
return domain[i]; | |
} | |
// show only the first and last for others | |
else { | |
if (i === 0) | |
return Math.round(domain[0]*100)/100; | |
if (i === colors.length - 1) | |
return Math.round(domain[domain.length - 1]*100)/100; | |
} | |
}); | |
// the colors, each color is drawn as a rectangle | |
legendBoxes.append('rect') | |
.attr('x', function (d, i) { | |
return i * (boxWidth + boxSpacing); | |
}) | |
.attr('width', boxWidth) | |
.attr('height', boxHeight) | |
.style('fill', function (d, i) { return colors[i]; }); | |
// show a title in center of legend (bottom) | |
if (title) { | |
legend.append('text') | |
.attr('class', 'colorlegend-title') | |
.attr('x', (colors.length * (boxWidth / 2))) | |
.attr('y', boxHeight + titlePadding) | |
.attr('dy', '1.2em') | |
.style('text-anchor', 'middle') | |
.style('pointer-events', 'none') | |
.text(title); | |
} | |
return this; | |
}; |
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> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css" /> | |
<link href='https://api.tiles.mapbox.com/mapbox.js/v1.6.4/mapbox.css' rel='stylesheet' /> | |
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script> | |
<script src="http://cdn.leafletjs.com/leaflet-0.7/leaflet.js"></script> | |
<script src='https://api.tiles.mapbox.com/mapbox.js/v1.6.4/mapbox.js'></script> | |
<script src='colorlegend.js'></script> | |
<style> | |
html, | |
body { | |
height: 100%; | |
width: 100%; | |
} | |
body { | |
margin: 0; | |
} | |
#map { | |
width: 650px; | |
height: 550px; | |
} | |
.travelMarker { | |
fill: grey; | |
opacity: 0.75; | |
} | |
.waypoints { | |
fill: black; | |
opacity: 0; | |
} | |
.food { | |
stroke: black; | |
fill: red; | |
} | |
.lineConnect { | |
fill: none; | |
stroke: black; | |
opacity: 0.25; | |
} | |
.locnames { | |
fill: black; | |
text-shadow: 1px 1px 1px #FFF, 3px 3px 5px #000; | |
font-weight: bold; | |
font-size: 13px; | |
} | |
#thedata { | |
position: absolute; | |
top: 365px; | |
left: 0px; | |
z-index: 10; | |
width: 220px; | |
height: 150px; | |
/* -webkit-border-radius: 30px; | |
-moz-border-radius: 30px; | |
border-radius: 30px;*/ | |
background: rgba(255, 255, 255, 1); | |
-webkit-box-shadow: #B3B3B3 4px 4px 4px; | |
-moz-box-shadow: #B3B3B3 4px 4px 4px; | |
box-shadow: #B3B3B3 4px 4px 4px; | |
} | |
.datacontainer { | |
padding-top: 10px; | |
padding-left: 15px; | |
} | |
.dataelement { | |
} | |
.legend { | |
width: 200px; | |
height: 60px; | |
margin: 10px; | |
opacity: 1 !important; | |
padding-left: 15px; | |
padding-top: 15px; | |
} | |
.datatitle { | |
font-weight: bold; | |
font-family: "Trebuchet MS", Helvetica, sans-serif; | |
padding-left: 10px; | |
opacity: 1; | |
} | |
.datavals { | |
padding-left: 50px; | |
text-align: left; | |
font-family: "Trebuchet MS", Helvetica, sans-serif; | |
} | |
#no2mean{ | |
padding-left: 19px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="demo"></div> | |
<div id="map"></div> | |
<div id="thedata"> | |
<div class="datacontainer"> | |
<div class="dataelement"> | |
<span class="datatitle">Average NO<sub>2</sub>:</span> | |
<span id="no2mean" class="datavals"></span> | |
</div> | |
<div class="dataelement"> | |
<span class="datatitle">Max NO<sub>2</sub>:</span> | |
<span id="no2max" class="datavals"></span> | |
</div> | |
</div> | |
<div id="legendpieces" class="legend"> | |
</div> | |
</div> | |
<script type="text/javascript"> | |
var tweenToggle = 0; | |
var totalTimeSeconds = 10; | |
var waypointSize = 3; | |
//satellite: spatial.ec4cfb76 | |
var mapboxTiles = L.tileLayer('https://{s}.tiles.mapbox.com/v3/examples.map-zr0njcqy/{z}/{x}/{y}.png', { | |
attribution: '<a href="http://www.mapbox.com/about/maps/" target="_blank">Terms & Feedback</a>', | |
opacity: 0.75 | |
}); | |
var map = L.map('map') | |
.addLayer(mapboxTiles) | |
.setView([40.757, -73.95], 13); | |
map.scrollWheelZoom.disable() | |
var svg = d3.select(map.getPanes().overlayPane).append("svg"); | |
var g = svg.append("g").attr("class", "leaflet-zoom-hide"); | |
d3.json("points_pollution_continuous.geojson", function(collection) { | |
var collectionFeatures = collection.features | |
featuresdata = collectionFeatures.filter(function(d) { | |
return d.properties.id == "route1" | |
}) | |
featuresdata.sort(function(obj1, obj2) { | |
return obj1.properties.time - obj2.properties.time | |
}) | |
var maxpollution = d3.max(featuresdata, function(d) { | |
return d.properties.no2 | |
}) | |
var meanpollution = d3.mean(featuresdata, function(d) { | |
return d.properties.no2 | |
}) | |
var minpollution = d3.min(featuresdata, function(d) { | |
return d.properties.no2 | |
}) | |
// create, color, projection and toLine functions | |
var colorScale = d3.scale.linear() | |
.domain([minpollution, meanpollution, maxpollution]) | |
.range(["blue", "white", "red"]); | |
var transform = d3.geo.transform({ | |
point: projectPoint | |
}); | |
var d3path = d3.geo.path().projection(transform); | |
var toLine = d3.svg.line() | |
.interpolate("linear") | |
.x(function(d) { | |
return applyLatLngToLayer(d).x | |
}) | |
.y(function(d) { | |
return applyLatLngToLayer(d).y | |
}); | |
// here is our stuff - lines and points | |
var linePath = g.selectAll("path") | |
.data([featuresdata]) | |
.enter() | |
.append("path") | |
.attr("class", "lineConnect"); | |
var marker = g.append("circle") | |
.attr("r", 20) | |
.attr("id", "marker") | |
.attr("class", "travelMarker"); | |
var ptFeatures = g.selectAll("circle") | |
.data(featuresdata) | |
.enter() | |
.append("circle") | |
.attr("r", waypointSize) | |
.attr("class", function(d) { | |
return "waypoints " + "c" + d.properties.time | |
}) | |
.style("fill", function(d) { | |
return colorScale(d.properties.no2) | |
}); | |
var originANDdestination = [featuresdata[0], featuresdata[featuresdata.length - 1]] | |
var begend = g.selectAll(".food") | |
.data(originANDdestination) | |
.enter() | |
.append("circle", ".food") | |
.attr("r", 5) | |
.style("fill", "black") | |
.style("opacity", "1"); | |
var text = g.selectAll("text") | |
.data(originANDdestination) | |
.enter() | |
.append("text") | |
.text(function(d, i) { | |
var tmp = "Lomzynianka Pierogies" | |
if (i != 1) { | |
tmp = "Jacob's Pickles" | |
} | |
return tmp | |
}) | |
.attr("class", "locnames") | |
.attr("y", function(d) { | |
return -10 | |
}); | |
map.on("viewreset", reset); | |
reset(); | |
colorlegend("#legendpieces", colorScale, | |
"linear", { | |
title: "Nitrogen Dioxide (ppb)", | |
boxHeight: 20, | |
boxWidth: 23, | |
linearBoxes: 7 | |
}); | |
// functions | |
function reset() { | |
ptFeatures.attr("transform", | |
function(d) { | |
return "translate(" + | |
applyLatLngToLayer(d).x + "," + | |
applyLatLngToLayer(d).y + ")"; | |
}) | |
var bounds = d3path.bounds(collection), | |
topLeft = bounds[0], | |
bottomRight = bounds[1]; | |
text.attr("transform", | |
function(d) { | |
return "translate(" + | |
applyLatLngToLayer(d).x + "," + | |
applyLatLngToLayer(d).y + ")"; | |
}); | |
begend.attr("transform", | |
function(d) { | |
return "translate(" + | |
applyLatLngToLayer(d).x + "," + | |
applyLatLngToLayer(d).y + ")"; | |
}); | |
marker.attr("transform", | |
function() { | |
var y = featuresdata[0].geometry.coordinates[1] | |
var x = featuresdata[0].geometry.coordinates[0] | |
return "translate(" + | |
map.latLngToLayerPoint(new L.LatLng(y, x)).x + "," + | |
map.latLngToLayerPoint(new L.LatLng(y, x)).y + ")"; | |
}); | |
// Setting the size and location of the overall SVG container | |
svg.attr("width", bottomRight[0] - topLeft[0] + 120) | |
.attr("height", bottomRight[1] - topLeft[1] + 120) | |
.style("left", topLeft[0] - 50 + "px") | |
.style("top", topLeft[1] - 50 + "px"); | |
linePath.attr("d", toLine) | |
g.attr("transform", "translate(" + (-topLeft[0] + 50) + "," + (-topLeft[1] + 50) + ")"); | |
transition(); | |
} // end reset | |
/////////////////////////////////////////////////////////////// | |
function transition(path) { | |
ptFeatures | |
.transition() | |
.style("opacity", 0); | |
var l = linePath.node().getTotalLength(); | |
segments = [0]; | |
for (var i = 1; i < featuresdata.length; i++) { | |
var tmp = svg.append("path") | |
.datum([featuresdata[i - 1], featuresdata[i]]) | |
.attr("d", toLine); | |
segments.push(segments[i - 1] + tmp.node().getTotalLength()); | |
tmp.remove(); | |
} | |
ptFeatures.transition() | |
.delay(function(d, i) { | |
return (totalTimeSeconds * 1000 * segments[i] / l) - 0 | |
}) | |
.style("opacity", 1); | |
linePath.transition() | |
.duration(totalTimeSeconds * 1000) | |
.ease("linear") | |
.attrTween("stroke-dasharray", tweenDash) | |
.each("end", function() { | |
d3.select(this).call(transition); | |
}); | |
} //end transition | |
/////////////////////////////////////////////////////////////// | |
function tweenDash() { | |
return function(t) { | |
var l = linePath.node().getTotalLength(); | |
//http://jsfiddle.net/Y62Hq/2/ | |
interpolate = d3.interpolateString("0," + l, l + "," + l); | |
var marker = d3.select("#marker"); | |
var p = linePath.node().getPointAtLength(t * l); | |
//enable code for auto re-center | |
// if (tweenToggle == 0) { | |
// tweenToggle = 1; | |
// var newCenter = map.layerPointToLatLng(new L.Point(p.x, p.y)); | |
// map.panTo(newCenter, 14); | |
// } else { | |
// tweenToggle = 0; | |
// } | |
marker.attr("transform", "translate(" + p.x + "," + p.y + ")"); //move marker | |
// where on the line are we | |
var currentloc = parseFloat(interpolate(t).split(",")[0]) | |
var seq = []; | |
for (var i = 0; i != segments.length - 1; ++i) seq.push(i) | |
sumval = seq.filter(function(d, i) { | |
return segments[i] < currentloc | |
}); | |
var no2valsSoFar = featuresdata.slice(0, d3.max(sumval)) | |
var meanNO2 = Math.round(d3.mean(no2valsSoFar, function(d) { | |
return d.properties.no2 | |
}) * 100) / 100; | |
var maxNO2 = Math.round(d3.max(no2valsSoFar, function(d) { | |
return d.properties.no2 | |
}) * 100) / 100; | |
// console.log(no2valsSoFar.length) | |
d3.select('#no2mean').text(meanNO2.toFixed(2)); | |
d3.select('#no2max').text(maxNO2.toFixed(2)); | |
return interpolate(t); | |
} | |
} //end tweenDash | |
function projectPoint(x, y) { | |
var point = map.latLngToLayerPoint(new L.LatLng(y, x)); | |
this.stream.point(point.x, point.y); | |
} //end projectPoint | |
}); | |
// similar to projectPoint this function converts lat/long to | |
// svg coordinates except that it accepts a point from our | |
// GeoJSON | |
function applyLatLngToLayer(d) { | |
var y = d.geometry.coordinates[1] | |
var x = d.geometry.coordinates[0] | |
return map.latLngToLayerPoint(new L.LatLng(y, x)) | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment