├── 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 | 
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