Skip to content

Instantly share code, notes, and snippets.

@renecnielsen
Created September 8, 2015 07:51
Show Gist options
  • Save renecnielsen/d44783b9a0a12f3ccc3a to your computer and use it in GitHub Desktop.
Save renecnielsen/d44783b9a0a12f3ccc3a 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)});

forked from emeeks's block: Napoleon's March with d3.svg.ribbon

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="http://d3js.org/d3.v3.min.js" 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