D3.js 鼠标悬停在 html5 canvas 散点图上

D3.js mouseovers on html5 canvas scatter plot

假设我们绘制散点图不是使用 SVG,而是使用 canvas。

所以像这样:

    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]), 10, 0, 2 * Math.PI);
      picker.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);

      context.fill();
      picker.fill();

    });

现在我们如何将数据传递到鼠标悬停事件中,比如绘制工具提示?

我看到的工具提示示例都理所当然地认为您正在处理一个事件,其中数据绑定到鼠标悬停的元素。

但是 canvas 呢?

我假设您需要根据鼠标事件的 x y 坐标使用 d3.bisector 或类似的东西。

一种方法是遍历所有点并检查 x 和 y 是否与点击附近相匹配。当散点图点太多时,这肯定会很慢。{我认为在你的情况下你正在 canvas 中制作散点图只是为了解决这个问题}

另一种方法是利用四叉树。 首先我随机抽取 10000 点。

  sampleData = d3.range(1000).map(function(d) {
    var datapoint = {};
    datapoint.id = "Sample Node " + d;
    datapoint.x = Math.random() * 500;
    datapoint.y = Math.random() * 500;

    return datapoint;
  })

像这样将散点图中的所有点存储在四叉树中。

  quadtree = d3.geom.quadtree()
    .extent([[0,0], [500,500]]) //here 500 is the width and height of the canvas or the max x/y range of the points in scatter chart.
    .x(function(d) {return d.x})
    .y(function(d) {return d.y});

将所有点传递到四叉树中:

  quadData = quadtree(sampleData);

现在单击查找关联的节点数据:

  quadData = quadtree(sampleData);
   d3.select("canvas").on("click", function(){
    found = []; 
    //find in the vicinity of 10 pixel around the click.
    search(quadData, d3.event.pageX -10, d3.event.pageY-10, d3.event.pageX +10, d3.event.pageY+10);
    var message = "";
    //iterate the found and make the message
    found.forEach(function(d){
      message += d.id + " ";
    });
    alert("selected Node" + message);
    var data
  })

最后我的搜索功能检查四叉树矩形中的节点:

function search(quadtree, x0, y0, x3, y3) {
  quadtree.visit(function(node, x1, y1, x2, y2) {
    var p = node.point;
    if (p) {
      p.selected = (p.x >= x0) && (p.x < x3) && (p.y >= y0) && (p.y < y3);
      if(p.selected){
        found.push(p);
      }
    }
    return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
  });
}

点击任何一个圆圈,你会收到它包含的数据的提醒 工作代码 here

我最终使用了 Noah Veltman 建议的解决方案,如下所示:

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
    ];

  });

}

关键是使用

var factory = d3.geom.quadtree() .extent([ [0, 0], [width, height] ]);

其中有一个 getter setter 函数基于每个点的 x, y 值对。