【问题标题】:implement General Update Pattern for d3.js force layout为 d3.js 强制布局实现通用更新模式
【发布时间】:2015-04-24 21:34:24
【问题描述】:

this example(可能不是最佳选择)开始,我开始尝试开发一个适合我目的的应用程序,并在此过程中学习 d3.js。经过很多天真的修补后,我设法为我的测试数据获得了toy force layout,它的外观和行为令我满意。现在,我开始尝试在我的特定示例的上下文中理解 MB 的 General Update Pattern,以便用户可以交互地修改图形。我显然还没有掌握其中的原理。

从小处着手,我想创建一个函数,只需在标记为“Walteri”和“Roberti de Fonte”的节点之间添加一个附加链接到图形(有一个按钮可以执行addedge(),或者您可以执行它来自 js 控制台)。以一种破碎的方式,这产生了预期的结果。但是,现有图表仍然存在,同时生成了包含附加链接的重复图表。很明显,我仍然不了解通用更新模式。

如果有人看过并能提供任何见解,我将不胜感激。

【问题讨论】:

  • 你见过this tutorial吗?
  • 上面提到的force layout 已经更新为@coolblue 在下面接受的答案中提供的更改。该在线示例不会持续很长时间,但仔细阅读下面 coolblue 的注释代码(非常感谢!)将有助于阐明 MB 的 d3.js 通用更新模式及其在力布局中的应用。
  • 凭借@coolblue 在接受的答案中提供的见解,我使用this example from MB 作为模型从头开始。上面提到的force layout 示例已使用精简数据进行了更新,现在令人满意地演示了通过d3.json() 导入新的图形数据并以编程方式在节点之间添加单独的边。再次感谢@coolblue。我会暂时搁置这个。

标签: javascript d3.js force-layout


【解决方案1】:

管理更新
您遇到的主要问题是每次更新都会重新附加组元素。
你可以让 d3 像这样为你管理它......

//Nodes bag
//UPDATE
var circles = svg.selectAll(".circles")
            .data(["circles_g"]);
//ENTER
circles.enter()
    .append("svg:g")
    .attr("class", "circles");

你只需要组成一个单元素数组来驱动它。这样做的好处是它将被放置在 __data__ 成员上,该成员由 d3 添加到 g 元素中,因此它也便于调试。

一般模式
一般来说,这是最具防御性的模式……

//UPDATE
var update = baseSelection.selectAll(elementSelector)
            .data(values, key),
    //ENTER
        enter = update.enter().append(appendElement)
            .call(initStuff),
    //enter() has side effect of adding enter nodes to the update selection
    //so anything you do to update now will include the enter nodes

    //UPDATE+ENTER
        updateEnter = update
            .call(stuffToDoEveryTimeTheDataChanges);
    //EXIT
    exit = update.exit().remove()

第一次通过update 将是一个与数据具有相同结构的空数组。
在这种情况下,.selectAll() 返回一个零长度选择并且没有任何用处。

在后续更新中,.selectAll 不会为空,并将与values 进行比较,使用keys 来确定哪些节点是更新、进入和退出节点。这就是为什么您需要在数据连接之前进行选择。

要理解的重要一点是它必须是.enter().append(...),因此您要在输入选择中附加元素。如果您将它们附加到更新选择(数据连接返回的选择)上,那么您将重新输入相同的元素并看到与您得到的类似的行为。

输入选择是{ __data__: data }形式的简单对象数组
更新和退出选择是对 DOM 元素的引用数组。

d3 中的 data 方法对进入和退出选择保持一个闭包,这些选择由 update 上的 .enter().exit() 方法访问。两者都返回对象,其中包括二维数组(d3 中的所有选择都是组数组,其中组是节点数组。)。 enter 成员也被赋予了对update 的引用,以便它可以合并两者。这样做是因为在大多数情况下,对两组都做了相同的事情。

修改后的代码
有一个奇怪的错误,当添加不相关的边缘但由于节点中的d.xd.y 中的NaN 时,链接有时会消失。
如果你不每次都在showme 中重建强制布局并且如果你这样做......

links.push({ "source": nodes[i], "target": nodes[j], "type": "is_a_tenant_of" });
force.start();
showme();

错误消失了,一切正常。

这是因为布局的内部状态不包括 额外的链接,特别是 strengthsdistances 数组。这 内部force.tick() 方法使用这些来计算新链接 长度,如果链接数多于这些数组的成员,则 他们将返回undefined 和链接,长度计算将 返回NaN 然后乘以节点xy 值来计算新的 d.xd.y
这都是在force.start()中重新计算的

此外,您可以将 force = d3.layout.force()....start(); 移动到单独的 function 中,并且只在开始时调用一次。

d3.json("force-directed-edges.json", function(error, data){
        if (error) return console.warn(error)
                nodes = data.nodes, 
                links = data.links,
                predicates = data.predicates,
                json = JSON.stringify(data, undefined, 2);

                for (n in nodes) { // don't want to require incoming data to have links array for each node
                        nodes[n].links = []
                }

                links.forEach(function(link, i) {
                        // kept the 'Or' check, in case we're building the nodes only from the links
                        link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
                        link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
                        // To do any dijkstra searching, we'll need adjacency lists: node.links. (easier than I thought)
                        link.source.links.push(link);
                        link.target.links.push(link);
                });

                nodes = d3.values(nodes);
                reStart()
                showme();
});

function randomNode(i) {
    var j;
    do {
        j = Math.round(Math.random() * (nodes.length - 1))
    } while (j === (i ? i : -1))
    return j
}
function addedge() {
    var i = randomNode(), j = randomNode(i);

    links.push({ "source": nodes[i], "target": nodes[j], "type": "is_a_tenant_of" });
    force.start();
    showme();
}

function reStart() {
    force = d3.layout.force()
        .nodes(nodes)
        .links(links)
        .size([w, h])
        .linkDistance(function (link) {
            var wt = link.target.weight;
            return wt > 2 ? wt * 10 : 60;
        })
        .charge(-600)
        .gravity(.01)
        .friction(.75)
        //.theta(0)
        .on("tick", tick)
        .start();
}
function showme() {
    //Marker Types  
    var defs = svg.selectAll("defs")
                .data(["defs"], function (d) { return d }).enter()
                .append("svg:defs")
                    .selectAll("marker")
                        .data(predicates)
                        .enter().append("svg:marker")
                            .attr("id", String)
                            .attr("viewBox", "0 -5 10 10")
                            .attr("refX", 30)
                            .attr("refY", 0)
                            .attr("markerWidth", 4)
                            .attr("markerHeight", 4)
                            .attr("orient", "auto")
                        .append("svg:path")
                            .attr("d", "M0,-5L10,0L0,5"),
    //Link bag
            //UPDATE
            paths = svg.selectAll(".paths")
                .data(["paths_g"]);
            //ENTER
    paths.enter()
        .append("svg:g")
        .attr("class", "paths");

    //Links
    //UPDATE
    path = paths.selectAll("path")
        .data(links);
    //ENTER
    path.enter()
        .append("svg:path");
    //UPDATE+ENTER
    path
        .attr("indx", function (d, i) { return i })
        .attr("id", function (d) { return d.source.index + "_" + d.target.index; })
        .attr("class", function (d) { return "link " + d.type; })
        .attr("marker-end", function (d) { return "url(#" + d.type + ")"; });
    //EXIT          
    path.exit().remove();

    //Link labels bag
    //UPDATE
    var path_labels = svg.selectAll(".labels")
            .data(["labels_g"]);
            //ENTER
    path_labels.enter()
        .append("svg:g")
        .attr("class", "labels");

    //Link labels
            //UPDATE
    var path_label = path_labels.selectAll(".path_label")
                .data(links);
            //ENTER
    path_label.enter()
        .append("svg:text")
            .append("svg:textPath")
                .attr("startOffset", "50%")
                .attr("text-anchor", "middle")
                .style("fill", "#000")
                .style("font-family", "Arial");
            //UPDATE+ENTER
    path_label
        .attr("class", function (d, i) { return "path_label " + i })
//EDIT*******************************************************************
      .selectAll('textPath')
//EDIT*******************************************************************
        .attr("xlink:href", function (d) { return "#" + d.source.index + "_" + d.target.index; })
        .text(function (d) { return d.type; }),
    //EXIT
    path_label.exit().remove();

    //Nodes bag
            //UPDATE
    var circles = svg.selectAll(".circles")
                .data(["circles_g"]);
            //ENTER
    circles.enter()
        .append("svg:g")
        .attr("class", "circles");

    //Nodes
    //UPDATE
    circle = circles.selectAll(".nodes")
                .data(nodes);
    //ENTER
    circle.enter().append("svg:circle")
                .attr("class", function (d) { return "nodes " + d.index })
                .attr("stroke", "#000");
    //UPDATE+ENTER
    circle
        .on("click", clicked)
        .on("dblclick", dblclick)
        .on("contextmenu", cmdclick)
        .attr("fill", function (d, i) {
            console.log(i + " " + d.types[0] + " " + node_colors[d.types[0]])
            return node_colors[d.types[0]];
        })
        .attr("r", function (d) { return d.types.indexOf("Document") == 0 ? 24 : 12; })
        .call(force.drag);
    //EXIT
    circle.exit().remove();

    //Anchors bag
    //UPDATE
    var textBag = svg.selectAll(".anchors")
                .data(["anchors_g"]);
            //ENTER
            textBag.enter()
                .append("svg:g")
                .attr("class", "anchors"),

    //Anchors
            //UPDATE
            textUpdate = textBag.selectAll("g")
                .data(nodes, function (d) { return d.name; }),
    //ENTER
    textEnter = textUpdate.enter()
        .append("svg:g")
        .attr("text-anchor", "middle")
        .attr("class", function (d) { return "anchors " + d.index });

    // A copy of the text with a thick white stroke for legibility.
    textEnter.append("svg:text")
                .attr("x", 8)
                .attr("y", ".31em")
                .attr("class", "shadow")
                .text(function (d) { return d.name; });

    textEnter.append("svg:text")
                .attr("x", 8)
                .attr("y", ".31em")
                .text(function (d) { return d.name; });
    textUpdate.exit().remove();
    text = textUpdate;

    // calling force.drag() here returns the drag _behavior_ on which to set a listener
    // node element event listeners
    force.drag().on("dragstart", function (d) {
        d3.selectAll(".dbox").style("z-index", 0);
        d3.select("#dbox" + d.index).style("z-index", 1);
    })
}

编辑
针对@jjon 下面的评论和我自己的启发,这里是对具有相同命名约定和不同 cmets 的原始代码的最小更改。正确添加链接所需的模组没有改变,也没有讨论...

function showme() {
    svg
    /////////////////////////////////////////////////////////////////////////////////////
    //Problem
    //  another defs element is added to the document every update
    //Solution:
    //  create a data join on defs 
    //  append the marker definitions on the resulting enter selection
    //  this will only be appended once
    /////////////////////////////////////////////////////////////////////////////////////

//ADD//////////////////////////////////////////////////////////////////////////////////
    .selectAll("defs")
    .data(["defs"], function (d) { return d }).enter()
///////////////////////////////////////////////////////////////////////////////////////

    .append("svg:defs")
        .selectAll("marker")
        .data(predicates)
        .enter().append("svg:marker")
            .attr("id", String)
            .attr("viewBox", "0 -5 10 10")
            .attr("refX", 30)
            .attr("refY", 0)
            .attr("markerWidth", 4)
            .attr("markerHeight", 4)
            .attr("orient", "auto")
        .append("svg:path")
            .attr("d", "M0,-5L10,0L0,5");

    /////////////////////////////////////////////////////////////////////////////////////
    //Problem
    //  another g element is added to the document every update
    //Solution:
    //  create a data join on the g and class it .paths 
    //  append the path g on the resulting enter selection
    //  this will only be appeneded once
    /////////////////////////////////////////////////////////////////////////////////////

//ADD//////////////////////////////////////////////////////////////////////////////////
    //Link bag
    //UPDATE
    paths = svg
        .selectAll(".paths")
        .data(["paths_g"]);
    //ENTER
    paths.enter()
///////////////////////////////////////////////////////////////////////////////////////

        .append("svg:g")

//ADD//////////////////////////////////////////////////////////////////////////////////
        .attr("class", "paths");
///////////////////////////////////////////////////////////////////////////////////////

    //Links
    //UPDATE
    path = paths    //Replace svg with paths///////////////////////////////////////////////
        .selectAll("path")
        .data(links);

    path.enter().append("svg:path")
            .attr("id", function (d) { return d.source.index + "_" + d.target.index; })
            .attr("class", function (d) { return "link " + d.type; })
            .attr("marker-end", function (d) { return "url(#" + d.type + ")"; });

    path.exit().remove();

    /////////////////////////////////////////////////////////////////////////////////////
    //Problem
    //  another g structure is added every update
    //Solution:
    //  create a data join on the g and class it .labels 
    //  append the labels g on the resulting enter selection
    //  this will only be appeneded once
    //  include .exit().remove() to be defensive
    //Note:
    //  don't chain .enter() on the object assigned to path_label
    //  .data(...) returns an update selection which includes enter() and exit() methods
    //  .enter() returns a standard selection which doesn't have a .exit() member
    //  this will be needed if links are removed or even if the node indexing changes
    /////////////////////////////////////////////////////////////////////////////////////

//ADD//////////////////////////////////////////////////////////////////////////////////
    //Link labels bag
    //UPDATE
    var path_labels = svg.selectAll(".labels")
            .data(["labels_g"]);
    //ENTER
    path_labels.enter()
///////////////////////////////////////////////////////////////////////////////////////

        .append("svg:g")

//ADD//////////////////////////////////////////////////////////////////////////////////
        .attr("class", "labels");
///////////////////////////////////////////////////////////////////////////////////////

    //Link labels
    //UPDATE
    var path_label = path_labels
        .selectAll(".path_label")
        .data(links);
    //ENTER
    path_label
        .enter().append("svg:text")
            .attr("class", "path_label")
            .append("svg:textPath")
                .attr("startOffset", "50%")
                .attr("text-anchor", "middle")
                .attr("xlink:href", function (d) { return "#" + d.source.index + "_" + d.target.index; })
                .style("fill", "#000")
                .style("font-family", "Arial")
                .text(function (d) { return d.type; });

//ADD//////////////////////////////////////////////////////////////////////////////////
    path_label.exit().remove();
///////////////////////////////////////////////////////////////////////////////////////

    /////////////////////////////////////////////////////////////////////////////////////
    //Problem
    //  another g structure is added every update
    //Solution:
    //  create a data join on the g and class it .circles 
    //  append the labels g on the resulting enter selection
    //  this will only be appeneded once
    //  include .exit().remove() to be defensive
    /////////////////////////////////////////////////////////////////////////////////////

//ADD//////////////////////////////////////////////////////////////////////////////////
    //Nodes bag
    //UPDATE
    var circles = svg.selectAll(".circles")
                .data(["circles_g"]);
    //ENTER
    circles.enter()
///////////////////////////////////////////////////////////////////////////////////////

        .append("svg:g")

//ADD//////////////////////////////////////////////////////////////////////////////////
        .attr("class", "circles");
///////////////////////////////////////////////////////////////////////////////////////

    //Nodes
    //UPDATE
    circle = circles
        .selectAll(".node") //select on class instead of tag name//////////////////////////
        .data(nodes);
    circle                              //don't chain in order to keep the update selection////////////
        .enter().append("svg:circle")
            .attr("class", "node")
            .attr("fill", function (d, i) {
                return node_colors[d.types[0]];
            })
            .attr("r", function (d) { return d.types.indexOf("Document") == 0 ? 24 : 12; })
            .attr("stroke", "#000")
            .on("click", clicked)
            .on("dblclick", dblclick)
            .on("contextmenu", cmdclick)
            .call(force.drag);

//ADD//////////////////////////////////////////////////////////////////////////////////
    circle.exit().remove();
///////////////////////////////////////////////////////////////////////////////////////

//ADD//////////////////////////////////////////////////////////////////////////////////
    //Anchors bag
    //UPDATE
    var textBag = svg.selectAll(".anchors")
                .data(["anchors_g"]);
    //ENTER
    textBag.enter()
///////////////////////////////////////////////////////////////////////////////////////

        .append("svg:g")

//ADD//////////////////////////////////////////////////////////////////////////////////
        .attr("class", "anchors");

    //Anchors
    //UPDATE
    text = textBag
///////////////////////////////////////////////////////////////////////////////////////

        .selectAll(".anchor")
            .data(nodes, function (d) { return d.name});
var textEnter = text            //don't chain in order to keep the update selection//////////
        .enter()
        .append("svg:g")
            .attr("class", "anchor")
            .attr("text-anchor", "middle");

//ADD//////////////////////////////////////////////////////////////////////////////////
    text.exit().remove;
///////////////////////////////////////////////////////////////////////////////////////

    // A copy of the text with a thick white stroke for legibility.
    textEnter.append("svg:text")
            .attr("x", 8)
            .attr("y", ".31em")
            .attr("class", "shadow")
            .text(function (d) { return d.name; });

    textEnter.append("svg:text")
            .attr("x", 8)
            .attr("y", ".31em")
            .text(function (d) { return d.name; });

    // calling force.drag() here returns the drag _behavior_ on which to set a listener
    // node element event listeners
    force.drag().on("dragstart", function (d) {
        d3.selectAll(".dbox").style("z-index", 0);
        d3.select("#dbox" + d.index).style("z-index", 1);
    })
}

【讨论】:

  • 谢谢酷,您修改后的代码超越了这里。我发现它有效,但它丢失了路径标签。我稍微修改了一下,然后把它们拿回来了;结果反映在我的在线示例中。在您修改我的命名约定时,我有点困惑;但是,您对“一般模式”的 cmets 非常有启发性。还有很多我不明白,但这是学习的好代码。
  • @jjon 你可以通过添加一行代码来修复路径标签,我已经更新了修改后的代码部分来解决这个问题。顺便说一下,在您的最新版本中,标签没有附带添加的链接。单行修订版也解决了这一问题。
  • @jjon 添加了保留您的命名约定的最小修订代码。无论如何,我都想这样做,以确保我了解最低要求。
  • 再次感谢@coolblue。我的在线示例已用您的代码逐字修改。我会继续修补一段时间,但它不会在网上持续很长时间。但是,您的答案中的注释代码非常全面,即使在没有在线示例的情况下也将继续有用。谢谢!
猜你喜欢
  • 2015-09-17
  • 2012-09-10
  • 2013-07-15
  • 1970-01-01
  • 2016-06-05
  • 2013-08-14
  • 2017-03-02
  • 2016-08-15
  • 2015-06-25
相关资源
最近更新 更多