Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active September 30, 2018 02:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save veltman/f539d97e922b918d47e2b2d1a8bcd2dd to your computer and use it in GitHub Desktop.
Save veltman/f539d97e922b918d47e2b2d1a8bcd2dd to your computer and use it in GitHub Desktop.
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