javascriptd3.jsforce-layout

D3 force layout with some nodes collapsed


I am a complete beginner in d3 and have been stuck with a problem for weeks.

Based on the example: https://jsfiddle.net/t4vzg650/6/ I have developed a collapsible force layout with d3. I have successfully managed to add links and audios (only work locally at the moment). But by default, I only want the best practice, tools and course structure nodes to be expanded and the other nodes to be collapsed. They can then be opened on demand with a double click. I have gone through similar questions here, but I have not managed to implement it.

var data = {
"name": "Best Practices",
"size": 50,
"color": "#C37B89",
"font": 15,
"children": [{
"name": "Course Structure",
"size": 50,
"color": "#BCCC9A",
"font": 15,
"_children": null,
"children": [{
  "name": "x",
  "size": 30,
  "color": "#BCCC9A",
  "font": 8,
  "_children": null,
  "children" : [{
    "name": "x",
    "size": 30,
    "color": "#BCCC9A",
    "font": 8,
  }, {
    "name": "x",
    "size": 30,
    "color": "#BCCC9A",
    "font": 8,
  }
]
}, {
  "name": "x",
  "size": 30,
  "color": "#BCCC9A",
  "font": 8,
}]
}, {
"name": "Tools",
"size": 50,
"color": "#EAE7C6",
"font": 15,
"_children": null,
"children": [{
  "name": "x",
  "size": 30,
  "color": "#EAE7C6",
  "font": 8
}, {
  "name": "x",
  "size": 30,
  "color": "#EAE7C6",
  "font": 8
}]
}]
};

var i = 0;

var root = d3.hierarchy(data);

var nodeSvg, linkSvg, nodeEnter, linkEnter;

var width = 960
var height = 600

var centerx = width/2
var centery = height/2

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

var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
//center network
.force("center", d3.forceCenter(centerx, centery))
.on("tick", ticked);

update();

function update() {

var nodes = flatten(root);
var links = root.links()

linkSvg = svg.selectAll(".link")
.data(links, function(d) {
return d.target.id;

})

linkSvg.exit().remove();

var linkEnter = linkSvg.enter()
.append("line")
.attr("class", "link");

linkSvg = linkEnter.merge(linkSvg)

nodeSvg = svg.selectAll(".node")
.data(nodes, function(d) {
return d.id;
})

nodeSvg.exit().remove();

var nodeEnter = nodeSvg.enter()
.append("g")
.attr("class", "node")
.on("dblclick", click)
.call(d3.drag()
.on("end", dragended))

nodeEnter.append("circle")
.attr("r", function(d) {return d.data.size})
.attr("fill", function(d) {return d.data.color})

nodeEnter.append("text")
.text(function(d) {
return d.data.name;})
.attr("font-size", function(d) {return d.data.font})
.attr("class", "headline")

nodeSvg = nodeEnter.merge(nodeSvg);

simulation
.nodes(nodes)

simulation.force("link")
.links(links);

}

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

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

function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
update();
simulation.restart();
} else {
d.children = d._children;
d._children = null;
update();
simulation.restart();
}
}


function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}


function flatten(root) {

var nodes = [];

function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
else ++i;
nodes.push(node);
}
recurse(root);
return nodes;
}

My code is here: https://codepen.io/LaraB1612/pen/bGreEWV

I would be so happy if someone could help me with this.

Thank you very much


Solution

  • I have figured out how to implement this:

    1. You don't need to run the update and restart methods in each condition of the click method, once is enough after the conditions are checked:
    function click(d) {
      if (d.children) {
        d._children = d.children;
        d.children = null;
      } else {
        d.children = d._children;
        d._children = null;
      }
        update();
    }
    
    1. You need to call the update method after invoking the flatten method. You also need to flatten both childs of the root in this case, both Tools and Course Structure. It might not be the most elegant solution, but it's working:
    var flattenedChildOne = flatten(root.children[0]);
    var flattenedChildTwo = flatten(root.children[1]);
    
    flattenedChildOne.forEach(function(d) {
      d._children = d.children;
      d.children = null;
    });
    
    flattenedChildTwo.forEach(function(d) {
      d._children = d.children;
      d.children = null;
    });
    update();
    
    1. I have removed all the "_children": null from your data, they were unnecessary.

    I have solved this in here: https://jsfiddle.net/orsisi/tnv37wxr/39/