Use D3 to gradually reveal points on a Leaflet web map

In a previous post we demonstrated how to animate a path on a Leaflet.js map using the amazing D3 JavaScript library. In this post we will take that code one step further — instead of a path our animation will reveal a series of points representing air pollution measurements through time. The final map looks like this (note the low pollution through Central Park):

Here is what the finished product looks like:

1) More than just a path

In our previous post we outlined the steps for creating an animated path from point A to point B using D3 on a Leaflet.js map. That was the first step in a bit of a proof-of-concept. What we truly want is a way to visualize changes in air pollution levels as someone travels through New York City.

Since 2007 we have been part of a team that helped to develop, implement and analyze data from one of the world’s largest urban air monitoring networks. As part of this work, we worked with the team to develop smooth surfaces of estimated air pollutant levels. The surfaces look like this:

no2_vals

The goal, then, was to create a visualization of air pollutant levels as a bicyclist or other commuter travels through the City. In this case we use a hypothetical hungry commuter who starts at the delicious Jacob’s Pickles on the Upper West Side and then travels to Lomzynianka in Brooklyn for pierogies.

food

If you would like further background on the air monitoring project, part of the New York City Community Air Survey, you can read some of the reports on the New York City Department of Health and Mental Hygiene website.

2) Create equally-spaced points along our route

We have a blog post devoted to extracting geographic coordinates from a Google directions. This post gets us much of the way to having equally spaced points along our route. In particular, we use these steps:

  1. Submit path to Google Maps API. In this case we’re using a walking route to show us the path a bicycle might take.
  2. Extract the ‘overview_polyline’ from the Google output.
  3. Use the node.js package called polyline (from MapBox) to convert the overview_polyline to a set of points.
  4. My hack was to use R to reformat the output from polyline (there is definitely a better way).

This gives us a table of points along our route, but not equally-spaced points throughout our route. To get this we add two steps that were not included in the directions on our previous blog post:

  1. Use the QGIS plugin points2One to convert the points to a line.
  2. Use the QGIS plugin QChainage to convert the line to evenly-spaced points. Since my data was in decimal degrees I created a point each 0.0005 (approximately equivalent to 55 meters)
  3. Use the QGIS plugin NNJoin to do a spatial join from the points on the path to the lattice of pollution points.

3) Create the map and path animation (using code from our previous post)

Following the code in our previous post you can create a animated map with a marker moving from an origin to a destination.

4) Compute the cumulative path length at each point

With our initial animation we animated along a path gradually revealing the path by gradually lengthening the line. This is relatively simple because we only need to know the time and the total length of the path.

The way we plan to animate the points requires a bit more information. We will animate the points by revealing each one on a delay (essentially a timer). We take the total animation time and split this up into bits based on the distance between the points and gradually reveal the points. To do this we need to know not only the time and the total length of the path but we need to know the length between the points along the path or, more precisely, the cumulative length at each point.

In order to compute the cumulative length at each point we added code that cycles through the points, adds two adjacent points to a temporary SVG path using D3. Then we use the SVG method getTotalLength to compute the length between the two points. Finally the cumulative length is the sum of the previous cumulative length (at point 1 this is 0) and the length between the points:

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();
}

5) Use the cumulative lengths to generate our timer for point reveal

We use D3's transition.delay([delay]) function to assign a delay that is proportional to the cumulative distance at each point. At the end of a point's delay we change the opacity for that one point from 0 (transparent) to 1. The formula for this is:

Total Time * (Cumulative Length at Point i)/(Total Path Length)

Total time in our case is specified as a total time in seconds (totalTimeSeconds) * 1000 because the delay in D3 is in milliseconds. This extra piece of code looks like:

ptFeatures.transition()
    .delay(function(d, i) {
        return (totalTimeSeconds * 1000 * segments[i] / l) - 0
    })
    .style("opacity", 1);

6) Compute the mean and max pollution values

We added code the compute an animated ticker of mean and maximum pollution values that we put in the legend. With a little more care we could do this computation more efficiently but basically for each time (t) we compute where we are along the line (currentloc). We then use this to filter the segment indices and apply D3's mean and max functions to extract the required statistic and send it to the legend with jQuery.

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));

7) Add a legend

For the most part adding the legend is straightforward HTML/CSS. The one exception is adding the color bar. For this I took advantage of John Goodall's colorlegend.js script which is designed to add a legend using a D3 scale.

To use this I first created a color scale:

var colorScale = d3.scale.linear()
    .domain([minpollution, meanpollution, maxpollution])
    .range(["blue", "white", "red"]);

And then I use the colorLegend function

colorlegend("#legendpieces", colorScale,
    "linear", {
        title: "Nitrogen Dioxide (ppb)",
        boxHeight: 20,
        boxWidth: 23,
        linearBoxes: 7
    });

And we're done!

Here is what the finished product looks like:

2 responses

  1. And, finally (I think!), maybe is possible to do the same color change for “points” (your little circles) with your “lines” (the animated path of your post). Only can’t see where I can change the code for this. Best Regards and keep your very good posts. 😉

    • Tiles are now showing, thanks for reporting this. I was using an iFrame directed at the bl.ocks site but apparently that site does not allow this anymore. I probably have a similar issue on some other posts. Thanks again.

Leave a Reply

Your email address will not be published. Required fields are marked *