. After some attempts, I decided to use the 1:10m data. The files are:
With that, the land, countries and regions are available. To merge them into a single TopoJson file, I used:
Since the code is quite long, and I think that I will made some more posts about specific parts of the technique, the comments are a bit shorter than usually.
To achieve the effects, some layers are loaded more than one time, so different filters can be applied to get the shades and blurred borders:
svg.selectAll(".bgback")
.data(topojson.feature(data, data.objects.land).features)
.enter()
.append("path")
.attr("class", "bgback")
.attr("d", path)
.style("filter","url(#oglow)")
.style("stroke", "#999")
.style("stroke-width", 0.2);
In this case, the land masses are drawn, applying the effect named oglow, which looks like:
var oglow = defs.append("filter")
.attr("id","oglow");
oglow.append("feColorMatrix")
.attr("in","SourceGraphic")
.attr("type", "matrix")
.attr("values", "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0")
.attr("result","mask");
oglow.append("feMorphology")
.attr("in","mask")
.attr("radius","1")
.attr("operator","dilate")
.attr("result","mask");
oglow.append("feColorMatrix")
.attr("in","mask")
.attr("type", "matrix")
.attr("values", "0 0 0 0 0.6 0 0 0 0 0.5333333333333333 0 0 0 0 0.5333333333333333 0 0 0 1 0")
.attr("result","r0");
oglow.append("feGaussianBlur")
.attr("in","r0")
.attr("stdDeviation","4")
.attr("result","r1");
oglow.append("feComposite")
.attr("operator","out")
.attr("in","r1")
.attr("in2","mask")
.attr("result","comp");
To see how svg filters work,
many pages are available. I got them looking at the
Kartograph example generated html.
Adding the labels
The labels aren't inside the TopoJSON (although they could be!), so I decided the labels to add and put them into an array:
var cities = [
{'pos': [2.351, 48.857], 'name': 'Paris'},
{'pos':[5.381, 43.293], 'name': 'Marseille'},
{'pos':[3.878, 43.609], 'name': 'Montpellier'},
{'pos':[4.856, 45.756], 'name': 'Lyon'},
{'pos':[1.436, 43.602], 'name': 'Toulouse'},
{'pos':[-0.566, 44.841], 'name': 'Bordeaux'},
{'pos':[-1.553, 47.212], 'name': 'Nantes'},
{'pos':[8.737, 41.925], 'name': 'Ajaccio'},
];
Then, adding them to the map is easy:
var city_labels =svg.selectAll(".city_label")
.data(cities)
.enter();
city_labels
.append("text")
.attr("class", "city_label")
.text(function(d){return d.name;})
.attr("font-family", "AquilineTwoRegular")
.attr("font-size", "18px")
.attr("fill", "#544")
.attr("x",function(d){return projection(d.pos)[0];})
.attr("y",function(d){return projection(d.pos)[1];});
city_labels
.append("circle")
.attr("r", 3)
.attr("fill", "black")
.attr("cx",function(d){return projection(d.pos)[0];})
.attr("cy",function(d){return projection(d.pos)[1];});
Note that the positions must be calculated transforming the longitude and latitude using the
d3js projection functions.
The ship
To draw the ship, tow things are necessary, the path and the ship.
To draw the path:
var ferry_path = [[8.745, 41.908],
[8.308, 41.453],
[5.559, 43.043],
[5.268, 43.187],
[5.306, 43.289]
];
var shipPathLine = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return projection(d)[0]; })
.y(function(d) { return projection(d)[1]; });
var shipPath = svg.append("path")
.attr("d",shipPathLine(ferry_path))
.attr("stroke","#000")
.attr("class","ferry_path");
Basically,
d3.svg.line is used to interpolate the points, making the line smoother. This is easier than the Kartograph way with
geopaths, where the Bézier control points have to be calculated. d3.svg.line is amazing, more than what I thought before.
I don't know if the way to calculate the projected points is the best one, since I do it twice for each point, which is ugly.
To move the ship, a ship image is appended, and then moved with a
setInterval:
var shipPathEl = shipPath.node();
var shipPathElLen = shipPathEl.getTotalLength();
var pt = shipPathEl.getPointAtLength(0);
var shipIcon = svg.append("image")
.attr("xlink:href","ship.png")
.attr("x", pt.x - 10)
.attr("y", pt.y - 5.5)
.attr("width", 15)
.attr("height", 8);
var i = 0;
var delta = 0.05;
var dist_ease = 0.2;
var delta_ease = 0.9;
setInterval(function(){
pt = shipPathEl.getPointAtLength(i*shipPathElLen);
shipIcon
.transition()
.ease("linear")
.duration(1000)
.attr("x", pt.x - 10)
.attr("y", pt.y - 5.5);
//i = i + delta;
if (i < dist_ease){
i = i + delta * ((1-delta_ease) + i*delta_ease/dist_ease);
}else if (i > 1 - dist_ease){
i = i + delta * (1 - ((i - (1 - dist_ease)) * (delta_ease/dist_ease)));
}else{
i = i + delta;
}
if (i+0.0001 >= 1 || i-0.0001 <= 0)
delta = -1 * delta;
},1000);
The ship position is calculated every second, and moved with a d3js transition to make it smooth (calculating everything more often didn't give this smooth effect)
The speed of the ship is changed depending on the proximity to the
harbour, to a void the strange effect of the ship crashing into it. The effect is controlled by
dist_ease and
delta_ease parameters, that change the distance where the speed is changed, and the amount of speed changed.