I have a Dendrogram / cluster diagram’s root using d3.hierarchy. I’m trying to update the root with a selected node which should become the new head, with a new tree drawn with that node at the top. This should replace the old tree. The steps are as follows:
- read in flat data
- convert to hierarchy using d3.stratify
- convert this to a cluster (with coordinates etc)
- draw using new select.join (which no longer needs explicit exit / remove)
- user clicks on a node’s circle
- update hierarchy with selected node as the new root with parents removed
- re-draw, with nodes no longer present in the data (the parent and upwards) removed by join
However, it re-draws the new, smaller root and dependents but all of the old SVG is still there. I’ve tried explicitly adding exit/ remove but that doesn’t help.
What am I doing wrong?
A simplified, reproducible example can be see here. I’ve also created a fiddle at https://jsfiddle.net/colourblue/zp7ujra3/9/
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://d3js.org/d3.v6.js"></script> </head> <body> <div id="vis"></div> <script> let treeData = [] let currentTreeData = [] var flatData = [ { "ID" : 1000, "name" : "The Root", "parentID":null}, { "ID" : 1100, "name" : "Child 1", "parentID":1000 }, { "ID" : 1110, "name" : "G.Child 1.1", "parentID":1100 }, { "ID" : 1120, "name" : "G.Child 1.2", "parentID":1100 }, { "ID" : 1130, "name" : "G.Child 1.3", "parentID":1100 }, { "ID" : 1200, "name" : "Child 2", "parentID":1000 }, { "ID" : 1210, "name" : "G.Child 2.1", "parentID":1200 }, { "ID" : 1211, "name" : "G.G.Child 2.1.1", "parentID":1210 }, { "ID" : 1212, "name" : "G.G.Child 2.2.2", "parentID":1210 }, { "ID" : 12111, "name" : "G.G.G.Child 2.1.1.1", "parentID":1211 }, { "ID" : 1300, "name" : "Child 3", "parentID":1000 } ]; function chart(thisTreeData) { let root = clusterise(thisTreeData) // Add nodes (links) svg.append("g") .attr("class", "node") .attr("fill", "none") .attr("stroke", "#555") .attr("stroke-opacity", 0.3) .selectAll("path") .data(root.links(), function(d) { return "Link" + ":" + d.target.data.id }) .join("path") .attr("d", d3.linkRadial() .angle(d => d.x) .radius(d => d.y)); // Add circles svg.append("g") .attr("class", "node") .selectAll("circle") .data(root.descendants(), function(d) { return "Circle" + d.data.id; }) .join("circle") .attr("transform", d => ` rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0) `) .attr("r", 3) .on('click', click); // Add text svg.append("g") .attr("class", "node") .selectAll("text") .data(root.descendants(), function(d) { return "Text" + d.data.id; }) .join("text") .attr("transform", d => ` rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0) rotate(${d.x >= Math.PI ? 180 : 0}) `) .attr("text-anchor", d => d.x < Math.PI === !d.children ? "start" : "end") .text(d => d.data.data.name); } // Switch tree on click so centre is now selected node function click(event,d) { currentTreeData = findNode(treeData, d.data.id) chart(currentTreeData); } // HELPER FUNCTIONS // ---------------- // Function to Strafify flat CSV data into a tree function convertToHierarchy(data) { var stratify = d3.stratify() .parentId(function (d) { return d.parentID; }) .id(function (d) { return d.ID; }); let treeData = stratify(data); return (treeData) } // Function to Create d3 cluster with coordinates etc from stratified data function clusterise(treeData) { tree = d3.cluster().size([2 * Math.PI, radius - 100]) let root = tree(d3.hierarchy(treeData) .sort((a, b) => d3.ascending(a.name, b.name))); return (root) } function findNode(root, id) { console.log(root); let selected = root.find(obj => obj.id === id); selected.parent= null; console.log(selected); return(selected) } width = 800 height = 600 radius = width / 2 let svg = d3.select("#vis") .append('svg') .attr('width', width) .attr('height', height) .append('g') .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); treeData = convertToHierarchy(flatData) currentTreeData = treeData chart(currentTreeData); </script> </body> </html>
Advertisement
Answer
Here’s a much more complicated example which properly handles the enter
, update
, and exit
pattern with the newish .join
method. This does allow you to add transitions. Note, I removed your inner-wrapper g
nodes. Since every click appended a new one this messes up selections of your visible nodes (the paths, circles and text).
<html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://d3js.org/d3.v6.js"></script> </head> <body> <div id="vis"></div> <script> let treeData = []; let currentTreeData = []; var flatData = [ { ID: 1000, name: 'The Root', parentID: null }, { ID: 1100, name: 'Child 1', parentID: 1000 }, { ID: 1110, name: 'G.Child 1.1', parentID: 1100 }, { ID: 1120, name: 'G.Child 1.2', parentID: 1100 }, { ID: 1130, name: 'G.Child 1.3', parentID: 1100 }, { ID: 1200, name: 'Child 2', parentID: 1000 }, { ID: 1210, name: 'G.Child 2.1', parentID: 1200 }, { ID: 1211, name: 'G.G.Child 2.1.1', parentID: 1210 }, { ID: 1212, name: 'G.G.Child 2.2.2', parentID: 1210 }, { ID: 12111, name: 'G.G.G.Child 2.1.1.1', parentID: 1211 }, { ID: 1300, name: 'Child 3', parentID: 1000 }, ]; function chart(thisTreeData) { let root = clusterise(thisTreeData); // Add nodes (links) svg .selectAll('.line') .data(root.links(), function (d) { return 'Link' + ':' + d.target.data.id; }) .join( function (enter) { return enter .append('path') .attr('class', 'line') .attr( 'd', d3 .linkRadial() .angle((d) => d.x) .radius((d) => d.y) ) .attr('fill', 'none') .attr('stroke', '#555') .attr('stroke-opacity', 0.3); }, function (update) { update .transition() .duration(1000) .attr( 'd', d3 .linkRadial() .angle((d) => d.x) .radius((d) => d.y) ); return update; }, function (exit) { return exit.remove(); } ); // Add text svg .selectAll('.word') .data(root.descendants(), function (d) { return 'Text' + d.data.id; }) .join( function (enter) { return enter .append('text') .attr('class', 'word') .attr( 'transform', (d) => ` rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0) rotate(${d.x >= Math.PI ? 180 : 0}) ` ) .attr('text-anchor', (d) => d.x < Math.PI === !d.children ? 'start' : 'end' ) .text((d) => d.data.data.name); }, function (update) { update .transition() .duration(1000) .attr( 'transform', (d) => ` rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0) rotate(${d.x >= Math.PI ? 180 : 0}) ` ); return update; }, function (exit) { return exit.remove(); } ); // Add circles svg .selectAll('.round') .data(root.descendants(), function (d) { return 'circle' + d.data.id; }) .join( function (enter) { return enter .append('circle') .attr('class', 'round') .attr( 'transform', (d) => ` rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0) ` ) .attr('r', 5) .on('click', click); }, function (update) { update .transition() .duration(1000) .attr( 'transform', (d) => ` rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0) ` ); return update; }, function (exit) { return exit.remove(); } ); } // Switch tree on click so centre is now selected node function click(event, d) { currentTreeData = findNode(treeData, d.data.id); chart(currentTreeData); } // HELPER FUNCTIONS // ---------------- // Function to Strafify flat CSV data into a tree function convertToHierarchy(data) { var stratify = d3 .stratify() .parentId(function (d) { return d.parentID; }) .id(function (d) { return d.ID; }); let treeData = stratify(data); return treeData; } // Function to Create d3 cluster with coordinates etc from stratified data function clusterise(treeData) { tree = d3.cluster().size([2 * Math.PI, radius - 100]); let root = tree( d3.hierarchy(treeData).sort((a, b) => d3.ascending(a.name, b.name)) ); return root; } function findNode(root, id) { //console.log(root); let selected = root.find((obj) => obj.id === id); selected.parent = null; //console.log(selected); return selected; } width = 800; height = 600; radius = width / 2; let svg = d3 .select('#vis') .append('svg') .attr('width', width) .attr('height', height) .append('g') .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); treeData = convertToHierarchy(flatData); currentTreeData = treeData; chart(currentTreeData); </script> </body> </html>