block by HarryStevens f6e9003562bfe454a8f267f62cb2cf4a

Annotations with Swoopy Drag for Scatter Plot

Full Screen

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.

index.html

<!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>

annotations.js

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.js

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] } }
}

data.csv

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