block by jmuyskens e3257654c27f01f2a0f9

e3257654c27f01f2a0f9

Full Screen

Having fun with Adam Pearce’s cool new D3 tool swoopyDrag.js.

index.html

<head>
	<link rel="stylesheet" href="style.css">
	<meta http-equiv="content-type" content="text/html; charset=UTF8">
</head>
<body>
    
	<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
	<script type="text/javascript" src="swoopy-drag.js"></script>
    <script type="text/javascript" src="d3-jetpack.js"></script>
	<script src="chart.js"></script>
</body>

chart.js

var margin = {
	top: 10,
	right: 10,
	bottom: 10,
	left: 10
}

var width = 960 - margin.right - margin.left,
	height = 600 - margin.top - margin.bottom;

var svg = d3
	.select('body')
	.append('svg')
	.attr('height', height + margin.top + margin.bottom)
	.attr('width', width + margin.right + margin.left)
	.append('g')
	.translate([margin.left, margin.top])

xScale = d3.scale.linear()
	.domain([0, width])
	.range([0, width])

yScale = d3.scale.linear()
	.domain([0, height])
	.range([0, height])

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')

function randRange(minMax) {
	return minMax[0] + Math.random() * (minMax[1] - minMax[0])
}

function randCurve(xRange, yRange, startX, startY) {
	var endPoint  = (randRange(xRange) - startX) + "," + (randRange(yRange) - startY);
	var randPair  = (randRange(xRange) - startX) + "," + (randRange(yRange) - startY);
	var randPair2 = (randRange(xRange) - startX) + "," + (randRange(yRange) - startY);

	return "M 0,0 C " + randPair + ',' + randPair2 + ',' + endPoint;
}

function createRandomAnnotation() {
	var xVal = randRange(xScale.domain());
	var yVal = randRange(yScale.domain());
	var path = randCurve(xScale.domain(), yScale.domain(), xVal, yVal);
	console.log(xVal, yVal, path);

	return {
		"xVal": xVal,
		"yVal": yVal,
		"path": path,
		"text": "",
		"textOffset": [
	      0,
	      0
	    ]
	}
}

var swoopy = d3.swoopyDrag()
    .x(function(d){ return xScale(d.xVal) })
    .y(function(d){ return yScale(d.yVal) })
    .draggable(true)
    .annotations(d3.range(100).map(createRandomAnnotation))

var swoopySel = svg.append('g').call(swoopy)

swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)')

d3-jetpack.js

(function(root, factory) {
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = factory(require('d3'));
    } else if (typeof define === 'function' && define.amd) {
        define(['d3'], factory);
    } else {
        root.d3 = factory(root.d3);
    }
}(this, function(d3) {
        
    d3.selection.prototype.translate = function(xy) {
        return this.attr('transform', function(d,i) {
            return 'translate('+[typeof xy == 'function' ? xy.call(this, d,i) : xy]+')';
        });
    };

    d3.transition.prototype.translate = function(xy) {
        return this.attr('transform', function(d,i) {
            return 'translate('+[typeof xy == 'function' ? xy.call(this, d,i) : xy]+')';
        });
    };

    d3.selection.prototype.tspans = function(lines, lh) {
        return this.selectAll('tspan')
            .data(lines)
            .enter()
            .append('tspan')
            .text(function(d) { return d; })
            .attr('x', 0)
            .attr('dy', function(d,i) { return i ? lh || 15 : 0; });
    };

    d3.selection.prototype.append = 
    d3.selection.enter.prototype.append = function(name) {
        var n = d3_parse_attributes(name), s;
        //console.log(name, n);
        name = n.attr ? n.tag : name;
        name = d3_selection_creator(name);
        s = this.select(function() {
            return this.appendChild(name.apply(this, arguments));
        });
        return n.attr ? s.attr(n.attr) : s;
    };

    d3.selection.prototype.insert = 
    d3.selection.enter.prototype.insert = function(name, before) {
        var n = d3_parse_attributes(name), s;
        name = n.attr ? n.tag : name;
        name = d3_selection_creator(name);
        before = d3_selection_selector(before);
        s = this.select(function() {
            return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);
        });
        return n.attr ? s.attr(n.attr) : s;
    };

    var d3_parse_attributes_regex = /([\.#])/g;

    function d3_parse_attributes(name) {
        if (typeof name === "string") {
            var attr = {},
                parts = name.split(d3_parse_attributes_regex), p;
                name = parts.shift();
            while ((p = parts.shift())) {
                if (p == '.') attr['class'] = attr['class'] ? attr['class'] + ' ' + parts.shift() : parts.shift();
                else if (p == '#') attr.id = parts.shift();
            }
            return attr.id || attr['class'] ? { tag: name, attr: attr } : name;
        }
        return name;
    }

    function d3_selection_creator(name) {
        return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() {
            return this.ownerDocument.createElementNS(name.space, name.local);
        } : function() {
            return this.ownerDocument.createElementNS(this.namespaceURI, name);
        };
    }

    function d3_selection_selector(selector) {
        return typeof selector === "function" ? selector : function() {
            return this.querySelector(selector);
        };
    }

    d3.wordwrap = function(line, maxCharactersPerLine) {
        var w = line.split(' '),
            lines = [],
            words = [],
            maxChars = maxCharactersPerLine || 40,
            l = 0;
        w.forEach(function(d) {
            if (l+d.length > maxChars) {
                lines.push(words.join(' '));
                words.length = 0;
                l = 0;
            }
            l += d.length;
            words.push(d);
        });
        if (words.length) {
            lines.push(words.join(' '));
        }
        return lines;
    };
    
    d3.ascendingKey = function(key) {
        return typeof key == 'function' ? function (a, b) {
              return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : key(a) >= key(b) ? 0 : NaN;
        } : function (a, b) {
              return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : a[key] >= b[key] ? 0 : NaN;
        };
    };

    d3.descendingKey = function(key) {
        return typeof key == 'function' ? function (a, b) {
            return key(b) < key(a) ? -1 : key(b) > key(a) ? 1 : key(b) >= key(a) ? 0 : NaN;
        } : function (a, b) {
            return b[key] < a[key] ? -1 : b[key] > a[key] ? 1 : b[key] >= a[key] ? 0 : NaN;
        };
    };
    
    d3.f = function(){
        var functions = arguments;
        //convert all string arguments into field accessors
        var i = 0, l = functions.length;
        while (i < l) {
            if (typeof(functions[i]) === 'string' || typeof(functions[i]) === 'number'){
                functions[i] = (function(str){ return function(d){ return d[str] }; })(functions[i])
            }
            i++;
        }
         //return composition of functions
        return function(d) {
            var i=0, l = functions.length;
            while (i++ < l) d = functions[i-1].call(this, d);
            return d;
        };
    };
    // store d3.f as convenient unicode character function (alt-f on macs)
    if (typeof window !== 'undefined' && !window.hasOwnProperty('ƒ')) window.ƒ = d3.f;
    
    // this tweak allows setting a listener for multiple events, jquery style
    var d3_selection_on = d3.selection.prototype.on;
    d3.selection.prototype.on = function(type, listener, capture) {
        if (typeof type == 'string' && type.indexOf(' ') > -1) {
            type = type.split(' ');
            for (var i = 0; i<type.length; i++) {
                d3_selection_on.apply(this, [type[i], listener, capture]);
            }
        } else {
            d3_selection_on.apply(this, [type, listener, capture]);
        }
        return this;
    };
    
    // for everyone's sake, let's add prop as alias for property
    d3.selection.prototype.prop = d3.selection.prototype.property;

    return d3;

}));

style.css

path {
    fill: none;
    stroke: black;
}

circle {
    stroke: cornflowerblue;
}

svg {
    position: absolute;
    left: 0;
    top: 0;
}

swoopy-drag.js

d3.swoopyDrag = function(){
  var x = d3.scale.linear()
  var y = d3.scale.linear()

  var annotations = []
  var annotationSel

  var draggable = false

  var dispatch = d3.dispatch('drag')

  var textDrag = d3.behavior.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.drag()
      })
      .origin(function(d){ return {x: d.textOffset[0], y: d.textOffset[1]} })

  var circleDrag = d3.behavior.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 = ''
        parentSel.selectAll('circle').each(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.drag()
      })
      .origin(function(d){ return {x: d.pos[0], y: d.pos[1]} })


  var rv = function(sel){
    annotationSel = sel.selectAll('g').data(annotations)
    annotationSel.exit().remove()
    annotationSel.enter().append('g')
    annotationSel.call(translate, function(d){ return [x(d), y(d)] })
    
    var textSel = annotationSel.append('text')
        .call(translate, ƒ('textOffset'))
        .text(ƒ('text'))

    annotationSel.append('path')
        .attr('d', ƒ('path'))

    if (!draggable) return

    annotationSel.style('cursor', 'pointer')
    textSel.call(textDrag)

    annotationSel.selectAll('circle').data(function(d){
      var points = []

      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
        }
      }
      console.log(d.path.slice(i, j))
      points.push({pos: d.path.slice(i, j).split(','), type: type})
      return points
    }).enter().append('circle')
        .attr({r: 8, fill: 'rgba(0,0,0,0)', stroke: '#333', 'stroke-dasharray': '2 2'})
        .call(translate, ƒ('pos'))
        .call(circleDrag)

    dispatch.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
  }

  return d3.rebind(rv, dispatch, 'on')

  //no jetpack dependency 
  function translate(sel, pos){
    sel.attr('transform', function(d){
      var posStr = typeof(pos) == 'function' ? pos(d) : pos
      return 'translate(' + posStr + ')' 
    }) 
  }

  function ƒ(str){ return function(d){ return d[str] } } 
}