Skip to content

Instantly share code, notes, and snippets.

@zross
Last active October 26, 2016 12:04
Show Gist options
  • Save zross/d6433afbb55ddceb77f6 to your computer and use it in GitHub Desktop.
Save zross/d6433afbb55ddceb77f6 to your computer and use it in GitHub Desktop.
Animate path (with pollution values) on Leaflet map using D3
/*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;
};
<!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 &amp; 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>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment