D3 force directed layout with bounding box

折月煮酒 提交于 2019-11-26 09:23:13

问题


I am new to D3 and having trouble setting the bounds for my force directed layout. I have managed to piece together (from examples) what I would like, but I need the graph to be contained. In the tick function, a transform/translate will display my graph correctly, but when i use cx and cy with Math.max/min (See commented code), the nodes are pinned to the top left corner while the lines are contained properly.

Here is what I have below... what am I doing wrong??

var w=960, h=500, r=8,  z = d3.scale.category20();

var color = d3.scale.category20();

var force = d3.layout.force()
       .linkDistance( function(d) { return (d.value*180) } )
       .linkStrength( function(d) { return (1/(1+d.value)) } )
       .charge(-1000)
       //.gravity(.08)
       .size([w, h]);

var vis = d3.select(\"#chart\").append(\"svg:svg\")
       .attr(\"width\", w)
       .attr(\"height\", h)
       .append(\"svg:g\")
       .attr(\"transform\", \"translate(\" + w / 4 + \",\" + h / 3 + \")\");

vis.append(\"svg:rect\")
   .attr(\"width\", w)
   .attr(\"height\", h)
   .style(\"stroke\", \"#000\");


d3.json(\"miserables.json\", function(json) {

       var link = vis.selectAll(\"line.link\")
               .data(json.links);

       link.enter().append(\"svg:line\")
               .attr(\"class\", \"link\")
               .attr(\"x1\", function(d) { return d.source.x; })
               .attr(\"y1\", function(d) { return d.source.y; })
               .attr(\"x2\", function(d) { return d.source.x; })
               .attr(\"y2\", function(d) { return d.source.y; })
               .style(\"stroke-width\", function(d) { return (1/(1+d.value))*5 });

       var node = vis.selectAll(\"g.node\")
               .data(json.nodes);

       var nodeEnter = node.enter().append(\"svg:g\")
               .attr(\"class\", \"node\")
               .on(\"mouseover\", fade(.1))
               .on(\"mouseout\", fade(1))
               .call(force.drag);

       nodeEnter.append(\"svg:circle\")
               .attr(\"r\", r)
               .style(\"fill\", function(d) { return z(d.group); })
               .style(\"stroke\", function(d) { return
d3.rgb(z(d.group)).darker(); });

       nodeEnter.append(\"svg:text\")
               .attr(\"text-anchor\", \"middle\")
               .attr(\"dy\", \".35em\")
               .text(function(d) { return d.name; });

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

       function tick() {

       // This works
               node.attr(\"transform\", function(d) { return \"translate(\" + d.x + \",\"
+ d.y + \")\"; });

       // This contains the lines within the boundary, but the nodes are
stuck in the top left corner
               //node.attr(\"cx\", function(d) { return d.x = Math.max(r, Math.min(w
- r, d.x)); })
               //      .attr(\"cy\", function(d) { return d.y = Math.max(r, Math.min(h -
r, 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; });
       }

       var linkedByIndex = {};

   json.links.forEach(function(d) {
       linkedByIndex[d.source.index + \",\" + d.target.index] = 1;
   });

       function isConnected(a, b) {
       return linkedByIndex[a.index + \",\" + b.index] ||
linkedByIndex[b.index + \",\" + a.index] || a.index == b.index;
   }

       function fade(opacity) {
       return function(d) {
           node.style(\"stroke-opacity\", function(o) {
                       thisOpacity = isConnected(d, o) ? 1 : opacity;
                       this.setAttribute(\'fill-opacity\', thisOpacity);
               return thisOpacity;
                       });

                       link.style(\"stroke-opacity\", opacity).style(\"stroke-opacity\",
function(o) {
               return o.source === d || o.target === d ? 1 : opacity;
               });
       };
       }

});

回答1:


There's a bounding box example in my talk on force layouts. The position Verlet integration allows you to define geometric constraints (such as bounding boxes and collision detection) inside the "tick" event listener; simply move the nodes to comply with the constraint and the simulation will adapt accordingly.

That said, gravity is definitely a more flexible way to deal with this problem, since it allows users to drag the graph outside the bounding box temporarily and then the graph will recover. Depend on the size of the graph and the size of the displayed area, you should experiment with different relative strengths of gravity and charge (repulsion) to get your graph to fit.




回答2:


A custom force is a possible solution too. I like this approch more since not only the displayed nodes are repositioned but the whole simulation works with the bounding force.

let simulation = d3.forceSimulation(nodes)
    ...
    .force("bounds", boxingForce);

// Custom force to put all nodes in a box
function boxingForce() {
    const radius = 500;

    for (let node of nodes) {
        // Of the positions exceed the box, set them to the boundary position.
        // You may want to include your nodes width to not overlap with the box.
        node.x = Math.max(-radius, Math.min(radius, node.x));
        node.y = Math.max(-radius, Math.min(radius, node.y));
    }
}



回答3:


The commented code works on node which is, from your definition, a svg g(rouping) element and does not operate the cx/cy attributes. Select the circle element inside node to make these attributes come alive:

node.select("circle") // select the circle element in that node
    .attr("cx", function(d) { return d.x = Math.max(r, Math.min(w - r, d.x)); })
    .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h - r, d.y)); });


来源:https://stackoverflow.com/questions/9573178/d3-force-directed-layout-with-bounding-box

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!