d3.js collapsible force layout with all the nodes

2019-03-06 14:34发布

问题:

I've been trying to implement a directed force layout using a json file that I've written. Before I tried to make it begin with all nodes collapsed, it was working properly. I've declared a property called "index" for all the nodes, indicating which level of the tree they belong (root's index is 0, it's children are 1, etc.) I'm guessing that there is a problem with "index" property of the nodes because when I first start my page their values are correct, but when I collapse and re-open one a node the index values of related nodes change and it does not draw the links properly anymore.

Any ideas on why it keeps changing the index values that are taken from json whenever I click anything, or do you have any reference projects I might look into to solve the problem?

Thanks,

Here is my script code:

var indx = 0;

var width = 1260, height = 1220, root;

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

var svg = d3.select("#chart").append("svg")
        .attr("width", width).attr("height", height);

var link = svg.selectAll(".link"), node = svg
        .selectAll(".node");

var text = svg.selectAll(".link");


d3.json("graph.json", function(error, json) {

    root = json;

    update();

});

function toggle(d) {

    indx = d.index;

    if (d.children) {
        d._children = d.children;
        d.children = null;
    } else {
        d.children = d._children;
        d._children = null;
    }
    update();
}

function prepareLinks(allNodes) {

    var newLinks = new Array();

    var allLinks = d3.layout.tree().links(allNodes);

    for ( var i = 0; allLinks.length > i; i++) {

        if (allLinks[i].source.index <= indx) {
            console.log("source : " + allLinks[i].source.index
                    + "-" + allLinks[i].source.name);
            newLinks.push(allLinks[i]);
        }
        console.log(allLinks[i].target.index + "-"
                + allLinks[i].target.name);

    }

    for ( var i = 0; newLinks.length > i; i++) {

            console.log("newLinks : " + newLinks[i].source.index
                    + "-" + newLinks[i].source.name);
        console.log(newLinks[i].target.index + "-"
                + newLinks[i].target.name);

    }

    return newLinks;

}

function update() {

    var nodes = flatten(root, indx);
    var links = prepareLinks(nodes);


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

    // Update links.   
    link = link.data(links, function(d) {

        return d.target.id;

    });

    link.exit().remove();

    link.enter().insert("line", ".node").attr("class", "link");

    svg.selectAll("g.ltext").remove();

    text = svg.append("svg:g").selectAll("g").data(links);

    text.enter().append("svg:g").attr("class", "ltext");

    text.append("svg:text").attr("class", "linktext").attr(
            "dx", 5).attr("dy", ".35em").text(
            function(d) {

                return d.source.type;
            });

    // Update nodes.
    node = node.data(nodes, function(d) {
        return d.id;
    });

    node.exit().remove();

    var nodeEnter = node.enter().append("g").attr("class",
            "node").on("click", function(d) {
        toggle(d);
        update(d);
    }).call(force.drag);

    nodeEnter.append("circle").attr("r", function(d) {
        return Math.sqrt(d.size) / 10 || 10;
    });

    nodeEnter.append("text").attr("dy", ".35em").text(
            function(d) {
                return d.name;
            });

    node.select("circle").style("fill", color);

}

function tick() {

    link.attr("x1", function(d) {
        return d.target.x;
    }).attr("y1", function(d) {
        return d.target.y;
    }).attr("x2", function(d) {
        return d.source.x;
    }).attr("y2", function(d) {
        return d.source.y;
    });

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

    text.attr("transform", function(d) {
        var sourcex = d.source.x;
        var sourcey = d.source.y;
        var targetx = d.target.x;
        var targety = d.target.y;

        return "translate(" + (sourcex + targetx) / 2 + ","
                + (sourcey + targety) / 2 + ")";
    });

}

function color(d) {
    return d._children ? "#BA8343" // collapsed package
    : d.children ? "#7C4C6B" // expanded package
    : "#6F704A"; // leaf node
}

// Toggle children on click.

function click(d) {
    if (d3.event.defaultPrevented)
        return; // ignore drag

    if (d.children) {
        d._children = d.children;
        d.children = null;
    } else {
        d.children = d._children;
        d._children = null;
    }

    update(d);
}

// Returns a list of all nodes under the root.
function flatten(root, indx) {

    var nodes = [], i = 0;

    function recurse(node) {
        console.log("flatten'da başta index: " + node.index
                + "ad " + node.name);
        if (node.children) {

            node.children.forEach(recurse);

        }
        if (!node.id)
            node.id = ++i;

        if (indx >= node.index || indx + 1 == node.index) {

            nodes.push(node);
            //                          console.log("flatten'da index: "+ node.index+ "ad "+ node.name);
        }
    }

    recurse(root);
    return nodes;
}

And here is an example from my json file:

{
 "name": "Data Mining","type":"related","index":2,
 "children": [
  {
  "name": "Yapay Sinir Ağları", "size": 7074,"type":"related","index":3,
    "children": [
        {
        "name": "Compututional Intelligence","type":"narrower","index":4,
        "children":[
            {"name": "Genetik Algoritmalar", "size": 7074,"type":"related","index":5},
            {"name": "Bulanık Mantık", "size": 7074,"type":"related","index":5},
            {"name":"Soft Computing","type":"related","index":5,
                "children": [
                    {"name": "Esnek Hesaplama", "size": 7074,"type":"related","index":6}
                ]
             } 
         ]
        }
     ]
 }

回答1:

Most likely the problem is that index is a reserved property of nodes in d3's force layout, and it's getting overwritten when the nodes property is updated.

force.nodes([nodes])

If nodes is specified, sets the layout's associated nodes to the specified array. If nodes is not specified, returns the current array, which defaults to the empty array. Each node has the following attributes:

index - the zero-based index of the node within the nodes array.

x - the x-coordinate of the current node position.

y - the y-coordinate of the current node position.

px - the x-coordinate of the previous node position.

py - the y-coordinate of the previous node position.

fixed - a boolean indicating whether node position is locked.

weight - the node weight; the number of associated links.

These attributes do not need to be set before passing the nodes to the layout; if they are not set, suitable defaults will be initialized by the layout when start is called. However, be aware that if you are storing other data on your nodes, your data attributes should not conflict with the above properties used by the layout.

Documentation

To prevent the conflict, rename the index property in your json to something else. In my own project I'm referring to the distance of a given node from the root as it's depth.