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

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

问题描述



我已经选中了 /stackoverflow.com/questions/12992351/how-to-update-elements-when-the-underlying-data-changes-with-d3js\">如何在基础数据随d3js 更新时更新元素,以及当然,D3.js中的修改强制布局以及 mbostock 的一般更新模式(不幸的是,我只能发布最多两个链接...) / p>

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

 <!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>
<! - 添加更新按钮 - >
< div id =update>
< input name =updateButtontype =buttonvalue =Updateonclick =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)

.on(tick,tick);

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

var dataset = [
{source:Microsoft,target:Amazon,type:licensing},
{source:Microsoft HTC,键入:licensing},
{source:Samsung,target:Apple,type:suit},
{source:Motorola ,type:suit},
{source:Nokia,target:Apple,type:resolved},
{source:HTC :suit},
{source:Kodak,target:Apple,type:suit},
{source:Microsoft,target:Barnes& Noble类型:suit},
{source:Microsoft,target:Foxconn,type:suit},
{source:Oracle suit},
{source:Apple,target:HTC,type:suit},
{source:Microsoft,target:Inventec,type: },
{source:Samsung,target:Kodak,type:resolved},
{source:LG,target:Kodak ,
{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},
{来源:Microsoft,目标:摩托罗拉,类型:suit},
{source:摩托罗拉,目标:Microsoft,类型:suit},
{ Huawei,target:ZTE,type:suit},
{source:Ericsson,target:ZTE,type:suit},
{source:Kodak ,目标:三星,类型:已解决},
{源:苹果,目标:三星,输入: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),
mark = svg.append(defs)。

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

函数更新(链接)
{
//从链接中计算不同的节点。
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();

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

/ /计算数据连接。这将返回更新选择。
marker = marker.data([suit,licensing,resolved]);

//删除任何传出/旧标记。
marker.exit()。remove();

//计算用于输入和更新标记的新属性。
mark.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)//
.attr(d,M0,-5L10,0L0,5);

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

//计算数据连接返回更新选择
path = path.data(force.links() );

//删除任何传出/旧路径
path.exit()。remove();

//计算新的属性,路径
path.enter()。append(path)
.attr(class,function(d){returnlink+ d.type;})
。 attr(marker-end,function(d){returnurl(#+ d.type +);});

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

//计算数据连接。这将返回更新选择。
circle = circle.data(force.nodes());

//添加任何传入的圈子。
circle.enter()。append(circle);

//删除所有外出/旧的圈子。
circle.exit()。remove();

//计算用于输入和更新圈子的新属性。
circle
.attr(r,6)
.call(force.drag);

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

/ /计算数据连接。这将返回更新选择。
text = text.data(force.nodes());

//添加任何传入的文本。
text.enter()。append(text);

//删除任何传出/旧的文本。
text.exit()。remove();

//计算用于输入和更新文本的新属性。
text
.attr(x,8)
.attr(y,.31em)
.text(function(d){return d.name ;});
}

//使用椭圆弧路径段来对方向性进行双重编码。
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);
returnM+ d.source.x +,+ d.source.y +A+ dr +,+ dr +0 0,1+ d.target.x + ,+ d.target.y;
}

函数transform(d)
{
returntranslate(+ d.x +,+ d.y +);
}

< / script>

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



按下更新按钮后,我想动态更新我的图表。到目前为止这么好,问题是,只有路径会更新,但不是圆或文字(一些圈子和相应的文字仍然保留,不会被删除),你可以看到我的JSFiddle链接。
我试图找出最近几天没有成功的问题。



我缺少什么,如何使我的代码工作作为方面?



如果任何人都能帮助我永远感激。



解决方案作为@AmeliaBR提供:



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

 <!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>
<! - 添加更新按钮 - >
< div id =update>
< input name =updateButtontype =buttonvalue =Updateonclick =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)

.on(tick,tick);

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

var dataset = [
{source:Microsoft,target:Amazon,type:licensing},
{source:Microsoft HTC,键入:licensing},
{source:Samsung,target:Apple,type:suit},
{source:Motorola ,type:suit},
{source:Nokia,target:Apple,type:resolved},
{source:HTC :suit},
{source:Kodak,target:Apple,type:suit},
{source:Microsoft,target:Barnes& Noble类型:suit},
{source:Microsoft,target:Foxconn,type:suit},
{source:Oracle suit},
{source:Apple,target:HTC,type:suit},
{source:Microsoft,target:Inventec,type: },
{source:Samsung,target:Kodak,type:resolved},
{source:LG,target:Kodak ,
{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},
{来源:Microsoft,目标:摩托罗拉,类型:suit},
{source:摩托罗拉,目标:Microsoft,类型:suit},
{ Huawei,target:ZTE,type:suit},
{source:Ericsson,target:ZTE,type:suit},
{source:Kodak ,目标:三星,类型:已解决},
{源:苹果,目标:三星,输入: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)。

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

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

//从链接中计算不同的节点。
links.forEach(function(link)
{
link.source = nodes [link.source] ||(nodes [link.source] = {name:link.source,linkCount: 0}); //用零链接初始化新节点
link.source.linkCount ++;
//在源节点上记录这个链接,无论它是刚初始化的
//还是已经在列表,通过增加linkCount属性
//(记住,link.source只是对
// nodes数组中的节点对象的引用,当你改变它的属性,你改变节点本身。 )

link.target = nodes [link.target] ||(nodes [link.target] = {name:link.target,linkCount:0}); //用零链接初始化新节点

link.target.linkCount ++;
});

d3.keys(nodes).forEach(
//创建一个节点列表中所有当前键(名称)的数组,
//然后为每一个:

function(nodeKey)
{
if(!nodes [nodeKey] .linkCount)
{
//找到与该键匹配的节点,并检查它的linkCount值
//如果值为零(在Javascript中为false),则!(NOT)运算符
//将反向使if语句返回true,
//然后将执行:

delete(nodes [nodeKey]);
//从节点中删除对象及其键
}
}
);

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

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

/ /计算数据连接。这将返回更新选择。
marker = marker.data([suit,licensing,resolved]);

//删除任何传出/旧标记。
marker.exit()。remove();

//计算用于输入和更新标记的新属性。
mark.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)//
.attr(d,M0,-5L10,0L0,5);

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

//计算数据连接返回更新选择
path = path.data(force.links() );

//删除任何传出/旧路径
path.exit()。remove();

//计算新的属性,路径
path.enter()。append(path)
.attr(class,function(d){returnlink+ d.type;})
。 attr(marker-end,function(d){returnurl(#+ d.type +); });

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

/ /计算数据连接。这将返回更新选择。
circle = circle.data(force.nodes());

//添加任何传入的圈子。
circle.enter()。append(circle);

//删除所有外出/旧的圈子。
circle.exit()。remove();

//计算用于输入和更新圈子的新属性。
circle
.attr(r,6)
.call(force.drag);

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

/ /计算数据连接。这将返回更新选择。
text = text.data(force.nodes());

//添加任何传入的文本。
text.enter()。append(text);

//删除任何传出/旧的文本。
text.exit()。remove();

//计算用于输入和更新文本的新属性。
text
.attr(x,8)
.attr(y,.31em)
.text(function(d){return d.name ;});
}

//使用椭圆弧路径段来对方向性进行双重编码。
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);
returnM+ d.source.x +,+ d.source.y +A+ dr +,+ dr +0 0,1+ d.target.x + ,+ d.target.y;
}

函数transform(d)
{
returntranslate(+ d.x +,+ d.y +);
}

< / script>


解决方案

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



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

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

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



(如果它不在列表中,则 nodes [link.source] code>将返回null,因此 || OR运算符将引入,并且语句的后半部分被求值,创建对象,将其添加到节点列表,然后将其连接到链接对象。)



现在,第一次运行更新方法时,会用数据填充节点列表。第二次,节点列表已经满了,你不需要做任何事情来取走任何节点。



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



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



已编辑为添加



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



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

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

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

  links.forEach(function(link)
{
link.source = nodes [link.source]
||(nodes [link.source] = {name:link.source,linkCount:0});
//初始化新节点没有链接

link.source.linkCount ++;
//在源节点上记录此链接,无论它是刚刚初始化的
//还是已经在列表中,通过增加linkCount属性
//(记住,link.source只是对
// nodes数组中的节点对象的引用,当你改变它的属性时你改变了节点本身。)

link.target = / *然后对目标节点执行相同操作* /
});

第三步,选项一,使用过滤器仅包含至少具有一个链接的节点:

  force 
.nodes(d3.values(nodes).filter(function(d){return d.linkCount;})
//说明:d3.values()将节点的对象列表转换为数组。
//。filter()遍历该数组,并创建一个由
组成的新数组//当传递给回调函数时返回TRUE的节点。
//函数只返回该节点的linkCount,其中JavaScript
//如果linkCount为0则解释为false,否则为true。
.links(links)
.start();

请注意,不会 nodes 列表,它只过滤他们从传递到布局。



第三步,选项二

如果你不想再使用这些节点,你需要从节点列表中删除它们。 strong>,扫描节点列表并删除任何具有零链接的节点:

  d3.keys 
//在节点列表中创建一个所有当前键(名称)的数组,
//然后为每一个:

function(nodeKey){
if(!nodes [nodeKey] .linkCount){
//找到与该键匹配的节点,并检查其linkCount值
//如果值为零(Javascript中为false) !(NOT)operator
//将会使if语句返回true,
//并执行以下命令:

delete(nodes [nodeKey]) ;
//从节点中删除对象及其键
}

} //函数结束

); // 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 when the underlying data changes with d3js, 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天全站免登陆