FlavorScape
FlavorScape

Reputation: 14299

how to add mouse events to force directed graph using the d3 canvas renderer?

All the other examples have mouse events with svg.append().... I don't know where to "enter" to get the arcs in a canvas renderer and add .on('click', function(){}) in v4 style. I want to click to get the value of d. Where do I add the handler in this example? I understand the old way below this example.

Might something like this work? d3.select(canvas).call(d3.mouse()).on("click", ...)

link to working example

var links = d3.range(nodes.length - 1).map(function(i) {
  return {
    source: Math.floor(Math.sqrt(i)),
    target: i + 1
  };
});

var simulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody())
    .force("link", d3.forceLink(links).distance(20).strength(1))
    .force("x", d3.forceX())
    .force("y", d3.forceY())
    .on("tick", ticked);

var canvas = document.querySelector("canvas"),
    context = canvas.getContext("2d"),
    width = canvas.width,
    height = canvas.height;

d3.select(canvas)
    .call(d3.drag()
        .container(canvas)
        .subject(dragsubject)
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

function ticked() {
  context.clearRect(0, 0, width, height);
  context.save();
  context.translate(width / 2, height / 2);

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

  context.beginPath();
  nodes.forEach(drawNode);
  context.fill();
  context.strokeStyle = "#fff";
  context.stroke();

  context.restore();
}

function dragsubject() {
  return simulation.find(d3.event.x - width / 2, d3.event.y - height / 2);
}

function dragstarted() {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d3.event.subject.fx = d3.event.subject.x;
  d3.event.subject.fy = d3.event.subject.y;
}

function dragged() {
  d3.event.subject.fx = d3.event.x;
  d3.event.subject.fy = d3.event.y;
}

function dragended() {
  if (!d3.event.active) simulation.alphaTarget(0);
  d3.event.subject.fx = null;
  d3.event.subject.fy = null;
}

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);
}

old way

var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
  .attr("r", 8)
  .attr("fill", function(d) { return color(d.group); })
  .on("click", togglenode)
  .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended));

Upvotes: 2

Views: 2528

Answers (3)

JAnton
JAnton

Reputation: 1115

Expanded FlavorScape's hack with double click

        d3.select(canvas)
        .call(d3.drag()
            .container(canvas)
            .subject(dragsubject)
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
        var clickDate = new Date();
        var difference_ms;
        function dragstarted(d) {
            if (!d3.event.active) simulation.alphaTarget(0.3).restart();
            d3.event.subject.fx = d3.event.subject.x;
            d3.event.subject.fy = d3.event.subject.y;
            difference_ms = (new Date()).getTime() - clickDate.getTime();
            clickDate = new Date();
            //if clicks less than 200ms apart (double click)
            if(difference_ms < 200)
                console.log( d3.event.subject );
        }

Upvotes: 1

FlavorScape
FlavorScape

Reputation: 14299

Since I used the canvas renderer, I just cheated and used the d3 dragstart event already in the example. There's probably a way to do it like this I'd like to know.

        d3.select(canvas)
        .call(d3.drag()
            .container(canvas)
            .subject(dragsubject)
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));

function dragstarted(d) {
            if (!d3.event.active) simulation.alphaTarget(0.3).restart();
            d3.event.subject.fx = d3.event.subject.x;
            d3.event.subject.fy = d3.event.subject.y;
            //broadcast the selection to parent
            emitter.emit( d3.event.subject );
        }

Upvotes: 2

Gerardo Furtado
Gerardo Furtado

Reputation: 102194

In D3 v4.x, you add a click event pretty much the same way you do in v3.x:

selection.on("click", function(d){
    //do whatever you want with the datum
});

The problem in your question is not v3 versus v4, that's not the issue in the code you shared. The problem with that code is that it uses HTML canvas, not SVG, to render the dataviz.

Unlike SVG, canvas doesn't have a node tree of elements. You cannot "select something" and add an event handler to it.

Think of canvas as a raster image, like a BMP or a JPEG. You can find on what x and y position you clicked, you can even find the colour of that pixel, but you cannot select a given node element, because canvas has none.

For instance, check this tutorial from Nadieh Bremer to see how complicated is to get the circle the user clicks on when you use HTML canvas.

Upvotes: 3

Related Questions