Skip to content

Instantly share code, notes, and snippets.

@mforando
Last active March 10, 2020 13:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mforando/ca6e3aa62081f381b4bd9fa127df4868 to your computer and use it in GitHub Desktop.
Save mforando/ca6e3aa62081f381b4bd9fa127df4868 to your computer and use it in GitHub Desktop.
SwoopyLogic
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
.swoopyArrow{
fill:none;
stroke:black;
}
</style>
</head>
<svg>
<marker id="arrowhead" viewBox="-10 -10 20 20" refX="0" refY="0" markerWidth="20" markerHeight="20" stroke-width="1" orient="auto"><polyline stroke-linejoin="bevel" points="-6.75,-6.75 0,0 -6.75,6.75"></polyline></marker>
<circle id="targetCircle"></circle>
<circle id="annotationCircle"></circle>
<path id="swoop"></path>
</svg>
<script>
//https://github.com/bizweekgraphics/swoopyarrows/blob/master/swoopyArrow.js
function swoopyArrow() {
var angle = Math.PI,
clockwise = true,
xValue = function(d) { return d[0]; },
yValue = function(d) { return d[1]; };
function render(data) {
data = data.map(function(d, i) {
return [xValue.call(data, d, i), yValue.call(data, d, i)];
});
// get the chord length ("height" {h}) between points
var h = hypotenuse(data[1][0]-data[0][0], data[1][1]-data[0][1])
// get the distance at which chord of height h subtends {angle} radians
var d = h / ( 2 * Math.tan(angle / 2) );
// get the radius {r} of the circumscribed circle
var r = hypotenuse(d, h/2)
/*
SECOND, compose the corresponding SVG arc.
read up: http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
example: <path d = "M 200,50 a 50,50 0 0,1 100,0"/>
M 200,50 Moves pen to (200,50);
a draws elliptical arc;
50,50 following a degenerate ellipse, r1 == r2 == 50;
i.e. a circle of radius 50;
0 with no x-axis-rotation (irrelevant for circles);
0,1 with large-axis-flag=0 and sweep-flag=1 (clockwise);
100,0 to a point +100 in x and +0 in y, i.e. (300,50).
*/
var path = "M " + data[0][0] + "," + data[0][1]
+ " a " + r + "," + r
+ " 0 0," + (clockwise ? "1" : "0") + " "
+ (data[1][0]-data[0][0]) + "," + (data[1][1]-data[0][1]);
return path
}
function hypotenuse(a, b) {
return Math.sqrt( Math.pow(a,2) + Math.pow(b,2) );
}
render.angle = function(_) {
if (!arguments.length) return angle;
angle = Math.min(Math.max(_, 1e-6), Math.PI-1e-6);
return render;
};
render.clockwise = function(_) {
if (!arguments.length) return clockwise;
clockwise = !!_;
return render;
};
render.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return render;
};
render.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return render;
};
return render;
}
</script>
<body>
<script>
//https://github.com/bizweekgraphics/swoopyarrows
var svg = d3.select("svg")
.attr("width", 500)
.attr("height", 500)
.style("outline","1px solid black")
.style("margin","50px")
var target = [250, 250];
var annotation = [200, 200];
svg.select("#targetCircle")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
svg.select("#annotationCircle")
.call(d3.drag()
.on("start", dragstarted2)
.on("drag", dragged2)
.on("end", dragended2))
function dragstarted(){
}
function dragged(){
var coords = d3.mouse(svg.node());
d3.select("#targetCircle")
.attr('cx', ()=>{return coords[0]})
.attr('cy', ()=>{return coords[1]});
target = coords;
updateSwoop(annotation, target, 6);
}
function dragended(){
}
function dragstarted2(){
}
function dragged2(){
var coords = d3.mouse(svg.node());
annotation = coords;
d3.select("#annotationCircle")
.attr('cx', ()=>{return coords[0]})
.attr('cy', ()=>{return coords[1]});
updateSwoop(annotation, target, 6);
}
function dragended2(){
}
updateSwoop(annotation, target, 6);
function updateSwoop(annotation, target, pad){
//toggle clockwise based on x position of annotation relative to target.
var clockwise = false;
//to handle a pad dynamically, offset the x-y for the target by desired angle
var padx = 0;
var pady = 0;
if(annotation[0]>target[0] && annotation[1]<=target[1]){
// annotation is to the right and above/equal to the target.
clockwise = false;
pady = -pad;
} else if(annotation[0]>target[0]){
// annotation is to the right and below to the target.
clockwise = false;
padx = pad;
} else if(annotation[1]<=target[1]){
// annotation is to the right and above the target.
clockwise = true;
pady = -pad;
} else {
// annotation is to the left.
clockwise = true;
padx = -pad;
}
var swoopy = swoopyArrow()
.angle(Math.PI/2)
.clockwise(clockwise)
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});
svg.select("#annotationCircle")
.attr("cx", annotation[0])
.attr("cy", annotation[1])
.attr("r",12)
.style("fill","green");
svg.select("#targetCircle")
.attr("cx", target[0])
.attr("cy", target[1])
.attr("r",5)
.style("fill","red");
var offsetTarget = [target[0]+padx, target[1]+pady]
svg.select("path")
.attr("class","swoopyArrow")
.attr('marker-end', 'url(#arrowhead)')
.datum([annotation, offsetTarget])
.attr("d", swoopy);
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment