## Saturday, January 18, 2014

### D3 map Styling tutorial III: Drawing animated paths

The example La Belle France, or the original La Bella Italia by Gregor Aisch, have a nice ferry sailing along a path to connect the islands with the continent.
Drawing a path in a map, and animating some icon on it can be a nice tool to show information about routes, storm tracks, and other dynamic situations.

This example shows how to draw the Haiyan typhoon track on the map drawn in the last post.

The working examples are here:
Creating a geopath
Animating an object on the path

### Getting the data

Both the base map data and the typhoon data are explained in the post  D3 map Styling tutorial I: Preparing the data

### Creating a geopath

First, how to draw the path line on a map. The working example is here.



.graticule {
fill: none;
stroke: #777;
stroke-opacity: .5;
stroke-width: .5px;
}

.land {
fill: #999;
}

.boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
}

svg .path {
fill: none;
stroke-opacity: .8;
stroke-dasharray: 3,2;
stroke: #f44;
}

var width = 600,
height = 500;

var projection = d3.geo.mercator()
.scale(5*(width + 1) / 2 / Math.PI)
.translate([width / 2, height / 2])
.rotate([-125, -15, 0])
.precision(.1);

var path = d3.geo.path()
.projection(projection);

var graticule = d3.geo.graticule();

var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);

svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("track.json", function(error, track) {
d3.json("/mbostock/raw/4090846/world-50m.json", function(error, world) {
svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);

svg.insert("path", ".graticule")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);

var pathLine = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return projection([d.lon, d.lat])[0]; })
.y(function(d) { return projection([d.lon, d.lat])[1]; });

var haiyanPath = svg.append("path")
.attr("d",pathLine(track))
.attr("class","path");

});
});

d3.select(self.frameElement).style("height", height + "px");


• The base map is drawn in the simplest way, as shown in this example, so the script stays clearer.
• The typhoon track is loaded from the json file generated in the first tutorial post (line 55)
• The path is created and inserted from lines 68 to 75:
• A d3.svg.line element is created. This will interpolate a line between the points. An other option is to draw segments from each point, so the line is not so smooth, but the actual points are more visible.
• The interpolate method sets the interpolation type to be used.
• x and y methods, set the svg coordinates to be used. In our case, we will transform the geographical coordinates using the same projection function set for the map. The coordinates transformation is done twice, one for the x and another for the y. It would be nice to do it only once.
• The path is added to the map, using the created d3.svg.line, passing the track object as a parameter to be used by the line function. The class is set to path, so is set to a dashed red line (line 20)
Drawing the paths is quite easy, taking only two steps.

### Animating an object on the path

The typhoon position for every day is shown on the path, with an icon. The icon size and color change with the typhoon class. The working example is here.


.graticule {
fill: none;
stroke: #777;
stroke-opacity: .5;
stroke-width: .5px;
}

.land {
fill: #999;
}

.boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
}

var width = 600,
height = 500;

var projection = d3.geo.mercator()
.scale(5*(width + 1) / 2 / Math.PI)
.translate([width / 2, height / 2])
.rotate([-125, -15, 0])
.precision(.1);

var path = d3.geo.path()
.projection(projection);

var graticule = d3.geo.graticule();

var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);

svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("track.json", function(error, track) {
d3.json("/mbostock/raw/4090846/world-50m.json", function(error, world) {
var color_scale = d3.scale.quantile().domain([1, 5]).range(colorbrewer.YlOrRd[5]);

var filter = svg.append("defs")
.append("filter")
.attr("height", "130%");
filter.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", 5)
.attr("result", "blur");

filter.append("feOffset")
.attr("in", "blur")
.attr("dx", 5)
.attr("dy", 5)
.attr("result", "offsetBlur");

var feMerge = filter.append("feMerge");

feMerge.append("feMergeNode")
.attr("in", "offsetBlur")
feMerge.append("feMergeNode")
.attr("in", "SourceGraphic");

svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path)

svg.insert("path", ".graticule")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);

var dateText = svg.append("text")
.attr("id", "dataTitle")
.text("2013/11/"+track[0].day + " " + track[0].hour + ":00 class: " + track[0].class)
.attr("x", 70)
.attr("y", 20)
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", color_scale(track[0].class));

var pathLine = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return projection([d.lon, d.lat])[0]; })
.y(function(d) { return projection([d.lon, d.lat])[1]; });

var haiyanPath = svg.append("path")
.attr("d",pathLine(track))
.attr("fill","none")
.attr("stroke", color_scale(track[0].class))
.attr("stroke-width", 3)

.style('stroke-dasharray', function(d) {
var l = d3.select(this).node().getTotalLength();
return l + 'px, ' + l + 'px';
})
.style('stroke-dashoffset', function(d) {
return d3.select(this).node().getTotalLength() + 'px';
});

var haiyanPathEl = haiyanPath.node();
var haiyanPathElLen = haiyanPathEl.getTotalLength();

var pt = haiyanPathEl.getPointAtLength(0);

var icon = svg.append("path")
.attr("d","m 20,-42 c -21.61358,0.19629 -34.308391,10.76213 -41.46346,18.0657 -7.155097,7.3036 -11.451337,17.59059 -11.599112,26.13277 0,14.45439 9.037059,26.79801 21.767213,31.69368 -14.965519,10.64929 -25.578236,6.78076 -37.671451,7.85549 C -4.429787,54.20699 14.03,37.263 23.12144,28.41572 32.2133,19.56854 34.6802,10.79063 34.82941,2.19847 c 0,-14.45219 -9.03405,-26.79679 -21.76113,-31.69364 14.90401,-10.54656 25.48889,-6.69889 37.55061,-7.77104 C 38.78869,-40.57565 29.11666,-41.95733 21.03853,-42 20.68954,-42.0105 20.34303,-42.0105 20,-42 z M 0.82306,-7.46851 c 4.72694,0 8.56186,4.27392 8.56186,9.54602 0,5.2725 -3.83492,9.54651 -8.56186,9.54651 -4.726719,0 -8.555958,-4.27401 -8.555958,-9.54651 0,-5.2721 3.829239,-9.54602 8.555958,-9.54602 z")
.attr("transform", "translate(" + pt.x + "," + pt.y + "), scale("+(0.15*track[0].class)+")")
.attr("fill", color_scale(track[0].class))
.attr("class","icon");

var i = 0;
var animation = setInterval(function(){
pt = haiyanPathEl.getPointAtLength(haiyanPathElLen*i/track.length);
icon
.transition()
.ease("linear")
.duration(1000)
.attr("transform", "translate(" + pt.x + "," + pt.y + "), scale("+(0.15*track[i].class)+"), rotate("+(i*15)+")")
.attr("fill", color_scale(track[i].class));

haiyanPath
.transition()
.duration(1000)
.ease("linear")
.attr("stroke", color_scale(track[i].class))
.style('stroke-dashoffset', function(d) {
var stroke_offset = (haiyanPathElLen - haiyanPathElLen*i/track.length + 9);
return (haiyanPathElLen < stroke_offset) ? haiyanPathElLen : stroke_offset + 'px';
});

dateText
.text("2013/11/"+track[i].day + " " + track[i].hour + ":00 class: " + track[i].class)
.attr("fill", color_scale(track[i].class));
i = i + 1;
if (i==track.length)
clearInterval(animation)

},1000);

});
});

d3.select(self.frameElement).style("height", height + "px");


This second example is more complex than the first one:
• The base map has a shadow effect. See the second part of the tutorial for the source.
• The map is animated:
• Line 135 sets an interval, so the icon and line can change with the date.
• A variable i is set, so the array elements are used in every interval.
• When the dates have ended, the interval is removed, so everything stays quiet. Line 158.
• An icon moves along the path indicating the position of the typhoon
•  Line 128 created the icon. First, I created it using inkscape, and with the xml editor that comes with it, I copied the path definition. This method can get really complex with bigger shapes.
• Line 136 finds the position of the typhoon. The length of the track is found at line 122 with the getTotalLength() method.
• Line 137 moves the icon. A transition is set, so the movement is continuous even thought the points are separated. The duration is the same as the interval, so when the icon has arrived at the final point, a new transformation starts to the next one.
• Line 141 has the transform operation that sets the position (translate), the size (scale) and rotation (rotate). The factors multiplying the scale and rotation are those only to adjust the size and rotation speed. They are completely arbitrary.
• The path gets filled when the icon has passed. I made this example to learn how to do it. Everything happens at line 144. Basically, the trick is creating a dashed line, and playing with the stroke-dashoffset attribute to set where the path has to arrive.
• The color of the path and icon change with the typhoon class
• At line 54, a color scale is created using the method d3.scale.quantile
• The colors are chosen with colorbrewer, which is a set of color scales for mapping, and has a handy javascript library to set the color scales just by choosing their name. I learned how to use it with this example by Mike Bostock.
• The lines 156 and 142 change the track and icon colours.
• Finally, at line 154, the date is changed, with the same color as the typhoon and track.