Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active March 16, 2016 15:48
Show Gist options
  • Save emeeks/a0b5a95c999628547494 to your computer and use it in GitHub Desktop.
Save emeeks/a0b5a95c999628547494 to your computer and use it in GitHub Desktop.
Napoleon's March with d3.svg.ribbon

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)});
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;
};
}
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment