Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active Sep 30, 2018
Embed
What would you like to do?
Canvas scatterplot with mouse events

Canvas scatterplot w/ colorpicking for mouse events. Renders lots of dots on a <canvas> for speed, but uses a hidden canvas with a color-to-data dictionary to get the value under a mouse event. Hover over some dots.

Using nonrectangular shapes creates the chance of weirdness from antialiasing when triggered on the very edge of a shape. If you wanted to prevent them entirely you could verify that the resulting data center point is within r pixels of the current mouse position or you could sample additional pixels near the event.

This approach handles layer order and weird shapes, but if your points are all tiny dots, a quadtree approach is probably faster and more effective.

See also: Needles, Haystacks, and the Canvas API, WebGL Beginner's Guide - Picking

<!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 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 offscreen = d3.select(document.createElement("canvas"))
.attr("width", width)
.attr("height", height)
var context = canvas.node().getContext("2d"),
picker = offscreen.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),
colors = {};
// Update canvas
context.clearRect(0, 0, width, height);
picker.clearRect(0, 0, width, height);
points.forEach(function(p,i){
// Space out the colors a bit
var color = getColor(i * 1000 + 1);
colors[color] = p;
picker.fillStyle = "rgb(" + color + ")";
context.beginPath();
picker.beginPath();
context.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
picker.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
picker.fill();
});
canvas.on("mousemove",function(){
var xy = d3.mouse(this);
// Get pixel from offscreen canvas
var color = picker.getImageData(xy[0], xy[1], 1, 1).data;
selected = colors[color.slice(0,3).toString()];
highlight.classed("hidden", !selected);
// If it matches a point, highlight it
if (selected) {
highlight.attr("cx", x(selected[0]))
.attr("cy", y(selected[1]));
}
});
}
function randomPoints(scale) {
// Get points
return d3.range(1000).map(function(d){
return [
Math.random() * scale,
Math.random() * scale
];
});
}
function getColor(i) {
return (i % 256) + "," + (Math.floor(i / 256) % 256) + "," + (Math.floor(i / 65536) % 256);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment