3

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:

enter image description here

<!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.

ICoded
  • 319
  • 3
  • 18
  • Since you want to fix the `innerNodes` somewhere within their `outerNodes`, would you consider an option not to use a force network (i.e. `innerLayout`) and just update `innerNodes` (and `innerLinks`) in `ticked` based on the updated positions? – Robin Mackenzie Apr 15 '21 at 10:11
  • @RobinMackenzie unfortunately I need to update the outerNodes and their links too. Please correct me if I am wrong but a innerLayout doesn´t meet this requirements right? As it is similar to a TreeLayout. I don´t mind to keep the inner- and outer graph separate as long as I could set a specific outerNode gravity to an innerNode. Which kinda does the job too. – ICoded Apr 15 '21 at 10:36
  • Oh I meant keep the force layout for outer but draw the inner bits without a 2nd force layout? Maybe I am over simplifying it... – Robin Mackenzie Apr 15 '21 at 10:38
  • @RobinMackenzie for now I can´t think about a practical need to keep those 2nd force. It only brings a bit of joy to drag those inner nodes around during data presentations. Thanks for the hint. Do you know practical resources to guide me, as it is kinda nested visualization. – ICoded Apr 15 '21 at 10:46
  • 1
    Would you consider something like this: https://ialab.it.monash.edu/webcola/examples/smallgroups.html – Robin Mackenzie Apr 15 '21 at 11:04
  • @RobinMackenzie yes, I guess this can be rewritten to get circles instead rectangles. – ICoded Apr 15 '21 at 11:10
  • @RobinMackenzie I also found the codepen version. Unfortunately it receive several errors. I still prefer a nativ d3.js solution as I am at least a bit familiar with it. https://codepen.io/karlin/pen/qdYYmz – ICoded Apr 15 '21 at 11:21

1 Answers1

3

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 so nodes have a parent 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 - for innerNodes 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 - for innerLinks, force the source and target 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>
Robin Mackenzie
  • 18,801
  • 7
  • 38
  • 56
  • 1
    the root example I modified was this http://jsfiddle.net/s6y183mh/4/. Further I noticed the existing of D3.pack() which allows to bundle different layouts. I will also play around with those and let you know. https://bl.ocks.org/VinodLouis/1f729f4e6b121b9c820c36246edc3fff – ICoded Apr 16 '21 at 05:42
  • 1
    @ICoded - thanks; pleased to have a crack at it - it's an interesting problem. Keen to hear if you get a better outcome with `pack()`. All the best. – Robin Mackenzie Apr 16 '21 at 06:21
  • wouldn´t it be possible to define the force separately. Like ("center", d3.forceCenter(outerNodeX, outerNodeY).. of course with an proper condition as soon as working. – ICoded Apr 16 '21 at 08:14