I want to visualize the “children” insight each node. I guess the D3v6 .join() function can be nested. Unfortunately I can´t find any example. The snippet below contains an outerGraph with 3 nodes and children as attribute. So far those children aren´t used yet.
The innerGraph instead visualize the small nodes which will be obsolete as soon as the children approach is working. Another Idea would be to work with those two graphs and create a gravity / cluster, which will be the parent.
Goal: Either utilize the children attribute or combine both graphs with the help of an cluster /gravity or even nested join(). I am appreciating any hint / tip. The visuals result should be:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>D3v6 nested nodes</title> <script src="https://d3js.org/d3.v6.min.js"></script> </head> <style> body { background-color: whitesmoke; } </style> <body> <script> var svg = d3.select("body").append("svg") .attr("width", window.innerWidth) .attr("height", window.innerHeight); var width = window.innerWidth var height = window.innerHeight var outerLinkContainer = svg.append("g").attr("class", "outerLinkContainer") var outerNodeContainer = svg.append("g").attr("class", "outerNodeContainer") var innerLinkContainer = svg.append("g").attr("class", "innerLinkContainer") var innerNodeContainer = svg.append("g").attr("class", "innerNodeContainer") //############################################### //############# outer force Layouts ############# //############################################### var outerGraph = { "nodes": [ { "id": "A", "children": [ { "id": "A1" }, { "id": "A2" } ] }, { "id": "B", "children": [ { "id": "B1" }, { "id": "B2" } ] }, { "id": "C", "children": [ { "id": "C1" }, { "id": "C2" } ] } ], "links": [ { "source": "A", "target": "B" }, { "source": "B", "target": "C" }, { "source": "C", "target": "A" }, ] } var outerLayout = d3.forceSimulation() .force("center", d3.forceCenter(width / 2, height / 2)) .force("charge", d3.forceManyBody().strength(-500)) .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(200)) var outerLinks = outerLinkContainer.selectAll(".link") .data(outerGraph.links) .join("line") .attr("class", "link") .style("stroke", "black") .style("opacity", 0.2) var outerNodes = outerNodeContainer.selectAll("g.outer") .data(outerGraph.nodes, function (d) { return d.id; }) .join("circle") .attr("class", "outer") .style("fill", "pink") .style("stroke", "blue") .attr("r", 40) .call(d3.drag() .on("start", dragStarted) .on("drag", dragged) .on("end", dragEnded) ) outerLayout .nodes(outerGraph.nodes) .on("tick", ticked) outerLayout .force("link") .links(outerGraph.links) //############################################### //############## inner force Layout ############# //############################################### var innerGraph = { "nodes": [ { "id": "A1" }, { "id": "A2" } ], "links": [ { "source": "A1", "target": "A2" } ] } var innerlayout = d3.forceSimulation() .force("center", d3.forceCenter(width / 2, height / 2)) .force("charge", d3.forceManyBody().strength(-500)) .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(200)) var innerLinks = innerLinkContainer.selectAll(".link") .data(innerGraph.links) .join("line") .attr("class", "link") .style("stroke", "black") var innerNodes = innerNodeContainer.selectAll("g.inner") .data(innerGraph.nodes, function (d) { return d.id; }) .join("circle") .style("fill", "orange") .style("stroke", "blue") .attr("r", 6) .attr("class", "inner") .attr("id", function (d) { return d.id; }) .call(d3.drag() .on("start", dragStarted) .on("drag", dragged) .on("end", dragEnded) ) innerlayout .nodes(innerGraph.nodes) .on("tick", ticked) innerlayout .force("link") .links(innerGraph.links) //############################################### //################## functons ################### //############################################### function ticked() { outerLinks .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; }); innerLinks .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; }); outerNodes.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); innerNodes.attr("transform", function (d) { return "translate(" + (d.x) + "," + (d.y) + ")"; }); } function dragStarted(event, d) { if (!event.active) outerLayout.alphaTarget(0.3).restart(); innerlayout.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragEnded(event, d) { if (!event.active) outerLayout.alphaTarget(0) innerlayout.alphaTarget(0) d.fx = undefined; d.fy = undefined; } </script> </body> </html>
I will update the post as soon as I found a solution.
Advertisement
Answer
Here’s a slightly hack way to do it – I am a bit disappointed in the outcome because if you play with the outerNodes
then the links between innerNodes
cross over in an unattractive way.
The changes I made in your code:
- update
innerGraph
sonodes
have aparent
property (plus add the links required to match your screenshot in the question) - add an additional class on
outerNodes
so that each outer node can be identified e.g..outer_A
,.outer_B
etc - add an additional class on
innerNodes
so that each inner node can be identified e.g..child_A1
,.child_A2
etc - in
ticked
– forinnerNodes
return a point for the inner node so that it is sitting inside centre of it’s parent at roughly 20px from the centre on the vector between the original force simulation selected point and the parent’s centre. - in
ticked
– forinnerLinks
, force thesource
andtarget
coordinates to update per the previous step
Those last two points are per here and here.
So it works – but only just. Vertical scrolling in the stack snippet seems to upset it a bit but it’s maybe better if you try it out on your own dev environment. I still think you could you look at other tools – maybe this one from cytoscape.js and also the webcola example I mentioned in the comments?
var svg = d3.select("body").append("svg") .attr("width", window.innerWidth) .attr("height", window.innerHeight); var width = window.innerWidth var height = window.innerHeight var outerLinkContainer = svg.append("g").attr("class", "outerLinkContainer") var outerNodeContainer = svg.append("g").attr("class", "outerNodeContainer") var innerLinkContainer = svg.append("g").attr("class", "innerLinkContainer") var innerNodeContainer = svg.append("g").attr("class", "innerNodeContainer") //############################################### //############# outer force Layouts ############# //############################################### var outerGraph = { "nodes": [ { "id": "A", "children": [ { "id": "A1" }, { "id": "A2" } ] }, { "id": "B", "children": [ { "id": "B1" }, { "id": "B2" } ] }, { "id": "C", "children": [ { "id": "C1" }, { "id": "C2" } ] } ], "links": [ { "source": "A", "target": "B" }, { "source": "B", "target": "C" }, { "source": "C", "target": "A" }, ] } var outerLayout = d3.forceSimulation() .force("center", d3.forceCenter(width / 2, height / 2)) .force("charge", d3.forceManyBody().strength(-500)) .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(200)) var outerLinks = outerLinkContainer.selectAll(".link") .data(outerGraph.links) .join("line") .attr("class", "link") .style("stroke", "black") .style("opacity", 0.2) var outerNodes = outerNodeContainer.selectAll("g.outer") .data(outerGraph.nodes, function (d) { return d.id; }) .join("circle") .attr("class", d => `outer outer_${d.id}`) .style("fill", "pink") .style("stroke", "blue") .attr("r", 40) .call(d3.drag() .on("start", dragStarted) .on("drag", dragged) .on("end", dragEnded) ) outerLayout .nodes(outerGraph.nodes) .on("tick", ticked) outerLayout .force("link") .links(outerGraph.links) //############################################### //############## inner force Layout ############# //############################################### var innerGraph = { "nodes": [ { "id": "A1", "parent": "A" }, { "id": "A2", "parent": "A" }, { "id": "B1", "parent": "B" }, { "id": "B2", "parent": "B" }, { "id": "C1", "parent": "C" }, { "id": "C2", "parent": "C" } ], "links": [ { "source": "A1", "target": "A2" }, { "source": "A2", "target": "B2" }, { "source": "A1", "target": "C2" }, { "source": "B1", "target": "B2" }, { "source": "B1", "target": "C1" }, { "source": "C2", "target": "C1" } ] } var innerlayout = d3.forceSimulation() .force("center", d3.forceCenter(width / 2, height / 2)) .force("charge", d3.forceManyBody().strength(-500)) .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(200)) var innerLinks = innerLinkContainer.selectAll(".link") .data(innerGraph.links) .join("line") .attr("class", "link linkChild") .style("stroke", "black") var innerNodes = innerNodeContainer.selectAll("g.inner") .data(innerGraph.nodes, function (d) { return d.id; }) .join("circle") .style("fill", "orange") .style("stroke", "blue") .attr("r", 6) .attr("class", d => `inner child_${d.id}`) .attr("id", function (d) { return d.id; }) .call(d3.drag() .on("start", dragStarted) .on("drag", dragged) .on("end", dragEnded) ) innerlayout .nodes(innerGraph.nodes) .on("tick", ticked) innerlayout .force("link") .links(innerGraph.links) //############################################### //################## functons ################### //############################################### function ticked() { outerLinks .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; }); outerNodes.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); innerNodes.attr("transform", function (d) { var parent = d3.select(`.outer_${d.parent}`); var pr = parent.node().getBoundingClientRect(); var prx = pr.left + (pr.width / 2); var pry = pr.top + (pr.height / 2); var distance = Math.sqrt( ((d.x - prx) ** 2) + ((d.y - pry) ** 2 )); var ratio = 20 / distance; var childX = ((1 - ratio) * prx) + (ratio * d.x); var childY = ((1 - ratio) * pry) + (ratio * d.y); return "translate(" + (childX) + "," + (childY) + ")"; }); innerLinks.attr("x1", d => { var m1 = d3.select(`.child_${d.source.id}`).node().transform.baseVal[0].matrix; return m1.e; }).attr("y1", d => { var m1 = d3.select(`.child_${d.source.id}`).node().transform.baseVal[0].matrix; return m1.f; }).attr("x2", d => { var m2 = d3.select(`.child_${d.target.id}`).node().transform.baseVal[0].matrix; return m2.e; }).attr("y2", d => { var m2 = d3.select(`.child_${d.target.id}`).node().transform.baseVal[0].matrix; return m2.f; }); } function dragStarted(event, d) { if (!event.active) outerLayout.alphaTarget(0.3).restart(); innerlayout.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragEnded(event, d) { if (!event.active) outerLayout.alphaTarget(0) innerlayout.alphaTarget(0) d.fx = undefined; d.fy = undefined; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>