Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active March 5, 2017 02:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save HarryStevens/f6e9003562bfe454a8f267f62cb2cf4a to your computer and use it in GitHub Desktop.
Save HarryStevens/f6e9003562bfe454a8f267f62cb2cf4a to your computer and use it in GitHub Desktop.
Annotations with Swoopy Drag for Scatter Plot
license: gpl-3.0

Use swoopy drag to annotate your scatter plot in d3.js.

Drag the annotation points around the page. When you're happy with their locations, type copy(annotations) in the console, and paste the clipboard into your var annotations.

For a full tutorial, visit the swoopy drag page.

var annotations = [
{
"xValue": 40.18594637,
"yValue": 66.66316631,
"path": "M -60,18 A 31.408 31.408 0 0 0 -2,6",
"text": "Elizabeth",
"textOffset": [
-93,
11
]
},
{
"xValue": 28.36302159,
"yValue": 57.69543829,
"path": "M -6,50 A 25.855 25.855 0 0 1 -7,-1",
"text": "Aria",
"textOffset": [
1,
54
]
},
{
"xValue": 11.27003571,
"yValue": 88.77343766,
"path": "M 39,25 A 23.896 23.896 0 1 0 4,-7",
"text": "Noah",
"textOffset": [
17,
39
]
}
];
d3.swoopyDrag = function(){
var x = d3.scaleLinear()
var y = d3.scaleLinear()
var annotations = []
var annotationSel
var draggable = false
var dispatch = d3.dispatch('drag')
var textDrag = d3.drag()
.on('drag', function(d){
var x = d3.event.x
var y = d3.event.y
d.textOffset = [x, y].map(Math.round)
d3.select(this).call(translate, d.textOffset)
dispatch.call('drag')
})
.subject(function(d){ return {x: d.textOffset[0], y: d.textOffset[1]} })
var circleDrag = d3.drag()
.on('drag', function(d){
var x = d3.event.x
var y = d3.event.y
d.pos = [x, y].map(Math.round)
var parentSel = d3.select(this.parentNode)
var path = ''
var points = parentSel.selectAll('circle').data()
if (points[0].type == 'A'){
path = calcCirclePath(points)
} else{
points.forEach(function(d){ path = path + d.type + d.pos })
}
parentSel.select('path').attr('d', path).datum().path = path
d3.select(this).call(translate, d.pos)
dispatch.call('drag')
})
.subject(function(d){ return {x: d.pos[0], y: d.pos[1]} })
var rv = function(sel){
annotationSel = sel.html('').selectAll('g')
.data(annotations).enter()
.append('g')
.call(translate, function(d){ return [x(d), y(d)] })
var textSel = annotationSel.append('text')
.call(translate, token('textOffset'))
.text(token('text'))
annotationSel.append('path')
.attr('d', token('path'))
if (!draggable) return
annotationSel.style('cursor', 'pointer')
textSel.call(textDrag)
annotationSel.selectAll('circle').data(function(d){
var points = []
if (~d.path.indexOf('A')){
//handle arc paths seperatly -- only one circle supported
var pathNode = d3.select(this).select('path').node()
var l = pathNode.getTotalLength()
points = [0, .5, 1].map(function(d){
var p = pathNode.getPointAtLength(d*l)
return {pos: [p.x, p.y], type: 'A'}
})
} else{
var i = 1
var type = 'M'
var commas = 0
for (var j = 1; j < d.path.length; j++){
var curChar = d.path[j]
if (curChar == ',') commas++
if (curChar == 'L' || curChar == 'C' || commas == 2){
points.push({pos: d.path.slice(i, j).split(','), type: type})
type = curChar
i = j + 1
commas = 0
}
}
points.push({pos: d.path.slice(i, j).split(','), type: type})
}
return points
}).enter().append('circle')
.attr('r', 8)
.attr('fill', 'rgba(0,0,0,0)')
.attr('stroke', '#333')
.attr('stroke-dasharray', '2 2')
.call(translate, token('pos'))
.call(circleDrag)
dispatch.call('drag')
}
rv.annotations = function(_x){
if (typeof(_x) == 'undefined') return annotations
annotations = _x
return rv
}
rv.x = function(_x){
if (typeof(_x) == 'undefined') return x
x = _x
return rv
}
rv.y = function(_x){
if (typeof(_x) == 'undefined') return y
y = _x
return rv
}
rv.draggable = function(_x){
if (typeof(_x) == 'undefined') return draggable
draggable = _x
return rv
}
rv.on = function() {
var value = dispatch.on.apply(dispatch, arguments);
return value === dispatch ? rv : value;
}
return rv
//convert 3 points to an Arc Path
function calcCirclePath(points){
var a = points[0].pos
var b = points[2].pos
var c = points[1].pos
var A = dist(b, c)
var B = dist(c, a)
var C = dist(a, b)
var angle = Math.acos((A*A + B*B - C*C)/(2*A*B))
//calc radius of circle
var K = .5*A*B*Math.sin(angle)
var r = A*B*C/4/K
r = Math.round(r*1000)/1000
//large arc flag
var laf = +(Math.PI/2 > angle)
//sweep flag
var saf = +((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) < 0)
return ['M', a, 'A', r, r, 0, laf, saf, b].join(' ')
}
function dist(a, b){
return Math.sqrt(
Math.pow(a[0] - b[0], 2) +
Math.pow(a[1] - b[1], 2))
}
//no jetpack dependency
function translate(sel, pos){
sel.attr('transform', function(d){
var posStr = typeof(pos) == 'function' ? pos(d) : pos
return 'translate(' + posStr + ')'
})
}
function token(str){ return function(d){ return d[str] } }
}
name x y
Noah 11.27003571 88.77343766
Liam 19.73385696 79.04811495
Ethan 3.761929642 85.68535388
Lucas 5.421437532 82.80845477
Mason 2.549084585 79.97105809
Oliver 33.36555772 58.44080684
Aiden 1.594728697 92.31637968
Elijah 5.028269931 84.06370038
Benjamin 29.62285349 70.40983908
James 4.300790399 65.71298073
Logan 1.018589568 92.72992499
Jacob 6.189890001 62.0497844
Jackson 2.500819713 91.00477379
Michael 4.354894457 76.83763512
Carter 14.050125 63.6362308
William 1.988261951 84.52044745
Daniel 11.94771355 55.17891294
Alexander 7.257912644 76.05499515
Luke 8.287677281 77.36675344
Owen 1.536290248 88.48360983
Jack 3.226684682 96.3338812
Gabriel 1.709155851 88.068512
Matthew 6.537126369 90.79426085
Henry 1.959780252 92.16606293
Sebastian 4.613524263 84.75412344
Wyatt 2.546143547 84.67401936
Grayson 2.203033882 86.39972227
Isaac 2.802066804 85.69300716
Ryan 8.737254378 84.5829933
Nathan 9.391822654 75.38122126
Jayden 1.629713601 89.32460249
Jaxon 2.067608467 79.76530605
Caleb 2.071654475 90.27542871
David 3.169471649 91.46316395
Levi 14.12227801 66.81414111
Eli 3.151841812 85.71375168
Julian 9.458633815 89.29621373
Andrew 3.727868003 89.33320868
Dylan 1.691066983 88.59192863
Hunter 2.151502825 91.26024528
Emma 8.994238858 83.04809322
Olivia 3.173394263 89.79970872
Ava 9.931748154 82.77564203
Sophia 9.257203392 84.31899348
Isabella 2.125536698 85.79547586
Mia 5.751086254 82.15530514
Charlotte 4.476227873 76.55001202
Harper 2.86511372 95.26945167
Amelia 6.165794958 77.07868992
Abigail 1.527170789 81.8296847
Emily 3.310661013 93.05720137
Madison 19.26649252 93.47722192
Lily 4.397012544 90.71884874
Ella 1.903926077 80.23402878
Avery 24.53763972 63.39872735
Evelyn 2.362863791 91.81263692
Sofia 19.99035985 48.32296873
Aria 28.36302159 57.69543829
Riley 21.56565684 71.3442642
Chloe 4.674148004 85.11159217
Scarlett 4.659473604 91.32189682
Ellie 8.262626513 45.97488076
Aubrey 19.2708132 56.74192862
Elizabeth 40.18594637 66.66316631
Grace 4.605922517 75.82782712
Layla 2.787963348 86.69875359
Addison 3.359345533 80.24495283
Zoey 21.67122862 57.09057474
Hannah 9.039155653 68.79092791
Mila 1.836441804 69.9295158
Victoria 1.784124503 79.29206832
Brooklyn 1.34223936 93.35376759
Zoe 2.534419191 82.7585241
Penelope 7.46372613 87.89232417
Lucy 2.937304061 84.5204076
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
body {
margin: 0 auto;
display: table;
font-family: "Helvetica Neue", sans-serif;
}
.annotation path {
fill: none;
stroke: #3a403d;
}
.annotation text {
fill: #3a403d;
stroke: none;
font-size: .8em;
}
</style>
</head>
<body>
<div class="chart"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3.swoopyDrag.js"></script>
<script src="annotations.js"></script>
<script>
var margin = {top: 5, right: 5, bottom: 20, left: 20},
width = 450 - margin.left - margin.right,
height = 450 - margin.top - margin.bottom;
var svg = d3.select(".chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.range([0,width]);
var y = d3.scaleLinear()
.range([height,0]);
var xAxis = d3.axisBottom()
.scale(x);
var yAxis = d3.axisLeft()
.scale(y);
var swoopy = d3.swoopyDrag()
.x(function(d){ return x(d.xValue); })
.y(function(d){ return y(d.yValue); })
.draggable(true)
.annotations(annotations);
d3.csv("data.csv", types, function(error, data){
x.domain(d3.extent(data, function(d){ return d.x; }));
y.domain(d3.extent(data, function(d){ return d.y; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".point")
.data(data)
.enter().append("circle")
.attr("class", "point")
.attr("r", 3)
.attr("cy", function(d){ return y(d.y); })
.attr("cx", function(d){ return x(d.x); })
var swoopySel = svg.append('g')
.attr("class","annotation")
.call(swoopy);
svg.append('marker')
.attr('id', 'arrow')
.attr('viewBox', '-10 -10 20 20')
.attr('markerWidth', 20)
.attr('markerHeight', 20)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75')
swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)');
});
function types(d){
d.x = +d.x;
d.y = +d.y;
return d;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment