├── README.md ├── distance-limited-voronoi.js └── img └── explanation.png /README.md: -------------------------------------------------------------------------------- 1 | # d3-distanceLimitedVoronoi 2 | D3 plugin which computes a Voronoi tesselation where each cell defines a region inside a given distance. 3 | 4 | Because a picture is worth a thousand words: 5 | 6 | ![Explanation](./img/explanation.png) 7 | 8 | This page (as the all _master branch_) concerns the plugin's version compatible with __d3v4__. Switch to the [_d3v3 branch_](https://github.com/Kcnarf/d3-distanceLimitedVoronoi/tree/d3v3) for the version compatible with __d3v3__. 9 | 10 | ## Context 11 | 12 | As stated in the first sentence of the README file of the [d3-voronoi repository](https://github.com/d3/d3-voronoi): 13 | 14 | > Voronoi layouts are particularly useful for invisible interactive regions, as demonstrated in Nate Vack’s [Voronoi picking](http://bl.ocks.org/njvack/1405439) example 15 | 16 | But this cited example also shows that interactive regions should be close to each point/subjectOfMatter. In other words, if the interactive region is far away from the subject of matter, interaction becomes confusing. 17 | 18 | In its example, Nate Vack uses SVG's clipPath technique to cut off Voronoï-based interactive regions. This plugin mimic the final result by computing the adequate distance-limited region around each subject of matter. The adequate region is the intersection area between the Voronoï cell and a max-distance circle. 19 | 20 | Finally, I highly encourage everyone to also take a look at [Using a D3 Voronoi grid to improve a chart's interactive experience](http://www.visualcinnamon.com/2015/07/voronoi.html), from [Nadieh Bremer (visualcinnamon.com)](http://www.visualcinnamon.com/about), where everyone will find a step-by-step use of this technique on a concrete use case. 21 | 22 | ## Examples 23 | 24 | * This [block](http://bl.ocks.org/Kcnarf/6d5ace3aa9cc1a313d72b810388d1003) is an update of Nate Vack’s _Voronoi picking_ example, using the __d3-distanceLimitedVoronoi__ plugin 25 | * This [block](http://bl.ocks.org/Kcnarf/4de291d8b2d1e6501990540d87bc1baf) uses the __d3-distanceLimitedVoronoi__ plugin in a real case study 26 | * [Using a D3 Voronoi grid to improve a chart's interactive experience](http://www.visualcinnamon.com/2015/07/voronoi.html) from [Nadieh Bremer (visualcinnamon.com)](http://www.visualcinnamon.com/about) 27 | 28 | ## Installing 29 | In your HTML file, load the plugin after loading D3. The result may look like: 30 | ```html 31 | 32 | 33 | ``` 34 | 35 | ## TL;DR; 36 | In your javascript, in order to define the layout: 37 | ```javascript 38 | var limitedVoronoi = d3.distanceLimitedVoronoi() 39 | .x(...) // set the x accessor (as in d3.voronoi) 40 | .y(...) // set the y accessor (as in d3.voronoi) 41 | .limit(20) // set the maximum distance 42 | var limitedCells = limitedVoronoi(data) // compute the layout; return an array of {path: , datum: } 43 | // where 'path' is the adequate region around the datum 44 | // and 'datum' is the datum 45 | ``` 46 | 47 | Later, in your javascript, in order to draw the (interactive) regions on an SVG: 48 | ```javascript 49 | d3.selectAll(".interactive-region") 50 | .data(limitedCells) 51 | .enter() 52 | .append("path") 53 | .attr("d", function(d) { return d.path; }) 54 | .on('mouseenter', ...) 55 | .on('mouseout', ...) 56 | ``` 57 | , or in order to draw the regions on a Canvas: 58 | ```javascript 59 | var canvas = document.querySelector("#my-canvas"); 60 | var context = canvas.getContext("2d"); 61 | limitedVoronoi.context(context); //set the context to render to 62 | 63 | context.strokeStyle = 'lightblue'; 64 | context.beginPath(); 65 | limitedVoronoi(data); 66 | context.stroke(); 67 | ``` 68 | 69 | ## Reference 70 | 71 | * d3-voronoi: [https://github.com/d3/d3-voronoi/](https://github.com/d3/d3-voronoi/) 72 | 73 | ## API Reference 74 | 75 | # d3.distanceLimitedVoronoi() 76 | 77 | Creates a new distanceLimitedVoronoi diagram with the default settings: 78 | ```javascript 79 | voronoi = d3.voronoi().extent([[-1e6,-1e6], [1e6,1e6]]); 80 | limit = 20; 81 | context = null; // set it to render to a canvas' 2D context 82 | ``` 83 | 84 | 85 | # distanceLimitedVoronoi(data) 86 | 87 | Computes the distanceLimitedVoronoi tesselation for the specified _data_ points. 88 | 89 | If the _context_ of the layout is null, returns an array of ```{path: , datum: }```, where ```path``` is the adequate region around the datum and ```datum``` is the datum. 90 | 91 | If the _context_ of the layout is defined, the layout is supposed to be drawn on a Canvas. Hence the layout defines a new path, composed of each adequate regions, and return ```true```. Note that the layout doesn't ```stroke()``` (or ```fill()```, or anything else ...) on its own in the case you don't want to fill regions. The use of the produced path remains at your charge. 92 | 93 | 94 | # distanceLimitedVoronoi.limit([radius]) 95 | 96 | If _radius_ is specified, set the _limit_ (ie. maximum distance) of each cell and returns this distanceLimitedVoronoi. If _radius_ is not specified, return the current _limit_, which defaults to ```20```. 97 | 98 | 99 | # distanceLimitedVoronoi.voronoi([voronoi]) 100 | 101 | If _voronoi_ is specified, set the voronoi layout used by the distanceLimitedVoronoi and returns it. If _voronoi_ is not specified, return the currently used voronoi, which defaults to ```d3.voronoi().extent([[-1e6,-1e6], [1e6,1e6]])```. 102 | 103 | 104 | # distanceLimitedVoronoi.context([context]) 105 | 106 | If _context_ is specified, set the context used by the distanceLimitedVoronoi to draw each distance-limited cell, and returns it. The context must be a 2D canvas context (for canvas rendering), or ```null```(for SVG rendering). If _context_ is not specified, return the currently used context, which defaults to ```null```. 107 | 108 | 109 | # distanceLimitedVoronoi.x([callback]) 110 | 111 | Exposes distanceLimitedVoronoi.voronoi().x(...) 112 | 113 | If _callback_ is specified, set the _x_-coordinate accessor and returns this distanceLimitedVoronoi. If _callback_ is not specified, return the current _x_-coordinate accessor, which defaults to ```function(d) { return d[0]; }```. 114 | 115 | 116 | # distanceLimitedVoronoi.y([callback]) 117 | 118 | Exposes distanceLimitedVoronoi.voronoi().y(...) 119 | 120 | If _callback_ is specified, set the _y_-coordinate accessor and returns this distanceLimitedVoronoi. If _callback_ is not specified, return the current _y_-coordinate accessor, which defaults to ```function(d) { return d[1]; }```. 121 | 122 | 123 | # distanceLimitedVoronoi.extent([extent]) 124 | 125 | Exposes distanceLimitedVoronoi.voronoi().extent(...) 126 | 127 | If _extent_ is specified, set the clip extent of the layout to the specified bounds and returns this distanceLimitedVoronoi. The extent bounds are specified as an array [​[x0, y0], [x1, y1]​], where x0 is the left side of the extent, y0 is the top, x1 is the right and y1 is the bottom. If _extent_ is not specified, return the current clip extent, which defaults to ```[[-1e6, -1e6], [1e6,1e6]]```. 128 | -------------------------------------------------------------------------------- /distance-limited-voronoi.js: -------------------------------------------------------------------------------- 1 | d3.distanceLimitedVoronoi = function () { 2 | /////// Internals /////// 3 | var voronoi = d3.voronoi().extent([[-1e6,-1e6], [1e6,1e6]]); 4 | var limit = 20; // default limit 5 | var context = null; // set it to render to a canvas' 2D context 6 | 7 | function _distanceLimitedVoronoi (data) { 8 | if (context!=null) { 9 | //renders into a Canvas 10 | context.beginPath(); 11 | voronoi.polygons(data).forEach(function(cell) { 12 | distanceLimitedCell(cell, limit, context); 13 | }); 14 | return true; 15 | } else { 16 | //final viz is an SVG 17 | return voronoi.polygons(data).map(function(cell) { 18 | return { 19 | path: distanceLimitedCell(cell, limit, d3.path()).toString(), 20 | datum: cell.data 21 | }; 22 | }); 23 | } 24 | } 25 | 26 | /////////////////////// 27 | ///////// API ///////// 28 | /////////////////////// 29 | 30 | _distanceLimitedVoronoi.limit = function(_) { 31 | if (!arguments.length) { return limit; } 32 | if (typeof _ === "number") { 33 | limit = Math.abs(_); 34 | } 35 | 36 | return _distanceLimitedVoronoi; 37 | }; 38 | 39 | _distanceLimitedVoronoi.x = function(_) { 40 | if (!arguments.length) { return voronoi.x(); } 41 | voronoi.x(_); 42 | 43 | return _distanceLimitedVoronoi; 44 | }; 45 | 46 | _distanceLimitedVoronoi.y = function(_) { 47 | if (!arguments.length) { return voronoi.y(); } 48 | voronoi.y(_); 49 | 50 | return _distanceLimitedVoronoi; 51 | }; 52 | 53 | _distanceLimitedVoronoi.extent = function(_) { 54 | if (!arguments.length) { return voronoi.extent(); } 55 | voronoi.extent(_); 56 | 57 | return _distanceLimitedVoronoi; 58 | }; 59 | 60 | //exposes the underlying d3.geom.voronoi 61 | //eg. allows to code 'limitedVoronoi.voronoi().triangle(data)' 62 | _distanceLimitedVoronoi.voronoi = function(_) { 63 | if (!arguments.length) { return voronoi; } 64 | voronoi = _; 65 | 66 | return _distanceLimitedVoronoi; 67 | }; 68 | 69 | _distanceLimitedVoronoi.context = function(_) { 70 | if (!arguments.length) { return context; } 71 | context = _; 72 | 73 | return _distanceLimitedVoronoi; 74 | }; 75 | 76 | /////////////////////// 77 | /////// Private /////// 78 | /////////////////////// 79 | 80 | function distanceLimitedCell (cell, r, context) { 81 | var seed = [voronoi.x()(cell.data), voronoi.y()(cell.data)]; 82 | if (allVertecesInsideMaxDistanceCircle(cell, seed, r)) { 83 | context.moveTo(cell[0][0], cell[0][1]); 84 | for (var j = 1, m = cell.length; j < m; ++j) { 85 | context.lineTo(cell[j][0], cell[j][1]); 86 | } 87 | context.closePath(); 88 | return context; 89 | } else { 90 | var pathNotYetStarted = true; 91 | var firstPointTooFar = pointTooFarFromSeed(cell[0], seed, r); 92 | var p0TooFar = firstPointTooFar; 93 | var p0, p1, intersections; 94 | var openingArcPoint, lastClosingArcPoint; 95 | var startAngle, endAngle; 96 | 97 | //begin: loop through all segments to compute path 98 | for (var iseg=0; isegMath.pow(r, 2)); 205 | } 206 | 207 | function angle(seed, p) { 208 | var v = [p[0] - seed[0], p[1] - seed[1]]; 209 | // from http://stackoverflow.com/questions/2150050/finding-signed-angle-between-vectors, with v1 = horizontal radius = [seed[0]+r - seed[0], seed[0] - seed[0]] 210 | return Math.atan2( v[1], v[0]); 211 | } 212 | } 213 | 214 | function segmentCircleIntersections (A, B, C, r) { 215 | /* 216 | from http://stackoverflow.com/questions/1073336/circle-line-segment-collision-detection-algorithm 217 | */ 218 | var Ax = A[0], Ay = A[1], 219 | Bx = B[0], By = B[1], 220 | Cx = C[0], Cy = C[1]; 221 | 222 | // compute the euclidean distance between A and B 223 | var LAB = Math.sqrt(Math.pow(Bx-Ax, 2)+Math.pow(By-Ay, 2)); 224 | 225 | // compute the direction vector D from A to B 226 | var Dx = (Bx-Ax)/LAB; 227 | var Dy = (By-Ay)/LAB; 228 | 229 | // Now the line equation is x = Dx*t + Ax, y = Dy*t + Ay with 0 <= t <= 1. 230 | 231 | // compute the value t of the closest point to the circle center (Cx, Cy) 232 | var t = Dx*(Cx-Ax) + Dy*(Cy-Ay); 233 | 234 | // This is the projection of C on the line from A to B. 235 | 236 | // compute the coordinates of the point E on line and closest to C 237 | var Ex = t*Dx+Ax; 238 | var Ey = t*Dy+Ay; 239 | 240 | // compute the euclidean distance from E to C 241 | var LEC = Math.sqrt(Math.pow(Ex-Cx, 2)+Math.pow(Ey-Cy, 2)); 242 | 243 | // test if the line intersects the circle 244 | if( LEC < r ) 245 | { 246 | // compute distance from t to circle intersection point 247 | var dt = Math.sqrt(Math.pow(r, 2)-Math.pow(LEC, 2)); 248 | var tF = (t-dt); // t of first intersection point 249 | var tG = (t+dt); // t of second intersection point 250 | 251 | var result = []; 252 | if ((tF>0)&&(tF0)&&(tG