implement General Update Pattern for d3.js force layout

时间:2015-04-24 21:34:24

标签: javascript d3.js force-layout

Beginning with this example (possibly not the best choice), I set about trying to develop an application to suit my purposes, and learn d3.js in the process. After a lot of naive tinkering, I managed to get a toy force layout for my test data that satisfied me in its appearance and behavior. Now I've set about trying to understand MB's General Update Pattern in the context of my particular example, so that users can interactively modify the graph. I clearly have not yet grasped the principle.

Starting small, I thought to create a function that would simply add a single additional link to the graph between the nodes labeled "Walteri" and "Roberti de Fonte" (there's a button that executes addedge(), or you can execute it from the js console). In a broken sort of way, this had the desired result; however, the existing graph remained in place while a duplicate graph was generated containing the additional link. It's clear that there is something about the General Update Pattern that I'm still not understanding.

If anyone has a look and can offer any insight, I'd be grateful.

1 个答案:

答案 0 :(得分:1)

管理更新
您遇到的主要问题是每次更新时都会重新附加组元素 你可以得到d3为你管理这个......

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

你只需要组成一个单元素阵列来驱动它。关于它的好处是它将被放置在由{3}添加到__data__元素的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不会为空,并将使用valueskeys进行比较,以确定哪些节点正在更新,进入和退出节点。这就是您在数据加入之前需要选择的原因。

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

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

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

修订后的代码
有一个奇怪的错误,当添加边缘时,链接有时会消失,但是由于节点中NaNd.x中的d.y而导致链接消失了。 如果您不是每次都在showme重建力布局,并且如果您这样做......

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

这个bug消失了,一切正常。

  

这是因为布局的内部状态不包括   额外链接,尤其是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的评论和我自己的启发,这里是原始代码的最小变化,具有相同的命名约定和差异注释。正确添加链接所需的模块未更改,未进行讨论......

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