An example of d3.svg.ribbon using Napoleon’s March through Russia made famous by Minard’s map. Dataset comes from Ben Schmidt’s use of it in his excellent d3.trail layout.
The pattern for using d3.svg.ribbon with geodata is simply to factor the projection into the accessors:
projection = d3.geo.equirectangular().translate([-3300,7800]).scale(8000);
sizeScale = d3.scale.linear().domain([6000,340000]).range([1,10])
ribbon = d3.svg.ribbon()
.x(function(d) {return projection([d.lon, d.lat])[0]})
.y(function(d) {return projection([d.lon, d.lat])[1]})
.r(function(d) {return sizeScale(d.size)});
<html>
<head>
<title>d3.svg.ribbon with Napoleon's March</title>
<meta charset="utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="d3.svg.ribbon.js" type="text/JavaScript"></script>
</head>
<style>
svg {
height: 2000px;
width: 2000px;
}
</style>
<body>
<div id="viz">
<svg>
</svg>
</div>
</body>
<footer>
<script>
colorScale = d3.scale.ordinal().range(["#96abb1", "#313746", "#b0909d", "#687a97", "#292014"])
army = [
/* Group 1 */
{lon:24.0, lat:54.9, size:340000, dir:1, group:1},
{lon:24.5, lat:55.0, size:340000, dir:1, group:1},
{lon:25.5, lat:54.6, size:340000, dir:1, group:1},
{lon:26.0, lat:54.7, size:320000, dir:1, group:1},
{lon:27.0, lat:54.8, size:300000, dir:1, group:1},
{lon:28.0, lat:54.9, size:280000, dir:1, group:1},
{lon:28.5, lat:55.0, size:240000, dir:1, group:1},
{lon:29.0, lat:55.1, size:210000, dir:1, group:1},
{lon:30.0, lat:55.2, size:180000, dir:1, group:1},
{lon:30.3, lat:55.3, size:175000, dir:1, group:1},
{lon:32.0, lat:54.8, size:145000, dir:1, group:1},
{lon:33.2, lat:54.9, size:140000, dir:1, group:1},
{lon:34.4, lat:55.5, size:127100, dir:1, group:1},
{lon:35.5, lat:55.4, size:100000, dir:1, group:1},
{lon:36.0, lat:55.5, size:100000, dir:1, group:1},
{lon:37.6, lat:55.8, size:100000, dir:1, group:1},
{lon:37.65, lat:55.65, size:100000, dir:-1, group:1},
{lon:37.45, lat:55.62, size:98000, dir:-1, group:1},
{lon:37.0, lat:55.0, size:97000, dir:-1, group:1},
{lon:36.8, lat:55.0, size:96000, dir:-1, group:1},
{lon:35.4, lat:55.3, size:87000, dir:-1, group:1},
{lon:34.3, lat:55.2, size:55000, dir:-1, group:1},
{lon:33.3, lat:54.8, size:37000, dir:-1, group:1},
{lon:32.0, lat:54.6, size:24000, dir:-1, group:1},
{lon:30.4, lat:54.4, size:20000, dir:-1, group:1},
{lon:29.2, lat:54.3, size:20000, dir:-1, group:1},
{lon:29.13, lat:54.29, size:50000, dir:-1, group:1}, /* joined by group 2 */
{lon:28.5, lat:54.2, size:50000, dir:-1, group:1},
{lon:28.3, lat:54.3, size:48000, dir:-1, group:1},
{lon:26.8, lat:54.3, size:12000, dir:-1, group:1},
{lon:26.8, lat:54.4, size:14000, dir:-1, group:1},
{lon:25.0, lat:54.4, size:8000, dir:-1, group:1},
{lon:24.4, lat:54.4, size:4000, dir:-1, group:1},
{lon:24.2, lat:54.4, size:4000, dir:-1, group:1},
{lon:24.1, lat:54.4, size:4000, dir:-1, group:1},
/* Group 2 */
{lon:24.0, lat:55.1, size:60000, dir:1, group:2},
{lon:24.5, lat:55.2, size:60000, dir:1, group:2},
{lon:25.5, lat:54.7, size:60000, dir:1, group:2},
{lon:26.6, lat:55.7, size:40000, dir:1, group:2},
{lon:27.4, lat:55.6, size:33000, dir:1, group:2},
{lon:28.7, lat:55.5, size:33000, dir:1, group:2},
{lon:28.7, lat:55.5, size:33000, dir:-1, group:2},
{lon:29.2, lat:54.29, size:30000, dir:-1, group:2},
/* Group 3 */
{lon:24.0, lat:55.2, size:22000, dir:1, group:3},
{lon:24.5, lat:55.3, size:22000, dir:1, group:3},
{lon:24.6, lat:55.8, size:6000, dir:1, group:3},
{lon:24.6, lat:55.8, size:6000, dir:-1, group:3},
{lon:24.2, lat:54.4, size:6000, dir:-1, group:3},
{lon:24.1, lat:54.4, size:6000, dir:-1, group:3}
];
projection = d3.geo.equirectangular().translate([-3300,7800]).scale(8000);
sizeScale = d3.scale.linear().domain([6000,340000]).range([1,10])
ribbon = d3.svg.ribbon()
.x(function(d) {return projection([d.lon, d.lat])[0]})
.y(function(d) {return projection([d.lon, d.lat])[1]})
.r(function(d) {return sizeScale(d.size)});
drag = d3.behavior.drag().on("drag", function (d) {
d.x = d3.event.x;
d.y = d3.event.y;
redraw();
});
var groupNest = d3.nest().key(function (d) {return d.group}).entries(army);
d3.select("svg")
.selectAll("path.minard")
.data(groupNest)
.enter()
.append("path")
.attr("class", "minard")
.style("fill", function (d) {return colorScale(d.key)})
.style("stroke", "gray")
.style("stroke-opacity", 0.15)
.style("stroke-width", "2px")
.attr("d", function (d) {return ribbon(d.values)})
</script>
</footer>
</html>
d3.svg.ribbon = function() {
var _lineConstructor = d3.svg.line();
var _xAccessor = function (d) {return d.x}
var _yAccessor = function (d) {return d.y}
var _rAccessor = function (d) {return d.r}
var _interpolator = "linear-closed";
function _ribbon(pathData) {
var bothPoints = buildRibbon(pathData);
return _lineConstructor.x(function (d) {return d.x}).y(function (d) {return d.y}).interpolate(_interpolator)(bothPoints);
}
_ribbon.x = function (_value) {
if (!arguments.length) return _xAccessor;
_xAccessor = _value;
return _ribbon;
}
_ribbon.y = function (_value) {
if (!arguments.length) return _yAccessor;
_yAccessor = _value;
return _ribbon;
}
_ribbon.r = function (_value) {
if (!arguments.length) return _rAccessor;
_rAccessor = _value;
return _ribbon;
}
_ribbon.interpolate = function(_value) {
if (!arguments.length) return _interpolator;
_interpolator = _value;
return _ribbon;
}
return _ribbon;
function offsetEdge(d) {
var diffX = _yAccessor(d.target) - _yAccessor(d.source);
var diffY = _xAccessor(d.target) - _xAccessor(d.source);
var angle0 = ( Math.atan2( diffY, diffX ) + ( Math.PI / 2 ) );
var angle1 = angle0 + ( Math.PI * 0.5 );
var angle2 = angle0 + ( Math.PI * 0.5 );
var x1 = _xAccessor(d.source) + (_rAccessor(d.source) * Math.cos(angle1));
var y1 = _yAccessor(d.source) - (_rAccessor(d.source) * Math.sin(angle1));
var x2 = _xAccessor(d.target) + (_rAccessor(d.target) * Math.cos(angle2));
var y2 = _yAccessor(d.target) - (_rAccessor(d.target) * Math.sin(angle2));
return {x1: x1, y1: y1, x2: x2, y2: y2}
}
function buildRibbon(points) {
var bothCode = [];
var x = 0;
var transformedPoints = {};
while (x < points.length) {
if (x !== points.length - 1) {
transformedPoints = offsetEdge({source: points[x], target: points[x + 1]});
var p1 = {x: transformedPoints.x1, y: transformedPoints.y1};
var p2 = {x: transformedPoints.x2, y: transformedPoints.y2};
bothCode.push(p1,p2);
if (bothCode.length > 3) {
var l = bothCode.length - 1;
var lineA = {a: bothCode[l - 3], b: bothCode[l - 2]};
var lineB = {a: bothCode[l - 1], b: bothCode[l]};
var intersect = findIntersect(lineA.a.x, lineA.a.y, lineA.b.x, lineA.b.y, lineB.a.x, lineB.a.y, lineB.b.x, lineB.b.y);
if (intersect.found == true) {
lineA.b.x = intersect.x;
lineA.b.y = intersect.y;
lineB.a.x = intersect.x;
lineB.a.y = intersect.y;
}
}
}
x++;
}
x--;
//Back
while (x >= 0) {
if (x !== 0) {
transformedPoints = offsetEdge({source: points[x], target: points[x - 1]});
var p1 = {x: transformedPoints.x1, y: transformedPoints.y1};
var p2 = {x: transformedPoints.x2, y: transformedPoints.y2};
bothCode.push(p1,p2);
if (bothCode.length > 3) {
var l = bothCode.length - 1;
var lineA = {a: bothCode[l - 3], b: bothCode[l - 2]};
var lineB = {a: bothCode[l - 1], b: bothCode[l]};
var intersect = findIntersect(lineA.a.x, lineA.a.y, lineA.b.x, lineA.b.y, lineB.a.x, lineB.a.y, lineB.b.x, lineB.b.y);
if (intersect.found == true) {
lineA.b.x = intersect.x;
lineA.b.y = intersect.y;
lineB.a.x = intersect.x;
lineB.a.y = intersect.y;
}
}
}
x--;
}
return bothCode;
}
function findIntersect(l1x1, l1y1, l1x2, l1y2, l2x1, l2y1, l2x2, l2y2) {
var d, a, b, n1, n2, result = {
x: null,
y: null,
found: false
};
d = ((l2y2 - l2y1) * (l1x2 - l1x1)) - ((l2x2 - l2x1) * (l1y2 - l1y1));
if (d == 0) {
return result;
}
a = l1y1 - l2y1;
b = l1x1 - l2x1;
n1 = ((l2x2 - l2x1) * a) - ((l2y2 - l2y1) * b);
n2 = ((l1x2 - l1x1) * a) - ((l1y2 - l1y1) * b);
a = n1 / d;
b = n2 / d;
result.x = l1x1 + (a * (l1x2 - l1x1));
result.y = l1y1 + (a * (l1y2 - l1y1));
if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
result.found = true;
}
return result;
};
}