K-nearest neighbors search of a 2D set of points. Move the slider or scroll to change k.
The algorithm was adapted from this block
K-nearest neighbors search of a 2D set of points. Move the slider or scroll to change k.
The algorithm was adapted from this block
| <html> | |
| <head> | |
| <style> | |
| body { | |
| font-family: monospace; | |
| } | |
| .k { | |
| position: absolute; | |
| left: 10px; | |
| top: 10px; | |
| } | |
| .k span { | |
| position: relative; | |
| bottom: 7px; | |
| padding: 0 10px; | |
| } | |
| .hull { | |
| fill: tomato; | |
| fill-opacity: 0.25; | |
| stroke: tomato; | |
| pointer-events: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="k"> | |
| <span>K = 25</span><input type="range" name="k" value="25" min="1"> | |
| </div> | |
| <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
| <script src="k-nearest-neighbors.js"></script> | |
| <script> | |
| var width = 960, | |
| height = 500, | |
| k = 25; | |
| var kDiv = d3.select(".k") | |
| .on("change", function() { | |
| k = +kDiv.select("input").node().value; | |
| d3.select(this).select("span").text("K = " + k); | |
| findNearest.k(k); | |
| }); | |
| var svg = d3.select("body").append("svg") | |
| .attr("width", width) | |
| .attr("height", height); | |
| // add invisible rectangle that will pick up mouse movement | |
| svg.append("rect") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .style("fill-opacity", 0) | |
| .on("mousemove", mousemove) | |
| .on("wheel", function() { | |
| var wheelUp = event.deltaY < 0; | |
| wheelUp ? k++ : k--; | |
| k = clamp(k, 1, 100); // limit k to be between 1 and 100 | |
| kDiv.select("input").node().value = k; | |
| kDiv.select("span").text("K = " + k); | |
| findNearest.k(k); | |
| mousemove.call(this); | |
| }); | |
| var data = d3.range(500) | |
| .map(function(d) { | |
| return { | |
| x: d3.random.normal(width/2, width/8)(), | |
| y: Math.random() * height | |
| }; | |
| }); | |
| var findNearest = d3.kNearestNeighbors() | |
| .extent([[-1, -1], [width + 1, height + 1]]) | |
| .x(function(d) { return d.x; }) | |
| .y(function(d) { return d.y; }) | |
| .k(k) | |
| .data(data); | |
| var hull = svg.append("path").attr("class", "hull"); | |
| var circles = svg.append("g").attr("class", "circles") | |
| .selectAll("circle").data(data) | |
| .enter().append("circle") | |
| .attr("cx", function(d) { return d.x; }) | |
| .attr("cy", function(d) { return d.y; }) | |
| .attr("r", 2) | |
| .style("opacity", 0.5); | |
| function mousemove() { | |
| preventDefault(event); | |
| var nearest = findNearest(d3.mouse(this)); | |
| // Draw convex hull around k-nearest points (if k > 1) | |
| hull.datum(d3.geom.hull(nearest)) | |
| .attr("d", function(d) { | |
| return d.length > 1 ? "M" + d.join("L") + "Z" : null; | |
| }); | |
| // Get corresponding "nearest" data points from original data array | |
| nearest = nearest | |
| .map(function(d) { return data[d.i]; }); | |
| // Color nearest points red | |
| circles | |
| .style("fill", function(d) { | |
| return nearest.indexOf(d) !== -1 ? "red" : null; | |
| }); | |
| } | |
| function clamp(d, min, max) { | |
| return d < min ? min : d > max ? max : d; | |
| } | |
| function preventDefault(e) { | |
| e = e || window.event; | |
| if (e.preventDefault) e.preventDefault(); | |
| e.returnValue = false; | |
| } | |
| </script> | |
| </body> | |
| </html> |
| // algorithm source: http://bl.ocks.org/llb4ll/8709363 | |
| d3.kNearestNeighbors = function() { | |
| var points = [], | |
| nodes, | |
| data, | |
| extent = null, | |
| k = 1, | |
| quadtree = d3.geom.quadtree(), | |
| x = function(d) { return d[0]; }, | |
| y = function(d) { return d[1]; }; | |
| function findNearest(point) { | |
| // TODO: make this more efficient by not recalculating quadtree at | |
| // each call of findNearest() | |
| // Extract points from the data array | |
| points = data.map(function(d) { return [x(d), y(d)]; }); | |
| // Add quadtree info to the points | |
| nodes = quadtreeify(points); | |
| // Flag k-nearest points by adding `selected` property set to `true` | |
| kNearest(new Array(nodes), [], point); | |
| // Return nearest points along with indices from origianl `data` array | |
| return points | |
| .map(function(d, i) { | |
| var datum = [d[0], d[1]]; | |
| datum.i = i; | |
| return d.selected ? datum : null; | |
| }) | |
| .filter(function(d) { return d !== null; }); | |
| } | |
| findNearest.extent = function(_) { | |
| if (!arguments.length) return extent; | |
| extent = _; | |
| quadtree.extent(extent); | |
| return findNearest; | |
| }; | |
| findNearest.data = function(_) { | |
| if (!arguments.length) return data; | |
| data = _; | |
| return findNearest; | |
| }; | |
| findNearest.k = function(_) { | |
| if (!arguments.length) return k; | |
| k = _; | |
| return findNearest; | |
| }; | |
| findNearest.x = function(_) { | |
| if (!arguments.length) return x; | |
| x = _; | |
| return findNearest; | |
| }; | |
| findNearest.y = function(_) { | |
| if (!arguments.length) return y; | |
| y = _; | |
| return findNearest; | |
| }; | |
| return findNearest; | |
| // Add quadtree information to each point (i.e., rectangles, depth, ...) | |
| function quadtreeify(points) { | |
| var nodes = quadtree(points); | |
| nodes.depth = 0; | |
| nodes.visit(function(node, x1, y1, x2, y2) { | |
| node.x1 = x1; | |
| node.y1 = y1; | |
| node.x2 = x2; | |
| node.y2 = y2; | |
| for (var i = 0; i < 4; i++) { | |
| if (node.nodes[i]) node.nodes[i].depth = node.depth + 1; | |
| } | |
| }); | |
| return nodes; | |
| } | |
| // calculate the euclidean distance of two points with coordinates a(ax, ay) and b(bx, by) | |
| function euclideanDistance(ax, ay, bx, by) { | |
| return Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2)); | |
| } | |
| // calculate minimum distance between search point rectangles | |
| function minDistance(x, y, x1, y1, x2, y2) { | |
| var dx1 = x - x1, | |
| dx2 = x - x2, | |
| dy1 = y - y1, | |
| dy2 = y - y2; | |
| // x is between x1 and x2 | |
| if (dx1 * dx2 < 0) { | |
| // (x, y) is inside the rectangle | |
| if (dy1 * dy2 < 0) { | |
| return 0; // return 0 as a point in the rectangle | |
| } | |
| return Math.min(Math.abs(dy1), Math.abs(dy2)); | |
| } | |
| // y is between y1 and y2 (and not inside rectangle) | |
| if (dy1 * dy2 < 0) { | |
| return Math.min(Math.abs(dx1), Math.abs(dx2)); | |
| } | |
| return Math.min( | |
| Math.min(euclideanDistance(x,y,x1,y1), euclideanDistance(x,y,x2,y2)), | |
| Math.min(euclideanDistance(x,y,x1,y2), euclideanDistance(x,y,x2,y1)) | |
| ); | |
| } | |
| // Find the nodes within the specified rectangle (used recursively) | |
| function kNearest(bestQueue, resultQueue, point) { | |
| var x = point[0], | |
| y = point[1]; | |
| // sort children according to their minDistance/euclideanDistance to search point | |
| bestQueue.sort(function(a, b) { | |
| // add minDistance to nodes if not there already | |
| [a, b].forEach(function(d) { | |
| if (d.minDistance === undefined) { | |
| d.scanned = true; | |
| if (d.leaf) { | |
| d.point.scanned = true; | |
| d.minDistance = euclideanDistance(x, y, d.x, d.y); | |
| } | |
| else { | |
| d.minDistance = minDistance(x, y, d.x1, d.y1, d.x2, d.y2); | |
| } | |
| } | |
| }); | |
| return b.minDistance - a.minDistance; | |
| }); | |
| // add nearest leafs (if any) | |
| for (var i = bestQueue.length - 1; i >= 0; i--) { | |
| var elem = bestQueue[i]; | |
| if (elem.leaf) { | |
| elem.point.selected = true; | |
| bestQueue.pop(); | |
| resultQueue.push(elem); | |
| } else { break; } | |
| if (resultQueue.length >= k) break; | |
| } | |
| // check if enough points found | |
| if (resultQueue.length >= k || bestQueue.length == 0) { return; } | |
| else { | |
| // ...otherwise add child nodes to bestQueue and recurse | |
| var visitedNode = bestQueue.pop(); | |
| visitedNode.visited = true; | |
| visitedNode.nodes.forEach(function(d) { | |
| bestQueue.push(d); | |
| }); | |
| kNearest(bestQueue, resultQueue, point); | |
| } | |
| } | |
| } |