kellyh
kellyh

Reputation: 113

Dragging and panning in d3 force layout

I'm working on a force layout graph that displays relationships of writers. Since there are so many, I tried to implement zooming and dragging. Zooming works fine (with one exception), but when I drag a node it also drags the background. I tried following Mike Bostock's directions here and the StackOverflow question paired with it, but it still won't work. I based most of the code for the graph on this, which works beautifully, but since he used an older version of d3, his dragging breaks in the new version. (I can't just use the older version of d3 because I have some other parts of the graph not shown here that work only with the newer version.)

I think the problem has something to do with my grouping of SVG objects, but I also can't figure out what I'm doing wrong there. This also brings in the one zooming problem; when I zoom in or pan around, the legend also moves and zooms in. If there's an easy fix to make it stay still and sort of "hover" above the graph, that would be great.

I'm very new to coding, so I'm probably making really stupid mistakes, but any help would be appreciated.

Fiddle.

var graphData = {
nodes: [
  {
    id:0,
    name:"Plotinus"
  },
  {
    id:1,
    name:"Iamblichus"
  },
  {
    id:2,
    name:"Porphyry"
  }
],
links: [
  {
    relationship:"Teacher/student",
    source:0,
    target:1
  },
  {
    relationship:"Enemies",
    source:0,
    target:2
  },
  {
    relationship:"Family",
    source:1,
    target:2
  }
 ]
};


var linkColor = d3.scale.category10(); //Sets the color for links

var drag = d3.behavior.drag()
    .on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
    .on("drag", function(d) {
        d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
    });

var w = 300,
    h = 300;

var vis = d3.select(".graph")
    .append("svg:svg")
    .attr("width", w)
    .attr("height", h)
    .attr("pointer-events", "all")
    .append('svg:g')
    .call(d3.behavior.zoom().on("zoom", redraw))
    .append('svg:g');

vis.append('svg:rect')
    .attr('width', w)
    .attr('height', h)
    .attr('fill', 'rgba(1,1,1,0)');

function redraw() {
    vis.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); } 

    var force = d3.layout.force()
        .gravity(.6)
        .charge(-600)
        .linkDistance( 60 )
        .size([w, h]);

    var svg = d3.select(".text").append("svg")
        .attr("width", w)
        .attr("height", h);

        var link = vis.selectAll("line")
            .data(graphData.links)
            .enter().append("line")
            .style("stroke", function(d) { return linkColor(d.relationship); })
            .style("stroke-width", 1)
            .attr("class", "connector");

        var node = vis.selectAll("g.node")
            .data(graphData.nodes)
            .enter().append("svg:g")
            .attr("class","node")
            .call(force.drag);

            node.append("svg:circle")
              .attr("r", 10) //Adjusts size of nodes' radius
              .style("fill", "#ccc"); 

            node.append("svg:text")
                .attr("text-anchor", "middle") 
                .attr("fill","black")
                .style("pointer-events", "none") 
                .attr("font-size", "9px")
                .attr("font-weight", "100")
                .attr("font-family", "sans-serif")
                .text( function(d) { return d.name;} );


// Adds the legend.   
      var legend = vis.selectAll(".legend")
          .data(linkColor.domain().slice().reverse())
        .enter().append("g")
          .attr("class", "legend")
          .attr("transform", function(d, i) { return "translate(-10," + i * 20 + ")"; });

      legend.append("rect")
          .attr("x", w - 18)
          .attr("width", 18)
          .attr("height", 18)
          .style("fill", linkColor);

      legend.append("text")
          .attr("x", w - 24)
          .attr("y", 9)
          .attr("dy", ".35em")
          .attr("class", "legendText")
          .style("text-anchor", "end")
          .text(function(d) { return d; });

    force
        .nodes(graphData.nodes)
        .links(graphData.links)
        .on("tick", tick)
        .start();

  function tick() {
    node.attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
        .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")";});

    link.attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });
  }

Upvotes: 0

Views: 3699

Answers (4)

Joseph Ching
Joseph Ching

Reputation: 1

With your code, the node can be dragged but when you drag a node other nodes will move too. I come up this to stop rest of nodes and just let you finished dragging then re-generated the whole graph

function dragstarted(d) {
   d3.event.sourceEvent.stopPropagation();
   d3.select(this).classed("fixed", d.fixed = true);
}

function dragged(d) {
   force.stop();
   d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
   tick();
}

function dragended(d) {
   force.resume();
}

Upvotes: 0

Tomasz Werszko
Tomasz Werszko

Reputation: 359

Here is what is working for me:

const zoom = d3.behavior.zoom()
                .scaleExtent([.1, 10])
                .on('zoom', zoomed);

const force = d3.layout.force()
                .(...more stuff...);

const svg = d3.select('.some-parent-div')
            .append('svg')
            .attr('class', 'graph-container')
            .call(zoom);

const mainGroup = svg.append('g');

var node = mainGroup.selectAll('.node');

node.enter()
    .insert('g')
    .attr('class', 'node')
    .call(force.drag)
    .on('mousedown', function(){
        // line below is the key to make it work
        d3.event.stopPropagation();
    })
    .(...more stuff...);

function zoomed(){
    force.stop();
    const canvasTranslate = zoom.translate();
    mainGroup.attr('transform', 'translate('+canvasTranslate[0]+','+canvasTranslate[1]+')scale(' + zoom.scale() + ')');
    force.resume();
}

Upvotes: 0

kellyh
kellyh

Reputation: 113

I think I figured it out.

I had to combine the instructions from here and here, which was sort of already answered in the answer I linked.

My old way, grabbed from the first example, looked like this:

var drag = d3.behavior.drag()
    .on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
    .on("drag", function(d) {
        d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
    });

The problem was that I was focusing on d3.behavior.drag() instead of force.drag, which I think Stephen Thomas was trying to tell me. It should look like this:

//code code code//

function dragstarted(d) {
   d3.event.sourceEvent.stopPropagation();
   d3.select(this).classed("dragging", true);
}

function dragged(d) {
   d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}

function dragended(d) {
   d3.select(this).classed("dragging", false);
}

//code code code//

var drag = force.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);

Upvotes: 2

Stephen Thomas
Stephen Thomas

Reputation: 14053

You can use the drag() method of the force object instead of creating a separate drag behavior. Something like:

node.call(force.drag);

or, equivalently,

force.drag(node);

A complete example is available at http://bl.ocks.org/sathomas/a7b0062211af69981ff3

Upvotes: 1

Related Questions