3

I've made a large force layout using D3.v4 and canvas. Because of the size I can't really use SVG.

How can I make tooltips for the elements on the canvas?

At the very least, how can I identify where the mouse is on the canvas?

I have tried to use the answer from this question as inspiration to check where the mouse cursor is on the canvas and then use simulation.find(x,y,radius) to identify nearby nodes. However, that's returning a lot of false positives and I have a feeling that the HTML event and simulation.find() work differently and/or use different coordinate standards.

Does anyone have any ideas or solutions to this?

Edit: It was suggested to take a look at this question. I have tried to do this, and it works, somewhat. The problem now is that, as suspected, simulation.find(..) doesn't seem to use these canvas coordinates to find nodes in the network.

For the curious, this is the current function I use to find the cursor and nearby nodes:

canvas = d3.select('canvas')
function getPos(e) {
    rect = canvas.node().getBoundingClientRect();
    scaleX = width / rect.width;
    scaleY = height / rect.height;
    x = (e.clientX - rect.left);
    y = (e.clientY - rect.top);
    node = simulation.find(x,y, 3);
    return {
        x : x,
        y : y,
        node : node
    };
}
Community
  • 1
  • 1
ThePenguin
  • 123
  • 9

1 Answers1

5

You should use the built-in d3.mouse to figure out position within the canvas:

d3.select("canvas").on("mousemove", function(d){
  var p = d3.mouse(this);
  var node = simulation.find(p[0], p[1]);
});

Here's a quick example which highlights the closest node to the mouse position:

<!DOCTYPE html>
<meta charset="utf-8">
<canvas width="960" height="600"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
  var canvas = document.querySelector("canvas"),
    context = canvas.getContext("2d"),
    width = canvas.width,
    height = canvas.height;

  var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) {
      return d.id;
    }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));


  var graph = {};
  graph.nodes = d3.range(100).map(function(d) {
    return {
      id: d
    }
  });
  graph.links = d3.range(200).map(function(d) {
    return {
      source: Math.floor(Math.random() * 100),
      target: Math.floor(Math.random() * 100)
    };
  });

  simulation
    .nodes(graph.nodes)
    .on("tick", ticked);

  simulation.force("link")
    .links(graph.links);

  function ticked() {
    context.clearRect(0, 0, width, height);

    context.beginPath();
    graph.links.forEach(drawLink);
    context.strokeStyle = "#aaa";
    context.stroke();

    context.beginPath();
    graph.nodes.forEach(drawNode);
    context.fill();
    context.strokeStyle = "#fff";
    context.stroke();
    
    if (closeNode) {
      context.beginPath();
      drawNode(closeNode)
      context.fill();
      context.strokeStyle = "#ff0000";
      context.stroke();
    }
  }
  
  var closeNode;
  d3.select("canvas").on("mousemove", function(d){
    var p = d3.mouse(this);
    closeNode = simulation.find(p[0], p[1]);
    ticked();
  })

  function drawLink(d) {
    context.moveTo(d.source.x, d.source.y);
    context.lineTo(d.target.x, d.target.y);
  }

  function drawNode(d) {
    context.moveTo(d.x + 3, d.y);
    context.arc(d.x, d.y, 3, 0, 2 * Math.PI);
  }
</script>
Mark
  • 106,305
  • 20
  • 172
  • 230
  • Hey Mark! I am facing a similar problem. Your approach works perfect. But, when I zoom in/out the network, then `simulation.find()` does not detect the correct node anymore. I tried to grab the current zoom level and update `d3.mouse(this)` coordinates by doing this `p[0]* currentZoom.k + currentZoom.x`, but not succesfully for the time being. Any recommendations? – user_jt May 16 '19 at 15:48
  • 2
    @user3204834, you can use [transform.invert](https://github.com/d3/d3-zoom#transform_invert) to map the point to the transformed space. Update fiddle [here](https://jsfiddle.net/jyrzq1sL/). – Mark May 17 '19 at 00:42
  • Oh, that's correct! I needed the inverse transformation and not a new transformation. Thank you very much! – user_jt May 17 '19 at 08:25