├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── index.js ├── lib ├── d3v4+jetpack.js └── data.tsv ├── package.json └── swoopy-drag.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | *.zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adam Pearce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swoopyDrag 2 | 3 | > The annotation layer is the most important thing we do 4 | 5 | *- Amanda Cox -* 6 | 7 | ### [Demo/Documentation](http://1wheel.github.io/swoopy-drag/) 8 | 9 | ### API Reference 10 | 11 | #### d3.swoopyDrag() 12 | 13 | Creates a new `swoopyDrag`. 14 | 15 | #### swoopyDrag.x([function]) 16 | 17 | Function called on each annotation object to determine its `x` position. 18 | 19 | #### swoopyDrag.y([function]) 20 | 21 | Function called on each annotation object to determine its `y` position. 22 | 23 | #### swoopyDraw.draggable([boolean]) 24 | 25 | Boolean. Pass true while adjusting annotations to enable dragging and add control points to paths. 26 | 27 | #### swoopyDrag.annotations([array]) 28 | 29 | Array of objects representing annotations. The `path` in each annotations will have its `d` attribute set to the `path` property. The `text` element will contain the `text` property and be translated by `textOffset`. 30 | 31 | #### swoopyDrag.on('drag', [function]) 32 | 33 | Called as the labels or paths are dragged. -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 140 | 141 |
142 | 143 |147 | “The annotation layer is the most important thing we do” —Amanda Cox 148 |
149 |150 | swoopyDrag helps you hand place annotations on d3 graphics. It takes an array of objects representing annotations and turns them into lines and labels. Drag the text and control circles below to update the annotations array: 151 |
152 | 153 |160 | The x and y functions are called on each annotation to determine its position. In the annotations array here, the sepalWidth and sepalLength properties are the data values of point the annotation refers to. The functions passed to x and y look up these values and encode them as pixel position using the same scale set up to position the circles. 161 | 162 |
163 | var swoopy = d3.swoopyDrag() 164 | .x(d => xScale(d.sepalWidth)) 165 | .y(d => yScale(d.sepalLength)) 166 | .draggable(true) 167 | .annotations(annotations) 168 |169 | 170 |
171 | Setting
174 | The shape of each annotation's line is determined by the path property, the text by the text property and the position of the text by the testOffset property. Currently only straight paths (paths of the form
The annotations are added to the page just like
181 | var swoopySel = svg.append('g').call(swoopy) 182 |183 | 184 |
After posititioning the labels, open the dev tools, run
Since each annotation's position is determined primarily by scales, lines and labels will still point to the correct position when the chart size changes. As the chart shrinks though, the annotations might overlap or cover up data points. To show fewer or differently positioned labels on mobile, you could create multiple annotation arrays for different screen sizes: 194 |
195 | 196 |197 | d3.swoopyDrag() 198 | .annotations(innerWidth < 800 ? mobileAnnotations : desktopAnnotations) 199 |200 | 201 |
202 | Alternatively if there's just one or two problematic annotations that only work above or below some sizes, you could add
206 | d3.swoopyDrag() 207 | .annotations(annotations.filter(function(d){ 208 | return (typeof(d.minWidth) == 'undefined' || innerWidth > d.minWidth) 209 | && (typeof(d.maxWidth) == 'undefined' || innerWidth < d.maxWidth) 210 | })) 211 |212 | 213 |
SVG has native support for arrowheads, but they can be a little fiddly to get working. First, add a
219 | svg.append('marker') 220 | .attr('id', 'arrow') 221 | .attr('viewBox', '-10 -10 20 20') 222 | .attr('markerWidth', 20) 223 | .attr('markerHeight', 20) 224 | .attr('orient', 'auto') 225 | .append('path') 226 | .attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75') 227 |228 | 229 |
Next, select paths in each annotation and set their
232 | swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)') 233 |234 | 235 | 236 |
Multiline text can be added with d3-jetpack. Select all of the
242 | swoopySel.selectAll('text') 243 | .each(function(d){ 244 | d3.select(this) 245 | .text('') //clear existing text 246 | .tspans(d3.wordwrap(d.text, 20)) //wrap after 20 char 247 | }) 248 |249 | 250 |
251 | 252 |
On the page, annotations are made up of a
To customize the color of the text you could add a
259 | swoopySel.selectAll('text').style('fill', d => d.textColor || '#000') 260 |261 | 262 |
Or you could add a
265 | swoopySel.selectAll('g').attr('class', d => d.class) 266 |267 | 268 |
And emphasize them with css: 269 | 270 |
271 | g.highlight text{ font-weight: 700; } 272 | g.highlight path{ stroke-width: 2; } 273 |274 | 275 | 276 | 277 |
279 | d3-module-faces
280 |
281 | Minute by Minute Point Differentials
282 |
283 | NBA Win/Loss Records
284 |
285 | Bush and Kasich Donors Give to Clinton
286 |
293 | swoopyarrows creates fancier swoops, including circular and loopy arcs.
294 |
295 | labella.js uses a force directed layout to position timeline labels with no overlap.
296 |
297 | svg-crowbar lets you export a
299 | ai2html is an illustrator script that creates responsive
301 |
305 | github.com/1wheel/swoopy-drag 306 |
307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | export function swoopyDrag(){ 4 | var x = function(d){ return d } 5 | var y = function(d){ return d } 6 | 7 | var annotations = [] 8 | var annotationSel 9 | 10 | var draggable = false 11 | 12 | var dispatch = d3.dispatch('drag') 13 | 14 | var textDrag = d3.drag() 15 | .on('drag', function(d){ 16 | var x = d3.event.x 17 | var y = d3.event.y 18 | d.textOffset = [x, y].map(Math.round) 19 | 20 | d3.select(this).call(translate, d.textOffset) 21 | 22 | dispatch.call('drag') 23 | }) 24 | .subject(function(d){ return {x: d.textOffset[0], y: d.textOffset[1]} }) 25 | 26 | var circleDrag = d3.drag() 27 | .on('drag', function(d){ 28 | var x = d3.event.x 29 | var y = d3.event.y 30 | d.pos = [x, y].map(Math.round) 31 | 32 | var parentSel = d3.select(this.parentNode) 33 | 34 | var path = '' 35 | var points = parentSel.selectAll('circle').data() 36 | if (points[0].type == 'A'){ 37 | path = calcCirclePath(points) 38 | } else{ 39 | points.forEach(function(d){ path = path + d.type + d.pos }) 40 | } 41 | 42 | parentSel.select('path').attr('d', path).datum().path = path 43 | d3.select(this).call(translate, d.pos) 44 | 45 | dispatch.call('drag') 46 | }) 47 | .subject(function(d){ return {x: d.pos[0], y: d.pos[1]} }) 48 | 49 | 50 | var rv = function(sel){ 51 | annotationSel = sel.html('').selectAll('g') 52 | .data(annotations).enter() 53 | .append('g') 54 | .call(translate, function(d){ return [x(d), y(d)] }) 55 | 56 | var textSel = annotationSel.append('text') 57 | .call(translate, ƒ('textOffset')) 58 | .text(ƒ('text')) 59 | 60 | annotationSel.append('path') 61 | .attr('d', ƒ('path')) 62 | 63 | if (!draggable) return 64 | 65 | annotationSel.style('cursor', 'pointer') 66 | textSel.call(textDrag) 67 | 68 | annotationSel.selectAll('circle').data(function(d){ 69 | var points = [] 70 | 71 | if (~d.path.indexOf('A')){ 72 | //handle arc paths seperatly -- only one circle supported 73 | var pathNode = d3.select(this).select('path').node() 74 | var l = pathNode.getTotalLength() 75 | 76 | points = [0, .5, 1].map(function(d){ 77 | var p = pathNode.getPointAtLength(d*l) 78 | return {pos: [p.x, p.y], type: 'A'} 79 | }) 80 | } else{ 81 | var i = 1 82 | var type = 'M' 83 | var commas = 0 84 | 85 | for (var j = 1; j < d.path.length; j++){ 86 | var curChar = d.path[j] 87 | if (curChar == ',') commas++ 88 | if (curChar == 'L' || curChar == 'C' || commas == 2){ 89 | points.push({pos: d.path.slice(i, j).split(','), type: type}) 90 | type = curChar 91 | i = j + 1 92 | commas = 0 93 | } 94 | } 95 | 96 | points.push({pos: d.path.slice(i, j).split(','), type: type}) 97 | } 98 | 99 | return points 100 | }).enter().append('circle') 101 | .attr('r', 8) 102 | .attr('fill', 'rgba(0,0,0,0)') 103 | .attr('stroke', '#333') 104 | .attr('stroke-dasharray', '2 2') 105 | .call(translate, ƒ('pos')) 106 | .call(circleDrag) 107 | 108 | dispatch.call('drag') 109 | } 110 | 111 | 112 | rv.annotations = function(_x){ 113 | if (typeof(_x) == 'undefined') return annotations 114 | annotations = _x 115 | return rv 116 | } 117 | rv.x = function(_x){ 118 | if (typeof(_x) == 'undefined') return x 119 | x = _x 120 | return rv 121 | } 122 | rv.y = function(_x){ 123 | if (typeof(_x) == 'undefined') return y 124 | y = _x 125 | return rv 126 | } 127 | rv.draggable = function(_x){ 128 | if (typeof(_x) == 'undefined') return draggable 129 | draggable = _x 130 | return rv 131 | } 132 | rv.on = function() { 133 | var value = dispatch.on.apply(dispatch, arguments); 134 | return value === dispatch ? rv : value; 135 | } 136 | 137 | return rv 138 | 139 | //convert 3 points to an Arc Path 140 | function calcCirclePath(points){ 141 | var a = points[0].pos 142 | var b = points[2].pos 143 | var c = points[1].pos 144 | 145 | var A = dist(b, c) 146 | var B = dist(c, a) 147 | var C = dist(a, b) 148 | 149 | var angle = Math.acos((A*A + B*B - C*C)/(2*A*B)) 150 | 151 | //calc radius of circle 152 | var K = .5*A*B*Math.sin(angle) 153 | var r = A*B*C/4/K 154 | r = Math.round(r*1000)/1000 155 | 156 | //large arc flag 157 | var laf = +(Math.PI/2 > angle) 158 | 159 | //sweep flag 160 | var saf = +((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) < 0) 161 | 162 | return ['M', a, 'A', r, r, 0, laf, saf, b].join(' ') 163 | } 164 | 165 | function dist(a, b){ 166 | return Math.sqrt( 167 | Math.pow(a[0] - b[0], 2) + 168 | Math.pow(a[1] - b[1], 2)) 169 | } 170 | 171 | 172 | //no jetpack dependency 173 | function translate(sel, pos){ 174 | sel.attr('transform', function(d){ 175 | var posStr = typeof(pos) == 'function' ? pos(d) : pos 176 | return 'translate(' + posStr + ')' 177 | }) 178 | } 179 | 180 | function ƒ(str){ return function(d){ return d[str] } } 181 | } -------------------------------------------------------------------------------- /lib/data.tsv: -------------------------------------------------------------------------------- 1 | sepalLength sepalWidth petalLength petalWidth species 2 | 5.1 3.5 1.4 0.2 setosa 3 | 4.9 3.0 1.4 0.2 setosa 4 | 4.7 3.2 1.3 0.2 setosa 5 | 4.6 3.1 1.5 0.2 setosa 6 | 5.0 3.6 1.4 0.2 setosa 7 | 5.4 3.9 1.7 0.4 setosa 8 | 4.6 3.4 1.4 0.3 setosa 9 | 5.0 3.4 1.5 0.2 setosa 10 | 4.4 2.9 1.4 0.2 setosa 11 | 4.9 3.1 1.5 0.1 setosa 12 | 5.4 3.7 1.5 0.2 setosa 13 | 4.8 3.4 1.6 0.2 setosa 14 | 4.8 3.0 1.4 0.1 setosa 15 | 4.3 3.0 1.1 0.1 setosa 16 | 5.8 4.0 1.2 0.2 setosa 17 | 5.7 4.4 1.5 0.4 setosa 18 | 5.4 3.9 1.3 0.4 setosa 19 | 5.1 3.5 1.4 0.3 setosa 20 | 5.7 3.8 1.7 0.3 setosa 21 | 5.1 3.8 1.5 0.3 setosa 22 | 5.4 3.4 1.7 0.2 setosa 23 | 5.1 3.7 1.5 0.4 setosa 24 | 4.6 3.6 1.0 0.2 setosa 25 | 5.1 3.3 1.7 0.5 setosa 26 | 4.8 3.4 1.9 0.2 setosa 27 | 5.0 3.0 1.6 0.2 setosa 28 | 5.0 3.4 1.6 0.4 setosa 29 | 5.2 3.5 1.5 0.2 setosa 30 | 5.2 3.4 1.4 0.2 setosa 31 | 4.7 3.2 1.6 0.2 setosa 32 | 4.8 3.1 1.6 0.2 setosa 33 | 5.4 3.4 1.5 0.4 setosa 34 | 5.2 4.1 1.5 0.1 setosa 35 | 5.5 4.2 1.4 0.2 setosa 36 | 4.9 3.1 1.5 0.2 setosa 37 | 5.0 3.2 1.2 0.2 setosa 38 | 5.5 3.5 1.3 0.2 setosa 39 | 4.9 3.6 1.4 0.1 setosa 40 | 4.4 3.0 1.3 0.2 setosa 41 | 5.1 3.4 1.5 0.2 setosa 42 | 5.0 3.5 1.3 0.3 setosa 43 | 4.5 2.3 1.3 0.3 setosa 44 | 4.4 3.2 1.3 0.2 setosa 45 | 5.0 3.5 1.6 0.6 setosa 46 | 5.1 3.8 1.9 0.4 setosa 47 | 4.8 3.0 1.4 0.3 setosa 48 | 5.1 3.8 1.6 0.2 setosa 49 | 4.6 3.2 1.4 0.2 setosa 50 | 5.3 3.7 1.5 0.2 setosa 51 | 5.0 3.3 1.4 0.2 setosa 52 | 7.0 3.2 4.7 1.4 versicolor 53 | 6.4 3.2 4.5 1.5 versicolor 54 | 6.9 3.1 4.9 1.5 versicolor 55 | 5.5 2.3 4.0 1.3 versicolor 56 | 6.5 2.8 4.6 1.5 versicolor 57 | 5.7 2.8 4.5 1.3 versicolor 58 | 6.3 3.3 4.7 1.6 versicolor 59 | 4.9 2.4 3.3 1.0 versicolor 60 | 6.6 2.9 4.6 1.3 versicolor 61 | 5.2 2.7 3.9 1.4 versicolor 62 | 5.0 2.0 3.5 1.0 versicolor 63 | 5.9 3.0 4.2 1.5 versicolor 64 | 6.0 2.2 4.0 1.0 versicolor 65 | 6.1 2.9 4.7 1.4 versicolor 66 | 5.6 2.9 3.6 1.3 versicolor 67 | 6.7 3.1 4.4 1.4 versicolor 68 | 5.6 3.0 4.5 1.5 versicolor 69 | 5.8 2.7 4.1 1.0 versicolor 70 | 6.2 2.2 4.5 1.5 versicolor 71 | 5.6 2.5 3.9 1.1 versicolor 72 | 5.9 3.2 4.8 1.8 versicolor 73 | 6.1 2.8 4.0 1.3 versicolor 74 | 6.3 2.5 4.9 1.5 versicolor 75 | 6.1 2.8 4.7 1.2 versicolor 76 | 6.4 2.9 4.3 1.3 versicolor 77 | 6.6 3.0 4.4 1.4 versicolor 78 | 6.8 2.8 4.8 1.4 versicolor 79 | 6.7 3.0 5.0 1.7 versicolor 80 | 6.0 2.9 4.5 1.5 versicolor 81 | 5.7 2.6 3.5 1.0 versicolor 82 | 5.5 2.4 3.8 1.1 versicolor 83 | 5.5 2.4 3.7 1.0 versicolor 84 | 5.8 2.7 3.9 1.2 versicolor 85 | 6.0 2.7 5.1 1.6 versicolor 86 | 5.4 3.0 4.5 1.5 versicolor 87 | 6.0 3.4 4.5 1.6 versicolor 88 | 6.7 3.1 4.7 1.5 versicolor 89 | 6.3 2.3 4.4 1.3 versicolor 90 | 5.6 3.0 4.1 1.3 versicolor 91 | 5.5 2.5 4.0 1.3 versicolor 92 | 5.5 2.6 4.4 1.2 versicolor 93 | 6.1 3.0 4.6 1.4 versicolor 94 | 5.8 2.6 4.0 1.2 versicolor 95 | 5.0 2.3 3.3 1.0 versicolor 96 | 5.6 2.7 4.2 1.3 versicolor 97 | 5.7 3.0 4.2 1.2 versicolor 98 | 5.7 2.9 4.2 1.3 versicolor 99 | 6.2 2.9 4.3 1.3 versicolor 100 | 5.1 2.5 3.0 1.1 versicolor 101 | 5.7 2.8 4.1 1.3 versicolor 102 | 6.3 3.3 6.0 2.5 virginica 103 | 5.8 2.7 5.1 1.9 virginica 104 | 7.1 3.0 5.9 2.1 virginica 105 | 6.3 2.9 5.6 1.8 virginica 106 | 6.5 3.0 5.8 2.2 virginica 107 | 7.6 3.0 6.6 2.1 virginica 108 | 4.9 2.5 4.5 1.7 virginica 109 | 7.3 2.9 6.3 1.8 virginica 110 | 6.7 2.5 5.8 1.8 virginica 111 | 7.2 3.6 6.1 2.5 virginica 112 | 6.5 3.2 5.1 2.0 virginica 113 | 6.4 2.7 5.3 1.9 virginica 114 | 6.8 3.0 5.5 2.1 virginica 115 | 5.7 2.5 5.0 2.0 virginica 116 | 5.8 2.8 5.1 2.4 virginica 117 | 6.4 3.2 5.3 2.3 virginica 118 | 6.5 3.0 5.5 1.8 virginica 119 | 7.7 3.8 6.7 2.2 virginica 120 | 7.7 2.6 6.9 2.3 virginica 121 | 6.0 2.2 5.0 1.5 virginica 122 | 6.9 3.2 5.7 2.3 virginica 123 | 5.6 2.8 4.9 2.0 virginica 124 | 7.7 2.8 6.7 2.0 virginica 125 | 6.3 2.7 4.9 1.8 virginica 126 | 6.7 3.3 5.7 2.1 virginica 127 | 7.2 3.2 6.0 1.8 virginica 128 | 6.2 2.8 4.8 1.8 virginica 129 | 6.1 3.0 4.9 1.8 virginica 130 | 6.4 2.8 5.6 2.1 virginica 131 | 7.2 3.0 5.8 1.6 virginica 132 | 7.4 2.8 6.1 1.9 virginica 133 | 7.9 3.8 6.4 2.0 virginica 134 | 6.4 2.8 5.6 2.2 virginica 135 | 6.3 2.8 5.1 1.5 virginica 136 | 6.1 2.6 5.6 1.4 virginica 137 | 7.7 3.0 6.1 2.3 virginica 138 | 6.3 3.4 5.6 2.4 virginica 139 | 6.4 3.1 5.5 1.8 virginica 140 | 6.0 3.0 4.8 1.8 virginica 141 | 6.9 3.1 5.4 2.1 virginica 142 | 6.7 3.1 5.6 2.4 virginica 143 | 6.9 3.1 5.1 2.3 virginica 144 | 5.8 2.7 5.1 1.9 virginica 145 | 6.8 3.2 5.9 2.3 virginica 146 | 6.7 3.3 5.7 2.5 virginica 147 | 6.7 3.0 5.2 2.3 virginica 148 | 6.3 2.5 5.0 1.9 virginica 149 | 6.5 3.0 5.2 2.0 virginica 150 | 6.2 3.4 5.4 2.3 virginica 151 | 5.9 3.0 5.1 1.8 virginica 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-swoopy-drag", 3 | "version": "0.0.4", 4 | "description": "Artisanal label placement for d3 graphics.", 5 | "keywords": [ 6 | "d3", 7 | "d3-module", 8 | "annotation", 9 | "drag" 10 | ], 11 | "license": "MIT", 12 | "main": "swoopy-drag.js", 13 | "author": "Adam Pearce", 14 | "jsnext:main": "index", 15 | "module": "index", 16 | "homepage": "http://1wheel.github.io/swoopy-drag/", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/1wheel/swoopy-drag.git" 20 | }, 21 | "scripts": { 22 | "pretest": "rollup -f umd -n d3 -g d3:d3 -o swoopy-drag.js -- index.js", 23 | "test": "echo 'no tests'", 24 | "prepublish": "npm run test", 25 | "postpublish": "zip -j d3-swoopy-drag.zip -- LICENSE README.md d3-swoopy-drag.js" 26 | }, 27 | "devDependencies": { 28 | "rollup": "0.27", 29 | "tape": "4" 30 | }, 31 | "dependencies": { 32 | "d3": "4" 33 | } 34 | } -------------------------------------------------------------------------------- /swoopy-drag.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3')) : 3 | typeof define === 'function' && define.amd ? define(['exports', 'd3'], factory) : 4 | (factory((global.d3 = global.d3 || {}),global.d3)); 5 | }(this, function (exports,d3) { 'use strict'; 6 | 7 | function swoopyDrag(){ 8 | var x = function(d){ return d } 9 | var y = function(d){ return d } 10 | 11 | var annotations = [] 12 | var annotationSel 13 | 14 | var draggable = false 15 | 16 | var dispatch = d3.dispatch('drag') 17 | 18 | var textDrag = d3.drag() 19 | .on('drag', function(d){ 20 | var x = d3.event.x 21 | var y = d3.event.y 22 | d.textOffset = [x, y].map(Math.round) 23 | 24 | d3.select(this).call(translate, d.textOffset) 25 | 26 | dispatch.call('drag') 27 | }) 28 | .subject(function(d){ return {x: d.textOffset[0], y: d.textOffset[1]} }) 29 | 30 | var circleDrag = d3.drag() 31 | .on('drag', function(d){ 32 | var x = d3.event.x 33 | var y = d3.event.y 34 | d.pos = [x, y].map(Math.round) 35 | 36 | var parentSel = d3.select(this.parentNode) 37 | 38 | var path = '' 39 | var points = parentSel.selectAll('circle').data() 40 | if (points[0].type == 'A'){ 41 | path = calcCirclePath(points) 42 | } else{ 43 | points.forEach(function(d){ path = path + d.type + d.pos }) 44 | } 45 | 46 | parentSel.select('path').attr('d', path).datum().path = path 47 | d3.select(this).call(translate, d.pos) 48 | 49 | dispatch.call('drag') 50 | }) 51 | .subject(function(d){ return {x: d.pos[0], y: d.pos[1]} }) 52 | 53 | 54 | var rv = function(sel){ 55 | annotationSel = sel.html('').selectAll('g') 56 | .data(annotations).enter() 57 | .append('g') 58 | .call(translate, function(d){ return [x(d), y(d)] }) 59 | 60 | var textSel = annotationSel.append('text') 61 | .call(translate, ƒ('textOffset')) 62 | .text(ƒ('text')) 63 | 64 | annotationSel.append('path') 65 | .attr('d', ƒ('path')) 66 | 67 | if (!draggable) return 68 | 69 | annotationSel.style('cursor', 'pointer') 70 | textSel.call(textDrag) 71 | 72 | annotationSel.selectAll('circle').data(function(d){ 73 | var points = [] 74 | 75 | if (~d.path.indexOf('A')){ 76 | //handle arc paths seperatly -- only one circle supported 77 | var pathNode = d3.select(this).select('path').node() 78 | var l = pathNode.getTotalLength() 79 | 80 | points = [0, .5, 1].map(function(d){ 81 | var p = pathNode.getPointAtLength(d*l) 82 | return {pos: [p.x, p.y], type: 'A'} 83 | }) 84 | } else{ 85 | var i = 1 86 | var type = 'M' 87 | var commas = 0 88 | 89 | for (var j = 1; j < d.path.length; j++){ 90 | var curChar = d.path[j] 91 | if (curChar == ',') commas++ 92 | if (curChar == 'L' || curChar == 'C' || commas == 2){ 93 | points.push({pos: d.path.slice(i, j).split(','), type: type}) 94 | type = curChar 95 | i = j + 1 96 | commas = 0 97 | } 98 | } 99 | 100 | points.push({pos: d.path.slice(i, j).split(','), type: type}) 101 | } 102 | 103 | return points 104 | }).enter().append('circle') 105 | .attr('r', 8) 106 | .attr('fill', 'rgba(0,0,0,0)') 107 | .attr('stroke', '#333') 108 | .attr('stroke-dasharray', '2 2') 109 | .call(translate, ƒ('pos')) 110 | .call(circleDrag) 111 | 112 | dispatch.call('drag') 113 | } 114 | 115 | 116 | rv.annotations = function(_x){ 117 | if (typeof(_x) == 'undefined') return annotations 118 | annotations = _x 119 | return rv 120 | } 121 | rv.x = function(_x){ 122 | if (typeof(_x) == 'undefined') return x 123 | x = _x 124 | return rv 125 | } 126 | rv.y = function(_x){ 127 | if (typeof(_x) == 'undefined') return y 128 | y = _x 129 | return rv 130 | } 131 | rv.draggable = function(_x){ 132 | if (typeof(_x) == 'undefined') return draggable 133 | draggable = _x 134 | return rv 135 | } 136 | rv.on = function() { 137 | var value = dispatch.on.apply(dispatch, arguments); 138 | return value === dispatch ? rv : value; 139 | } 140 | 141 | return rv 142 | 143 | //convert 3 points to an Arc Path 144 | function calcCirclePath(points){ 145 | var a = points[0].pos 146 | var b = points[2].pos 147 | var c = points[1].pos 148 | 149 | var A = dist(b, c) 150 | var B = dist(c, a) 151 | var C = dist(a, b) 152 | 153 | var angle = Math.acos((A*A + B*B - C*C)/(2*A*B)) 154 | 155 | //calc radius of circle 156 | var K = .5*A*B*Math.sin(angle) 157 | var r = A*B*C/4/K 158 | r = Math.round(r*1000)/1000 159 | 160 | //large arc flag 161 | var laf = +(Math.PI/2 > angle) 162 | 163 | //sweep flag 164 | var saf = +((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) < 0) 165 | 166 | return ['M', a, 'A', r, r, 0, laf, saf, b].join(' ') 167 | } 168 | 169 | function dist(a, b){ 170 | return Math.sqrt( 171 | Math.pow(a[0] - b[0], 2) + 172 | Math.pow(a[1] - b[1], 2)) 173 | } 174 | 175 | 176 | //no jetpack dependency 177 | function translate(sel, pos){ 178 | sel.attr('transform', function(d){ 179 | var posStr = typeof(pos) == 'function' ? pos(d) : pos 180 | return 'translate(' + posStr + ')' 181 | }) 182 | } 183 | 184 | function ƒ(str){ return function(d){ return d[str] } } 185 | } 186 | 187 | exports.swoopyDrag = swoopyDrag; 188 | 189 | Object.defineProperty(exports, '__esModule', { value: true }); 190 | 191 | })); --------------------------------------------------------------------------------