d3.js:如何在强制布局中更新链接数据时删除节点 [英] d3.js: How to remove nodes when link-data updates in a force layout

查看:32
本文介绍了d3.js:如何在强制布局中更新链接数据时删除节点的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用强制布局图来显示网络,但在更新数据时遇到问题.

我已经检查了如何更新元素当底层数据发生变化时 D3 强制布局,当然还有 D3.js 中的mbostock"的修改强制布局"以及通用更新模式"(不幸的是,我最多只能发两个链接...)

我的代码基于移动专利诉讼"示例,但有一些修改和差异.你可以在这里查看我的完整代码:

<meta charset="utf-8"><风格>.关联 {填充:无;笔画:#666;笔画宽度:1.5px;}#许可{填充:绿色;}.link.licensing {笔画:绿色;}.link.resolved {中风-dasharray:0,2 1;}圆圈 {填写:#ccc;笔画:#333;笔画宽度:1.5px;}文本 {字体:10px 无衬线;指针事件:无;文本阴影:0 1px 0 #fff,1px 0 0 #fff,0 -1px 0 #fff,-1px 0 0 #fff;}</风格><身体><!-- 添加更新按钮--><div id="更新"><input name="updateButton" type="button" value="Update" onclick="newData()"/>

<script src="http://d3js.org/d3.v3.min.js"></script><脚本>无功宽度= 960,高度 = 500;var force = d3.layout.force().size([宽度, 高度]).link距离(60).charge(-300).on("滴答",滴答);var svg = d3.select("body").append("svg").attr("宽度", 宽度).attr("高度", 高度).style("边框", "1px 纯黑色");var 数据集 = [{来源:微软",目标:亚马逊",类型:许可"},{来源:微软",目标:HTC",类型:许可"},{来源:三星",目标:苹果",类型:西装"},{来源:摩托罗拉",目标:苹果",类型:西装"},{来源:诺基亚",目标:苹果",类型:已解决"},{来源:HTC",目标:苹果",类型:西装"},{来源:柯达",目标:苹果",类型:西装"},{来源:Microsoft",目标:Barnes & Noble",类型:suit"},{来源:微软",目标:富士康",类型:西装"},{来源:甲骨文",目标:谷歌",类型:西装"},{来源:苹果",目标:HTC",类型:西装"},{来源:微软",目标:英业达",类型:西装"},{来源:三星",目标:柯达",类型:已解决"},{来源:LG",目标:柯达",类型:已解决"},{来源:RIM",目标:柯达",类型:西装"},{来源:索尼",目标:LG",类型:西装"},{来源:柯达",目标:LG",类型:已解决"},{来源:苹果",目标:诺基亚",类型:已解决"},{来源:高通",目标:诺基亚",类型:已解决"},{来源:苹果",目标:摩托罗拉",类型:西装"},{来源:微软",目标:摩托罗拉",类型:西装"},{来源:摩托罗拉",目标:微软",类型:西装"},{来源:华为",目标:中兴",类型:西装"},{来源:爱立信",目标:中兴",类型:西装"},{来源:柯达",目标:三星",类型:已解决"},{来源:苹果",目标:三星",类型:西装"},{来源:柯达",目标:RIM",类型:西装"},{来源:诺基亚",目标:高通",类型:西装"}];var path = svg.append("g").selectAll("path"),circle = svg.append("g").selectAll("circle"),text = svg.append("g").selectAll("text"),标记 = svg.append("defs").selectAll("marker");var 节点 = {};更新(数据集);函数 newData(){var newDataset = [{来源:微软",目标:亚马逊",类型:许可"},{来源:微软",目标:HTC",类型:许可"},{来源:三星",目标:苹果",类型:西装"},];更新(新数据集);}功能更新(链接){//从链接计算不同的节点.链接.forEach(功能(链接){link.source = 节点[link.source] ||(节点[link.source] = {name: link.source});link.target = 节点[link.target] ||(节点[link.target] = {name: link.target});});力量.nodes(d3.values(nodes)).links(链接).开始();//-------------------------------//计算数据连接.这将返回更新选择.标记 = 标记.数据([西装",许可",已解决"]);//删除任何传出/旧标记.标记.exit().remove();//计算用于输入和更新标记的新属性.标记.输入().附加(标记").attr("id", function(d) { return d; }).attr("viewBox", "0 -5 10 10").attr("refX", 15).attr("refY", -1.5).attr("markerWidth", 6).attr("markerHeight", 6).attr(东方",自动").append("line")//使用 ".append("path") 作为'箭头'.attr("d", "M0,-5L10,0L0,5");//-------------------------------//计算数据连接.这将返回更新选择.path = path.data(force.links());//删除所有传出/旧路径.path.exit().remove();//计算用于输入和更新路径的新属性.path.enter().append("path").attr("class", function(d) { return "link" + d.type; }).attr("marker-end", function(d) { return "url(#" + d.type + ")"; });//-------------------------------//计算数据连接.这将返回更新选择.circle = circle.data(force.nodes());//添加任何传入的圆圈.circle.enter().append("circle");//删除所有传出/旧圈子.circle.exit().remove();//计算用于输入和更新圆的新属性.圆圈.attr("r", 6).call(force.drag);//-------------------------------//计算数据连接.这将返回更新选择.text = text.data(force.nodes());//添加任何传入的文本.text.enter().append("text");//删除任何传出/旧文本.text.exit().remove();//计算用于输入和更新文本的新属性.文本.attr("x", 8).attr("y", ".31em").text(function(d) { return d.name; });}//使用椭圆弧路径段对方向性进行双重编码.函数滴答(){path.attr("d", linkArc);circle.attr("变换",变换);text.attr("转换", 转换);}函数linkArc(d){var dx = d.target.x - d.source.x,dy = d.target.y - d.source.y,dr = Math.sqrt(dx * dx + dy * dy);返回 "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," +d.target.y;}函数变换(d){返回 "translate(" + d.x + "," + d.y + ")";}

我的代码的 JSFiddle 可以在这里找到:http://jsfiddle.net/5m8a9/

按下更新"按钮后,我想动态更新我的图表.到目前为止一切顺利,问题是,只有路径会被更新,而不会更新圆圈或文本(一些圆圈和相应的文本仍然存在并且不会被删除),如您所见我的 JSFiddle 链接.我前几天试图找出问题所在,但没有成功.

我缺少什么以及如何使我的代码按方面工作?

如果有人能提供帮助,我将永远感激不尽.

编辑以添加@AmeliaBR 提供的最终解决方案:

这是我最终解决方案的漏洞代码:

<meta charset="utf-8"><风格>.关联 {填充:无;笔画:#666;笔画宽度:1.5px;}#许可{填充:绿色;}.link.licensing {笔画:绿色;}.link.resolved {中风-dasharray:0,2 1;}圆圈 {填写:#ccc;笔画:#333;笔画宽度:1.5px;}文本 {字体:10px 无衬线;指针事件:无;文本阴影:0 1px 0 #fff,1px 0 0 #fff,0 -1px 0 #fff,-1px 0 0 #fff;}</风格><身体><!-- 添加更新按钮--><div id="更新"><input name="updateButton" type="button" value="Update" onclick="newData()"/>

<script src="http://d3js.org/d3.v3.min.js"></script><脚本>无功宽度= 960,高度 = 500;var force = d3.layout.force().size([宽度, 高度]).link距离(60).charge(-300).on("滴答",滴答);var svg = d3.select("body").append("svg").attr("宽度", 宽度).attr("高度", 高度).style("边框", "1px 纯黑色");var 数据集 = [{来源:微软",目标:亚马逊",类型:许可"},{来源:微软",目标:HTC",类型:许可"},{来源:三星",目标:苹果",类型:西装"},{来源:摩托罗拉",目标:苹果",类型:西装"},{来源:诺基亚",目标:苹果",类型:已解决"},{来源:HTC",目标:苹果",类型:西装"},{来源:柯达",目标:苹果",类型:西装"},{来源:Microsoft",目标:Barnes & Noble",类型:suit"},{来源:微软",目标:富士康",类型:西装"},{来源:甲骨文",目标:谷歌",类型:西装"},{来源:苹果",目标:HTC",类型:西装"},{来源:微软",目标:英业达",类型:西装"},{来源:三星",目标:柯达",类型:已解决"},{来源:LG",目标:柯达",类型:已解决"},{来源:RIM",目标:柯达",类型:西装"},{来源:索尼",目标:LG",类型:西装"},{来源:柯达",目标:LG",类型:已解决"},{来源:苹果",目标:诺基亚",类型:已解决"},{来源:高通",目标:诺基亚",类型:已解决"},{来源:苹果",目标:摩托罗拉",类型:西装"},{来源:微软",目标:摩托罗拉",类型:西装"},{来源:摩托罗拉",目标:微软",类型:西装"},{来源:华为",目标:中兴",类型:西装"},{来源:爱立信",目标:中兴",类型:西装"},{来源:柯达",目标:三星",类型:已解决"},{来源:苹果",目标:三星",类型:西装"},{来源:柯达",目标:RIM",类型:西装"},{来源:诺基亚",目标:高通",类型:西装"}];var path = svg.append("g").selectAll("path"),circle = svg.append("g").selectAll("circle"),text = svg.append("g").selectAll("text"),标记 = svg.append("defs").selectAll("marker");var 节点 = {};更新(数据集);函数 newData(){var newDataset = [{来源:微软",目标:亚马逊",类型:许可"},{来源:微软",目标:HTC",类型:许可"},{来源:三星",目标:苹果",类型:西装"},];更新(新数据集);}功能更新(链接){d3.values(nodes).forEach(function(aNode){aNode.linkCount = 0;});//重置所有现有节点的链接计数//从节点列表中创建一个数组,然后调用一个函数//在每个节点上将 linkCount 属性设置为零.//从链接计算不同的节点.链接.forEach(功能(链接){link.source = 节点[link.source] ||(nodes[link.source] = {name: link.source, linkCount:0});//用零个链接初始化新节点link.source.linkCount++;//在源节点上记录这个链接,是否刚初始化//或已经在列表中,通过增加 linkCount 属性//(记住,link.source 只是对节点对象的引用//节点数组,当你改变它的属性时,你改变了节点本身.)link.target = 节点[link.target] ||(nodes[link.target] = {name: link.target, linkCount:0});//用零个链接初始化新节点link.target.linkCount++;});d3.keys(nodes).forEach(//创建一个包含节点列表中所有当前键(名称)的数组,//然后对于每一个:功能(节点键){if (!nodes[nodeKey].linkCount){//找到匹配该键的节点,并检查它的链接计数值//如果值为零(在 Javascript 中为 false),则 !(NOT) 运算符//将反转它以使 if 语句返回 true,//然后将执行以下操作:删除(节点[节点密钥]);//这将从节点数组中删除对象及其键}});力量.nodes(d3.values(nodes)).links(链接).开始();//-------------------------------//计算数据连接.这将返回更新选择.标记 = 标记.数据([西装",许可",已解决"]);//删除任何传出/旧标记.标记.exit().remove();//计算用于输入和更新标记的新属性.标记.输入().附加(标记").attr("id", function(d) { return d; }).attr("viewBox", "0 -5 10 10").attr("refX", 15).attr("refY", -1.5).attr("markerWidth", 6).attr("markerHeight", 6).attr(东方",自动").append("line")//使用 ".append("path") 作为'箭头'.attr("d", "M0,-5L10,0L0,5");//-------------------------------//计算数据连接.这将返回更新选择.path = path.data(force.links());//删除所有传出/旧路径.path.exit().remove();//计算用于输入和更新路径的新属性.path.enter().append("path").attr("class", function(d) { return "link" + d.type; }).attr("marker-end", function(d) { return "url(#" + d.type + ")"; });//-------------------------------//计算数据连接.这将返回更新选择.circle = circle.data(force.nodes());//添加任何传入的圆圈.circle.enter().append("circle");//删除所有传出/旧圈子.circle.exit().remove();//计算用于输入和更新圆的新属性.圆圈.attr("r", 6).call(force.drag);//-------------------------------//计算数据连接.这将返回更新选择.text = text.data(force.nodes());//添加任何传入的文本.text.enter().append("text");//删除任何传出/旧文本.text.exit().remove();//计算用于输入和更新文本的新属性.文本.attr("x", 8).attr("y", ".31em").text(function(d) { return d.name; });}//使用椭圆弧路径段对方向性进行双重编码.函数滴答(){path.attr("d", linkArc);circle.attr("变换",变换);text.attr("转换", 转换);}函数linkArc(d){var dx = d.target.x - d.source.x,dy = d.target.y - d.source.y,dr = Math.sqrt(dx * dx + dy * dy);返回 "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," +d.target.y;}函数变换(d){返回 "translate(" + d.x + "," + d.y + ")";}

解决方案

一般的节点-链接图结构可以有没有链接的节点,就像它可以有一个、两个或数百个链接的节点一样.您的更新方法会替换链接的数据,但不会查看节点数据以删除那些不再附加任何链接的数据.

他们按照您目前的方式进行设置,但是,有一个相当简单的修复方法.按原样,您从数据集初始化链接,并将节点初始化为空.然后在更新方法的这一部分:

links.forEach(function(link){link.source = 节点[link.source]||(节点[link.source] = {name: link.source});link.target = 节点[link.target]||(节点[link.target] = {name: link.target});});

在首先检查它是否已经在列表中之后,您将所有提到的节点作为链接的源或目标添加到节点列表中.

(如果它不在列表中,nodes[link.source] 将返回 null,因此 || OR 运算符将启动,后半部分对语句的进行求值,创建对象,将其添加到节点列表中,然后将其连接到链接对象.)

现在,第一次运行更新方法时,这会用数据填充节点列表.然而,第二次,节点列表已经满了,你没有做任何事情来带走任何节点.

简单的修复是在更新方法开始时将节点列表重置为空对象 (nodes={};).然后,只有在当前链接集中的节点才会被添加回来,所以当你重新计算圆和文本上的数据连接时,所有未使用的节点将被放入 .exit() 选择并删除.

但是,我应该提一下,如果您更新很多,并且每次只更改几个对象,还有其他方法可以做到这一点,需要更多代码但更新速度会更快.此版本每次都重新创建所有节点和链接数据对象.如果您有很多(数百个)复杂的数据节点并且每次更新只更改几个,那么向您的节点对象添加一个额外的属性可能是值得的,以跟踪附加的链接数量,并且只重置在您的更新方法开始时.然后,您可以使用过滤器来确定要包含在数据连接中的节点对象.

编辑添加:

这是我用于更保守的更新功能的方法(相对于数据的完全重置).这不是唯一的选择,但它没有太多开销:

第一步(在更新方法中),将所有节点标记为零链接:

d3.values(nodes).forEach(function(aNode){ aNode.linkCount = 0;});//重置所有现有节点的链接计数//从节点列表中创建一个数组,然后调用一个函数//在每个节点上将linkCount属性设置为零.

第二步,改变links.forEach()方法,记录每个节点的链接数:

links.forEach(function(link){link.source = 节点[link.source]||(nodes[link.source] = {name: link.source, linkCount:0});//初始化具有零链接的新节点link.source.linkCount++;//在源节点上记录这个链接,是否刚初始化//或已经在列表中,通过增加 linkCount 属性//(记住,link.source 只是对节点对象的引用//节点数组,当你改变它的属性时,你改变了节点本身.)link.target =/* 然后对目标节点做同样的事情 */});

第三步,选项一,使用过滤 只包括至少有一个链接的节点:

力.nodes( d3.values(nodes).filter(function(d){ return d.linkCount;}) )//说明:d3.values() 将节点的对象列表转换为数组.//.filter() 遍历该数组并创建一个由以下内容组成的新数组//传递给回调函数时返回TRUE的节点.//该函数只返回那个节点的linkCount,Javascript//如果linkCount 为零,则解释为false,否则解释为true..links(链接).开始();

注意,这不会nodes列表中删除未使用的节点,它只会过滤它们不被传递到布局.如果您不希望再次使用这些节点,则需要将它们从节点列表中实际删除.

第三步,选项二,扫描节点列表并删除所有链接为零的节点:

d3.keys(nodes).forEach(//创建一个包含节点列表中所有当前键(名称)的数组,//然后对于每一个:功能(节点键){如果 (!nodes[nodeKey].linkCount) {//找到匹配该键的节点,并检查它的链接计数值//如果值为零(在 Javascript 中为 false),则 !(NOT) 运算符//将反转它以使 if 语句返回 true,//然后将执行以下操作:删除(节点[节点密钥]);//这将从节点数组中删除对象及其键}}//函数结束);//forEach方法结束/*然后像以前一样将节点列表添加到强制布局对象中,不需要过滤器,因为列表只包含你想要的节点*/

I'm using a force layout graph to show a network but I have problems when updating my data.

I already check How to update elements of D3 force layout when the underlying data changes, and of course the "Modifying a Force Layout" as well as the "General Update Pattern" by "mbostock" from D3.js (unfortunately, I can only post a maximum of two links...).

My code based on the "Mobile Patent Suits" example with some modifications and differences. You can check my full code here:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.link {
    fill: none;
    stroke: #666;
    stroke-width: 1.5px;
}

#licensing {
    fill: green;
}

.link.licensing {
    stroke: green;
}

.link.resolved {
    stroke-dasharray: 0,2 1;
}

circle {
    fill: #ccc;
    stroke: #333;
    stroke-width: 1.5px;
}

text {
    font: 10px sans-serif;
    pointer-events: none;
    text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}

</style>
<body>
    <!-- add an update button -->
    <div id="update">
        <input name="updateButton" type="button" value="Update" onclick="newData()"/>
    </div>
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <script>

    var width = 960,
        height = 500;

    var force = d3.layout.force()
        .size([width, height])
        .linkDistance(60)
        .charge(-300)
        .on("tick", tick);

    var svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height)
        .style("border", "1px solid black");

    var dataset = [
                 {source: "Microsoft", target: "Amazon", type: "licensing"},
                 {source: "Microsoft", target: "HTC", type: "licensing"},
                 {source: "Samsung", target: "Apple", type: "suit"},
                 {source: "Motorola", target: "Apple", type: "suit"},
                 {source: "Nokia", target: "Apple", type: "resolved"},
                 {source: "HTC", target: "Apple", type: "suit"},
                 {source: "Kodak", target: "Apple", type: "suit"},
                 {source: "Microsoft", target: "Barnes & Noble", type: "suit"},
                 {source: "Microsoft", target: "Foxconn", type: "suit"},
                 {source: "Oracle", target: "Google", type: "suit"},
                 {source: "Apple", target: "HTC", type: "suit"},
                 {source: "Microsoft", target: "Inventec", type: "suit"},
                 {source: "Samsung", target: "Kodak", type: "resolved"},
                 {source: "LG", target: "Kodak", type: "resolved"},
                 {source: "RIM", target: "Kodak", type: "suit"},
                 {source: "Sony", target: "LG", type: "suit"},
                 {source: "Kodak", target: "LG", type: "resolved"},
                 {source: "Apple", target: "Nokia", type: "resolved"},
                 {source: "Qualcomm", target: "Nokia", type: "resolved"},
                 {source: "Apple", target: "Motorola", type: "suit"},
                 {source: "Microsoft", target: "Motorola", type: "suit"},
                 {source: "Motorola", target: "Microsoft", type: "suit"},
                 {source: "Huawei", target: "ZTE", type: "suit"},
                 {source: "Ericsson", target: "ZTE", type: "suit"},
                 {source: "Kodak", target: "Samsung", type: "resolved"},
                 {source: "Apple", target: "Samsung", type: "suit"},
                 {source: "Kodak", target: "RIM", type: "suit"},
                 {source: "Nokia", target: "Qualcomm", type: "suit"}
                 ];

   var path = svg.append("g").selectAll("path"),
       circle = svg.append("g").selectAll("circle"),
       text = svg.append("g").selectAll("text"),
       marker = svg.append("defs").selectAll("marker");

   var nodes = {};

   update(dataset);

   function newData()
   {
        var newDataset = [
                    {source: "Microsoft", target: "Amazon", type: "licensing"},
                    {source: "Microsoft", target: "HTC", type: "licensing"},
                    {source: "Samsung", target: "Apple", type: "suit"},
                    ];

        update(newDataset);
   }

   function update(links)
   {
        // Compute the distinct nodes from the links.
        links.forEach(function(link)
        {
            link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
            link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
        });

        force
            .nodes(d3.values(nodes))
            .links(links)
            .start();

        // -------------------------------

        // Compute the data join. This returns the update selection.
        marker = marker.data(["suit", "licensing", "resolved"]);

        // Remove any outgoing/old markers.
        marker.exit().remove();

        // Compute new attributes for entering and updating markers.
        marker.enter().append("marker")
           .attr("id", function(d) { return d; })
           .attr("viewBox", "0 -5 10 10")
           .attr("refX", 15)
           .attr("refY", -1.5)
           .attr("markerWidth", 6)
           .attr("markerHeight", 6)
           .attr("orient", "auto")
           .append("line") // use ".append("path") for 'arrows'
           .attr("d", "M0,-5L10,0L0,5");

        // -------------------------------

        // Compute the data join. This returns the update selection.
        path = path.data(force.links());

        // Remove any outgoing/old paths.
        path.exit().remove();

        // Compute new attributes for entering and updating paths.
        path.enter().append("path")
           .attr("class", function(d) { return "link " + d.type; })
           .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });

        // -------------------------------

        // Compute the data join. This returns the update selection.
        circle = circle.data(force.nodes());

        // Add any incoming circles.
        circle.enter().append("circle");

        // Remove any outgoing/old circles.
        circle.exit().remove();

        // Compute new attributes for entering and updating circles.
        circle
           .attr("r", 6)
           .call(force.drag);

        // -------------------------------

        // Compute the data join. This returns the update selection.
        text = text.data(force.nodes());

        // Add any incoming texts.
        text.enter().append("text");

        // Remove any outgoing/old texts.
        text.exit().remove();

        // Compute new attributes for entering and updating texts.
        text
           .attr("x", 8)
           .attr("y", ".31em")
           .text(function(d) { return d.name; });
    }

    // Use elliptical arc path segments to doubly-encode directionality.
    function tick()
    {
       path.attr("d", linkArc);
       circle.attr("transform", transform);
       text.attr("transform", transform);
    }

    function linkArc(d)
    {
       var dx = d.target.x - d.source.x,
           dy = d.target.y - d.source.y,
           dr = Math.sqrt(dx * dx + dy * dy);
       return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
    }

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

    </script>

A JSFiddle of my code can be found here: http://jsfiddle.net/5m8a9/

After pressing the "Update" button I want to update my graph dynamically. So far so good, the problem is, that ONLY the paths will be updated but not the circles or the texts (some circles and the corresponding texts still remain and will not be removed) as you can see at my JSFiddle link. I tried to figure out the problem for the last couple of days without success.

What am I missing and how can I make my code work as aspected?

If anyone can help I would be eternally grateful.

Edited to add final solution as @AmeliaBR provided:

Here is the hole code to my final solution:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.link {
    fill: none;
    stroke: #666;
    stroke-width: 1.5px;
}

#licensing {
    fill: green;
}

.link.licensing {
    stroke: green;
}

.link.resolved {
    stroke-dasharray: 0,2 1;
}

circle {
    fill: #ccc;
    stroke: #333;
    stroke-width: 1.5px;
}

text {
    font: 10px sans-serif;
    pointer-events: none;
    text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}

    </style>
<body>
    <!-- add an update button -->
    <div id="update">
        <input name="updateButton" type="button" value="Update" onclick="newData()"/>
    </div>
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <script>

    var width = 960,
        height = 500;

    var force = d3.layout.force()
        .size([width, height])
        .linkDistance(60)
        .charge(-300)
        .on("tick", tick);

    var svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height)
        .style("border", "1px solid black");

    var dataset = [
                   {source: "Microsoft", target: "Amazon", type: "licensing"},
                   {source: "Microsoft", target: "HTC", type: "licensing"},
                   {source: "Samsung", target: "Apple", type: "suit"},
                   {source: "Motorola", target: "Apple", type: "suit"},
                   {source: "Nokia", target: "Apple", type: "resolved"},
                   {source: "HTC", target: "Apple", type: "suit"},
                   {source: "Kodak", target: "Apple", type: "suit"},
                   {source: "Microsoft", target: "Barnes & Noble", type: "suit"},
                   {source: "Microsoft", target: "Foxconn", type: "suit"},
                   {source: "Oracle", target: "Google", type: "suit"},
                   {source: "Apple", target: "HTC", type: "suit"},
                   {source: "Microsoft", target: "Inventec", type: "suit"},
                   {source: "Samsung", target: "Kodak", type: "resolved"},
                   {source: "LG", target: "Kodak", type: "resolved"},
                   {source: "RIM", target: "Kodak", type: "suit"},
                   {source: "Sony", target: "LG", type: "suit"},
                   {source: "Kodak", target: "LG", type: "resolved"},
                   {source: "Apple", target: "Nokia", type: "resolved"},
                   {source: "Qualcomm", target: "Nokia", type: "resolved"},
                   {source: "Apple", target: "Motorola", type: "suit"},
                   {source: "Microsoft", target: "Motorola", type: "suit"},
                   {source: "Motorola", target: "Microsoft", type: "suit"},
                   {source: "Huawei", target: "ZTE", type: "suit"},
                   {source: "Ericsson", target: "ZTE", type: "suit"},
                   {source: "Kodak", target: "Samsung", type: "resolved"},
                   {source: "Apple", target: "Samsung", type: "suit"},
                   {source: "Kodak", target: "RIM", type: "suit"},
                   {source: "Nokia", target: "Qualcomm", type: "suit"}
                   ];

    var path = svg.append("g").selectAll("path"),
    circle = svg.append("g").selectAll("circle"),
    text = svg.append("g").selectAll("text"),
    marker = svg.append("defs").selectAll("marker");

    var nodes = {};

    update(dataset);

    function newData()
    {
        var newDataset = [
                         {source: "Microsoft", target: "Amazon", type: "licensing"},
                         {source: "Microsoft", target: "HTC", type: "licensing"},
                         {source: "Samsung", target: "Apple", type: "suit"},
                         ];

        update(newDataset);
    }

    function update(links)
    {
        d3.values(nodes).forEach(function(aNode){ aNode.linkCount = 0;});
            // Reset the link count for all existing nodes by
            // creating an array out of the nodes list, and then calling a function
            // on each node to set the linkCount property to zero.

        // Compute the distinct nodes from the links.
        links.forEach(function(link)
        {
            link.source = nodes[link.source] || (nodes[link.source] = {name: link.source, linkCount:0});    // initialize new nodes with zero links
            link.source.linkCount++;
                // record this link on the source node, whether it was just initialized
                // or already in the list, by incrementing the linkCount property
                // (remember, link.source is just a reference to the node object in the
                // nodes array, when you change its properties you change the node itself.)

            link.target = nodes[link.target] || (nodes[link.target] = {name: link.target, linkCount:0});    // initialize new nodes with zero links

            link.target.linkCount++;
        });

        d3.keys(nodes).forEach(
            // create an array of all the current keys(names) in the node list,
            // and then for each one:

            function (nodeKey)
            {
                if (!nodes[nodeKey].linkCount)
                {
                    // find the node that matches that key, and check it's linkCount value
                    // if the value is zero (false in Javascript), then the ! (NOT) operator
                    // will reverse that to make the if-statement return true,
                    // and the following will execute:

                    delete(nodes[nodeKey]);
                        //this deletes the object AND its key from the nodes array
                 }
             }
         );

        force
            .nodes(d3.values(nodes))
            .links(links)
            .start();

        // -------------------------------

        // Compute the data join. This returns the update selection.
        marker = marker.data(["suit", "licensing", "resolved"]);

        // Remove any outgoing/old markers.
        marker.exit().remove();

        // Compute new attributes for entering and updating markers.
        marker.enter().append("marker")
            .attr("id", function(d) { return d; })
            .attr("viewBox", "0 -5 10 10")
            .attr("refX", 15)
            .attr("refY", -1.5)
            .attr("markerWidth", 6)
            .attr("markerHeight", 6)
            .attr("orient", "auto")
            .append("line") // use ".append("path") for 'arrows'
            .attr("d", "M0,-5L10,0L0,5");

        // -------------------------------

        // Compute the data join. This returns the update selection.
        path = path.data(force.links());

        // Remove any outgoing/old paths.
        path.exit().remove();

        // Compute new attributes for entering and updating paths.
        path.enter().append("path")
            .attr("class", function(d) { return "link " + d.type; })
            .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });

        // -------------------------------

        // Compute the data join. This returns the update selection.
        circle = circle.data(force.nodes());

        // Add any incoming circles.
        circle.enter().append("circle");

        // Remove any outgoing/old circles.
        circle.exit().remove();

        // Compute new attributes for entering and updating circles.
        circle
            .attr("r", 6)
            .call(force.drag);

        // -------------------------------

        // Compute the data join. This returns the update selection.
        text = text.data(force.nodes());

        // Add any incoming texts.
        text.enter().append("text");

        // Remove any outgoing/old texts.
        text.exit().remove();

        // Compute new attributes for entering and updating texts.
        text
            .attr("x", 8)
            .attr("y", ".31em")
            .text(function(d) { return d.name; });
    }

    // Use elliptical arc path segments to doubly-encode directionality.
    function tick()
    {
        path.attr("d", linkArc);
        circle.attr("transform", transform);
        text.attr("transform", transform);
    }

    function linkArc(d)
    {
        var dx = d.target.x - d.source.x,
            dy = d.target.y - d.source.y,
            dr = Math.sqrt(dx * dx + dy * dy);
        return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
    }

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

    </script>

解决方案

The general node-link graph structure can have nodes without links, just like it can have nodes with one, two or hundreds of link. Your update method replaces the links' data, but does not look through nodes data to remove those that no longer have any links attached.

They way you've currently got it set up, however, there is a fairly straightforward fix. As is, you initialize links from the dataset, and initialize nodes to be empty. Then in this section of your update method:

links.forEach(function(link)
        {
            link.source = nodes[link.source] 
                          || (nodes[link.source] = {name: link.source});

            link.target = nodes[link.target] 
                          || (nodes[link.target] = {name: link.target});
        });

You add all the nodes mentioned as either a source or target of a link to the nodes list, after first checking whether or not it's already in the list.

(If it isn't in the list, nodes[link.source] will return null, so the || OR operator will kick in and the second half of the statement is evaluated, creating the object, adding it to the nodes list, and then connecting it to the link object.)

Now, the first time your run your update method, this fills up the nodes list with data. The second time, however, the nodes list is already full and you don't do anything to take any nodes away.

The simple fix is to reset your nodes list to an empty object (nodes={};) at the start of your update method. Then, only the nodes that are in the current set of links will be added back in, so when you re-compute the data join on the circles and text all the unused nodes will be put into the .exit() selection and removed.

But, I should mention that if you're updating a lot, and only changing a few objects each time, there are other ways to do this that require more code but will be faster in update. This version recreates all the node and link data objects each time. If you've got a lot (many hundreds) of complex data nodes and are only changing a couple each update, it might be worth it to add an extra property to your node objects keeping track of how many links are attached, and only reset that at the start of your update method. Then you could use a filter to figure out which of the node objects to include in your data join.

Edited to Add:

Here is the approach I'd use for a more conservative update function (versus a complete reset of the data). It's not the only option, but it doesn't have much overhead:

First step (in update method), mark all nodes to have zero links:

d3.values(nodes).forEach(function(aNode){ aNode.linkCount = 0;}); 
   //Reset the link count for all existing nodes by
   //creating an array out of the nodes list, and then calling a function
   //on each node to set the linkCount property to zero.

Second step, change the links.forEach() method to record the number of links in each node:

links.forEach(function(link)
    {
     link.source = nodes[link.source] 
                      || (nodes[link.source] = {name: link.source, linkCount:0});
                                    //initialize new nodes with zero links

     link.source.linkCount++;
        // record this link on the source node, whether it was just initialized
        // or already in the list, by incrementing the linkCount property
        // (remember, link.source is just a reference to the node object in the 
        // nodes array, when you change its properties you change the node itself.)

     link.target = /* and then do the same for the target node */
    });

Third step, option one, use an filter to only include nodes that have at least one link:

force
    .nodes( d3.values(nodes).filter(function(d){ return d.linkCount;}) )
      //Explanation: d3.values() turns the object-list of nodes into an array.
      //.filter() goes through that array and creates a new array consisting of 
      //the nodes that return TRUE when passed to the callback function.  
      //The function just returns the linkCount of that node, which Javascript 
      //interprets as false if linkCount is zero, or true otherwise.
    .links(links)
    .start();

Note that this does not delete the unused nodes from the nodes list, it only filters them from getting passed to the layout. If you don't expect to be using those nodes again, you will need to actually remove them from the nodes list.

Third step, option two, scan through the nodes list and delete any nodes that have zero links:

d3.keys(nodes).forEach(
   //create an array of all the current keys(names) in the node list, 
   //and then for each one:

   function (nodeKey) {
       if (!nodes[nodeKey].linkCount) {
         // find the node that matches that key, and check it's linkCount value
         // if the value is zero (false in Javascript), then the ! (NOT) operator
         // will reverse that to make the if-statement return true, 
         // and the following will execute:

           delete(nodes[nodeKey]); 
             //this deletes the object AND its key from the nodes array
       }

   }//end of function

); //end of forEach method

  /*then add the nodes list to the force layout object as before, 
     no filter needed since the list only includes the nodes you want*/

这篇关于d3.js:如何在强制布局中更新链接数据时删除节点的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
前端开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆