block by veltman f539d97e922b918d47e2b2d1a8bcd2dd

Canvas scatterplot with mouse events

Full Screen

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

index.html

<!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>