Skip to content

Instantly share code, notes, and snippets.

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

An example of d3.svg.ribbon.

Drag the circles to see the underlying single svg:path element re-interpolated based on the changing position of the points and their radii that are used to generate it. This is a different way of generating areas than d3.svg.area and might prove more suitable for some applications.

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(_xAccessor).y(_yAccessor).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 Example</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: 1000px;
width: 1000px;
}
</style>
<body>
<div id="viz">
<svg>
</svg>
</div>
</body>
<footer>
<script>
var points = [{x: 20, y:20, t: 10},{x:200,y:20, t: 5},{x:20,y:200, t: 15},{x:200,y:200, t: 2}];
ribbon = d3.svg.ribbon()
.x(function(d) {return d.x})
.y(function(d) {return d.y})
.r(function(d) {return d.t});
drag = d3.behavior.drag().on("drag", function (d) {
d.x = d3.event.x;
d.y = d3.event.y;
redraw();
})
d3.select("svg")
.append("path")
.style("fill", "lightblue")
.style("stroke", "blue")
.style("stroke-opacity", 0.75)
.style("stroke-width", "2px")
.attr("d", ribbon(points))
d3.select("svg")
.selectAll("circle")
.data(points)
.enter()
.append("circle")
.attr("r", 3)
.style("fill", "lightgreen")
.style("stroke", "blue")
.style("stroke-width", "2px")
.attr("cx", function (d) {return d.x})
.attr("cy", function (d) {return d.y})
.attr("r", function (d) {return d.t})
.style("stroke-opacity", 0.75)
.call(drag);
function redraw() {
d3.selectAll("circle")
.attr("cx", function (d) {return d.x})
.attr("cy", function (d) {return d.y});
d3.select("path")
.attr("d", ribbon(points));
}
</script>
</footer>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment