Tree layout without mutation (or copy hierarchy?)

Hello!

My goal is to:

  1. Display several trees of data using d3.hierarchy and a force simulation.
  2. On clicking any node in one of the hierarchies:
    a: update the visualization to show only the node sin the selected hierarchy.
    b: transition those nodes to a tree layout Tree | D3 by Observable, excluding the leaf nodes, which will continue to be clustered around their parent nodes via the force simulation.

I have things working nicely through 2a, but I’m stuck on the tree layout. There are two main issues. I think solving either one would be sufficient.

First, the tree layout mutates the underlying data. If I apply it to the same hierarchy I’m using for the existing simulation, they don’t play nice–one will overwrite the values of the other.

This wouldn’t be a problem if I could just run the layout on a partial copy (i.e. excluding leaf nodes) of the hierarchy, grab the values, then use them to set X and Y forces in the simulation. However, (problem 2) I can’t seem to cleanly duplicate a hierarchy. Even if I iterate through and create a new hierarchy, copying field by field, something still seems to be passing by reference and leaks state from the old version as its mutated by the simulation. I’m new to JS generally, so I might be missing something obvious here!

Any help would be much appreciated!

Can you share a minimal example that demonstrates the problem?

As these things go, soon after I posted here I was able to resolve the issue. Here’s the relevant code:

function duplicateHierarchy(originalHierarchy: d3.HierarchyNode<any>, excludeLeaves: boolean = false): d3.HierarchyNode<any> {
    function createNode(data: any): any {
        const newNode: any = {
            uuid: data.uuid,
            children: []
        };
        return newNode;
    }
    function traverse(node: d3.HierarchyNode<any>, parent: any): void {
        const newNode = createNode(node.data);
        if (!excludeLeaves || (node.children && node.children.length > 0)) {
            parent.children.push(newNode);
            if (node.children) {
                node.children.forEach(child => traverse(child, newNode));
            }
        }
    }
    const newData = {
        uuid: originalHierarchy.data.uuid,
        children: []
    };
    traverse(originalHierarchy, newData);
    return d3.hierarchy(newData);
}

export function generateTreeLayoutMapping(root: d3.HierarchyNode<any>, width: number, height: number, excludeLeaves: boolean = false): Record<string, { x: number, y: number }> {
    const copiedHierarchy = duplicateHierarchy(root, excludeLeaves);
    const treeLayout = d3.tree().size([width, height]);
    const tree = treeLayout(copiedHierarchy);
    const mapping: Record<string, { x: number, y: number }> = {};
    tree.each(node => {
        mapping[(node as d3.HierarchyNode<any>).data.uuid] = { x: node.x, y: node.y };
    });
    return mapping;
}