【问题标题】:Adding new nodes to Force-directed layout将新节点添加到强制导向布局
【发布时间】:2012-03-21 07:26:47
【问题描述】:

关于 Stack Overflow 的第一个问题,请耐心等待!我是 d3.js 的新手,但一直对其他人能够用它完成的事情感到惊讶......几乎同样对我自己能够取得的进展感到惊讶!显然我不是在摸索什么,所以我希望这里的好心人能给我带来光明。

我的目的是制作一个可重复使用的 javascript 函数,它只需执行以下操作:

  • 在指定的 DOM 元素中创建一个空白的力导向图
  • 允许您在该图中添加和删除带标签的图像节点,并指定它们之间的连接

我以http://bl.ocks.org/950642 为起点,因为这基本上是我希望能够创建的那种布局:

我的代码如下所示:

<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="underscore-min.js"></script>
    <script type="text/javascript" src="d3.v2.min.js"></script>
    <style type="text/css">
        .link { stroke: #ccc; }
        .nodetext { pointer-events: none; font: 10px sans-serif; }
        body { width:100%; height:100%; margin:none; padding:none; }
        #graph { width:500px;height:500px; border:3px solid black;border-radius:12px; margin:auto; }
    </style>
</head>
<body>
<div id="graph"></div>
</body>
<script type="text/javascript">

function myGraph(el) {

    // Initialise the graph object
    var graph = this.graph = {
        "nodes":[{"name":"Cause"},{"name":"Effect"}],
        "links":[{"source":0,"target":1}]
    };

    // Add and remove elements on the graph object
    this.addNode = function (name) {
        graph["nodes"].push({"name":name});
        update();
    }

    this.removeNode = function (name) {
        graph["nodes"] = _.filter(graph["nodes"], function(node) {return (node["name"] != name)});
        graph["links"] = _.filter(graph["links"], function(link) {return ((link["source"]["name"] != name)&&(link["target"]["name"] != name))});
        update();
    }

    var findNode = function (name) {
        for (var i in graph["nodes"]) if (graph["nodes"][i]["name"] === name) return graph["nodes"][i];
    }

    this.addLink = function (source, target) {
        graph["links"].push({"source":findNode(source),"target":findNode(target)});
        update();
    }

    // set up the D3 visualisation in the specified element
    var w = $(el).innerWidth(),
        h = $(el).innerHeight();

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

    var force = d3.layout.force()
        .nodes(graph.nodes)
        .links(graph.links)
        .gravity(.05)
        .distance(100)
        .charge(-100)
        .size([w, h]);

    var update = function () {

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

        link.enter().insert("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.target.x; })
            .attr("y2", function(d) { return d.target.y; });

        link.exit().remove();

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

        node.enter().append("g")
            .attr("class", "node")
            .call(force.drag);

        node.append("image")
            .attr("class", "circle")
            .attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
            .attr("x", "-8px")
            .attr("y", "-8px")
            .attr("width", "16px")
            .attr("height", "16px");

        node.append("text")
            .attr("class", "nodetext")
            .attr("dx", 12)
            .attr("dy", ".35em")
            .text(function(d) { return d.name });

        node.exit().remove();

        force.on("tick", function() {
          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; });

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

        // Restart the force layout.
        force
          .nodes(graph.nodes)
          .links(graph.links)
          .start();
    }

    // Make it all go
    update();
}

graph = new myGraph("#graph");

// These are the sort of commands I want to be able to give the object.
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");

</script>
</html>

每次我添加一个新节点时,它都会重新标记所有现有节点;这些堆积在一起,事情开始变得丑陋。我明白这是为什么:因为当我在添加新节点时调用update() 函数时,它会对整个数据集执行node.append(...)。我无法弄清楚如何仅对我正在添加的节点执行此操作...而且我显然只能使用node.enter() 创建一个新元素,所以这不起作用对于我需要绑定到节点的其他元素。我该如何解决这个问题?

感谢您就这个问题提供的任何指导!

已编辑,因为我很快修复了之前提到的其他几个错误的来源

【问题讨论】:

    标签: javascript d3.js force-layout


    【解决方案1】:

    在长时间无法正常工作之后,我终于偶然发现了一个我认为与任何文档都没有关联的演示:http://bl.ocks.org/1095795:

    这个演示包含最终帮助我解决问题的关键。

    enter() 上添加多个对象可以通过将enter() 分配给一个变量,然后附加到该变量来完成。这是有道理的。第二个关键部分是节点和链接数组必须基于force()——否则随着节点的删除和添加,图和模型将不同步。

    这是因为如果改为构造一个新数组,它将缺少以下attributes

    • index - 节点数组中节点的从零开始的索引。
    • x - 当前节点位置的 x 坐标。
    • y - 当前节点位置的 y 坐标。
    • px - 前一个节点位置的 x 坐标。
    • py - 前一个节点位置的 y 坐标。
    • fixed - 一个布尔值,指示节点位置是否被锁定。
    • weight - 节点权重;关联链接的数量。

    调用force.nodes() 并不严格需要这些属性,但如果这些属性不存在,那么它们将由force.start() 在第一次调用时随机初始化。

    如果有人好奇,工作代码如下所示:

    <script type="text/javascript">
    
    function myGraph(el) {
    
        // Add and remove elements on the graph object
        this.addNode = function (id) {
            nodes.push({"id":id});
            update();
        }
    
        this.removeNode = function (id) {
            var i = 0;
            var n = findNode(id);
            while (i < links.length) {
                if ((links[i]['source'] === n)||(links[i]['target'] == n)) links.splice(i,1);
                else i++;
            }
            var index = findNodeIndex(id);
            if(index !== undefined) {
                nodes.splice(index, 1);
                update();
            }
        }
    
        this.addLink = function (sourceId, targetId) {
            var sourceNode = findNode(sourceId);
            var targetNode = findNode(targetId);
    
            if((sourceNode !== undefined) && (targetNode !== undefined)) {
                links.push({"source": sourceNode, "target": targetNode});
                update();
            }
        }
    
        var findNode = function (id) {
            for (var i=0; i < nodes.length; i++) {
                if (nodes[i].id === id)
                    return nodes[i]
            };
        }
    
        var findNodeIndex = function (id) {
            for (var i=0; i < nodes.length; i++) {
                if (nodes[i].id === id)
                    return i
            };
        }
    
        // set up the D3 visualisation in the specified element
        var w = $(el).innerWidth(),
            h = $(el).innerHeight();
    
        var vis = this.vis = d3.select(el).append("svg:svg")
            .attr("width", w)
            .attr("height", h);
    
        var force = d3.layout.force()
            .gravity(.05)
            .distance(100)
            .charge(-100)
            .size([w, h]);
    
        var nodes = force.nodes(),
            links = force.links();
    
        var update = function () {
    
            var link = vis.selectAll("line.link")
                .data(links, function(d) { return d.source.id + "-" + d.target.id; });
    
            link.enter().insert("line")
                .attr("class", "link");
    
            link.exit().remove();
    
            var node = vis.selectAll("g.node")
                .data(nodes, function(d) { return d.id;});
    
            var nodeEnter = node.enter().append("g")
                .attr("class", "node")
                .call(force.drag);
    
            nodeEnter.append("image")
                .attr("class", "circle")
                .attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
                .attr("x", "-8px")
                .attr("y", "-8px")
                .attr("width", "16px")
                .attr("height", "16px");
    
            nodeEnter.append("text")
                .attr("class", "nodetext")
                .attr("dx", 12)
                .attr("dy", ".35em")
                .text(function(d) {return d.id});
    
            node.exit().remove();
    
            force.on("tick", function() {
              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; });
    
              node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
            });
    
            // Restart the force layout.
            force.start();
        }
    
        // Make it all go
        update();
    }
    
    graph = new myGraph("#graph");
    
    // You can do this from the console as much as you like...
    graph.addNode("Cause");
    graph.addNode("Effect");
    graph.addLink("Cause", "Effect");
    graph.addNode("A");
    graph.addNode("B");
    graph.addLink("A", "B");
    
    </script>
    

    【讨论】:

    • 在添加新数据时使用force.start() 而不是force.resume() 是关键。非常感谢!
    • 这太棒了。如果它自动缩放缩放级别(可能会减少电荷直到一切都适合?)所以一切都适合它正在绘制的盒子的大小。
    • +1 用于干净的代码示例。我比 Bostock 先生的例子更喜欢它,因为它展示了如何将行为封装在对象中。做得好。 (考虑将其添加到 D3 示例库中?)
    • 太美了!我正在学习如何将 forceGraph 与 d3 一起使用几天,这是我见过的最漂亮的方法。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-10
    • 2015-05-01
    • 2012-02-23
    • 2012-03-01
    • 1970-01-01
    • 2014-11-20
    相关资源
    最近更新 更多