Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active May 16, 2019 17:15
Show Gist options
  • Save veltman/1b43f61887e89c371f1c8c73341540a3 to your computer and use it in GitHub Desktop.
Save veltman/1b43f61887e89c371f1c8c73341540a3 to your computer and use it in GitHub Desktop.
Canvas scatterplot with quadtree

Canvas scatterplot w/ quadtree for point picking on hover, per: https://twitter.com/mbostock/status/733317794419212288

If you don't want the full Voronoi effect because your chart has some big dead zones, you could add a distance threshold and only trigger the highlighting when the nearest point is within that distance.

If you need behavior that more closely mimics mouse/touch because your shapes have significant dimensions or their layer order matters, you could try colorpicking instead.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
padding: 0;
font: 12px sans-serif;
}
path,
line {
shape-rendering: crispEdges;
}
div,
svg,
canvas {
position: absolute;
}
svg {
pointer-events: none;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
stroke-width: 1px;
}
circle {
stroke-width: 4px;
stroke: #000;
fill: none;
}
.hidden {
display: none;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script>
var margin = {top: 20, right: 10, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + " " + margin.top + ")");
var factory = d3.geom.quadtree()
.extent([
[0, 0],
[width, height]
]);
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var xg = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
var yg = svg.append("g")
.attr("class", "y axis");
var chartArea = d3.select("body").append("div")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var canvas = chartArea.append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
context.fillStyle = "#f0f";
// Layer on top of canvas, example of selection details
var highlight = chartArea.append("svg")
.attr("width", width)
.attr("height", height)
.append("circle")
.attr("r", 7)
.classed("hidden", true);
redraw();
function redraw() {
// Randomize the scale
var scale = 1 + Math.floor(Math.random() * 10);
// Redraw axes
x.domain([0, scale]);
y.domain([0, scale]);
xg.call(xAxis);
yg.call(yAxis);
var points = randomPoints(scale);
var tree = factory(points);
// Update canvas
context.clearRect(0, 0, width, height);
points.forEach(function(p,i){
context.beginPath();
context.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
});
canvas.on("mousemove",function(){
var mouse = d3.mouse(this),
closest = tree.find([x.invert(mouse[0]), y.invert(mouse[1])]);
highlight.attr("cx", x(closest[0]))
.attr("cy", y(closest[1]));
});
canvas.on("mouseover",function(){
highlight.classed("hidden", false);
});
canvas.on("mouseout",function(){
highlight.classed("hidden", true);
});
}
function randomPoints(scale) {
// Get points
return d3.range(1000).map(function(d){
return [
Math.random() * scale,
Math.random() * scale
];
});
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment