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.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"//www.w3.org/TR/html4/loose.dtd">
<html xmlns="//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>
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