I’m trying to update the text nodes of my D3 tree every time I press a HTML button, adding one every time, but I don’t know how to do it properly without having to create the tree again. Can anyone help me with it?
The following snippet exemplarizes the state that I’m now
const DX = 120; const DY = 90; const RECT_WIDTH = 40; const RECT_HEIGHT = 15; // Zoom constants const MIN_ZOOM = 0.15; const MAX_ZOOM = Infinity; // Radius for the border of the rectangle of nodes const RECTANGLE_RADIUS = 3; // Time that takes to complete a change of state const TRANSITION_DURATION = 700; let data = { "name": "Nodo 1", "children": [ { "name": "Nodo 2", "children": [ { "name": "Nodo 3" }, { "name": "Nodo 4" } ] } ] }; // Get size of the screen available let width = $(document).width(); let height = $(document).height(); // New tree layout with default settings. Set the size of the nodes to DX in the // horizontal axis and to DY in the vertical axis let tree = d3.tree().nodeSize([DX, DY]); // Visualize links in a tree diagram rooted at the top let diagonal = d3.linkVertical() .x(d => d.x + RECT_WIDTH / 2) .y(d => d.y + RECT_HEIGHT / 2); /* Function that creates the first tree with the default layout */ function createRoot(data) { // Data is already in JSON format. We only need to create the hierarchy // based on it. let root = d3.hierarchy(data); root.x0 = DX / 2; root.y0 = 0; root.descendants().forEach((d, i) => { d.id = i; // Auxiliar variable to hide and show nodes when user clicks d._children = d.children; // Only the root is displayed at first sight if (d.depth >= 0) d.children = null; }); return root; } /* Function that updates all the nodes and links in a tree according to the click event */ function update(source) { // Get all nodes and links that are being shown const nodes = root.descendants().reverse(); const links = root.links(); // Compute the new tree layout tree(root); const transition = svg.transition() .duration(TRANSITION_DURATION) .tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle")); /*=============================NODE SECTION============================== */ // Obtain all the nodes const node = gNode.selectAll("g") .data(nodes, d => d.id); // Enter any new nodes at the parent's previous position. const nodeEnter = node.enter().append("g") .attr("class", "node") .attr("transform", d => `translate(${source.x0},${source.y0})`) .on("click", function (event, d) { if (d.children) // Node expanded -> Collapse collapse(d); else // Node collapsed -> Expand d.children = d._children update(d); centerNode(d); }) nodeEnter.append("rect") // Two different classes, one for the links that can be expanded and // another for the expanded ones or leafs. .attr("class", d => d._children && !d.children ? "notExpanded" : "expanded") .attr("height", RECT_HEIGHT) .attr("width", RECT_WIDTH) .attr("fill", d => d.color) .attr("rx", RECTANGLE_RADIUS) .attr("ry", RECTANGLE_RADIUS); // Inside node text nodeEnter.append("text") .attr("class", "nodeText") // The position of the text is at the top .attr("x", RECT_WIDTH / 2) .attr("y", RECT_HEIGHT / 2) .text(d => d.data.name) .clone(true).lower(); // Transition nodes to their new position (update) var nodeUpdate = node.merge(nodeEnter).transition(transition) .attr("transform", d => `translate(${d.x},${d.y})`) // Smooth transition from 0 to 1 and back to 0 when expanding/collapsing .attr("fill-opacity", 1) .attr("stroke-opacity", 1); // Update class of rectangles nodeUpdate.select('rect') .attr("class", d => d._children && !d.children ? "notExpanded" : "expanded"); nodeUpdate.select('text') .text(d => numberPressed == 0 ? d.data.name : `${d.data.name} - ${numberPressed}`); // Transition exiting nodes to the parent's new position node.exit().transition(transition).remove() .attr("transform", d => `translate(${source.x},${source.y})`) .attr("fill-opacity", 0) .attr("stroke-opacity", 0); /*=============================LINK SECTION============================== */ const link = gLink.selectAll("path") .data(links, d => d.target.id); // Enter any new links at the parent's previous position const linkEnter = link.enter().append("path") .attr("class", "link") .attr("x", RECT_WIDTH / 2) .attr("y", RECT_HEIGHT / 2) // d attribute defines a path to be drawn .attr("d", d => { const o = { x: source.x0, y: source.y0 }; // Diagonal actually paints a curve line between the source and the // target return diagonal({ source: o, target: o }); }); // Transition links to their new position link.merge(linkEnter).transition(transition) // In this case the link will be changed in order to have the arrows in // the correct position .attr("d", d => diagonal({ source: d.source, target: { x: d.target.x, y: d.target.y } })); // Transition exiting nodes to the parent's new position link.exit().transition(transition).remove() .attr("d", d => { const o = { x: source.x, y: source.y }; return diagonal({ source: o, target: o }); }); // Stash the old positions for transition root.eachBefore(d => { d.x0 = d.x; d.y0 = d.y; }); } /* Function that centers a given node in the screen. It will be used so that the node that has been clicked doesn't go out of sight */ function centerNode(source) { let scale = d3.zoomTransform(d3.select("svg").node()).k; let x = -source.x0 * scale + width / 2 - RECT_WIDTH / 2 * scale; let y = -source.y0 * scale + height / 2 - RECT_HEIGHT / 2 * scale; // Define the transition const transition = svg.transition() .duration(TRANSITION_DURATION) .tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle")); // Move all the nodes based on the previous parameters svg.transition(transition) .call(zoomBehaviours.transform, d3.zoomIdentity.translate(x, y).scale(scale)); } /* Function that collapses all the subtree of a given node. */ function collapse(node) { if (node.children) { // Expanded node.children = null; node._children.forEach(collapse) } } const root = createRoot(data); let numberPressed = 0; /* Function that will create the variable menu and add it to the window */ function createVariableMenu() { d3.select("body").append("button") .attr("class", "addButton") .text("Add") .on("click", function() { numberPressed++; update(root); }); } createVariableMenu(); // SVG variable that will contain all the configuration for the images. // We need to append it to the body const svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height) .attr("xmlns", "http://www.w3.org/2000/svg"); // HTML tooltip const div = d3.select("body").append("div") .attr("class", "tooltip") .style("opacity", 0); // HTML tooltip for vars const divVars = d3.select("body").append("div") .attr("class", "tooltipVar") .style("opacity", 0); // SVG group that will contain two groups declared below const g = svg.append("g"); // Two groups: One of links (and link labels) and another of nodes const gLink = g.append("g"); const gNode = g.append("g"); // Zoom configuration const zoomBehaviours = d3.zoom() .scaleExtent([MIN_ZOOM, MAX_ZOOM]) .on('zoom', (event) => { g.attr('transform', event.transform); }); // Add the zoom so that svg knows that it is available svg.call(zoomBehaviours); // We need to update the root first to generate the first tree, and center it update(root); centerNode(root);
.node { cursor: pointer; fill: lightblue; } .node .notExpanded { stroke: black; stroke-width: 1.2; } .node .nodeText { fill: black; font: 10px sans-serif; text-anchor: middle; text-align: center; dominant-baseline: central; } .addButton { position: absolute; cursor: pointer; font-size: 20px; } .link { fill: none; stroke: black; stroke-width: 1.5; stroke-opacity: 0.5; } body { overflow: hidden; cursor: grab; margin: 2px; } body:active { cursor: grabbing; }
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <link rel="stylesheet" href="style.css"> </head> <body></body> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <script src="https://d3js.org/d3.v6.min.js"></script> <script src="./src/main.js"></script> </html>
As you can see, whenever there is a click event over the button, the text is shown below the node, and the previous text remains. I’m not sure of what’s happening, but it seems that the selection of the text is not working properly, because I’m only appending the text once, but it’s showing up twice. How should I change the code in order to have it working?
Any help is appreciated. Thanks 🙂
Advertisement
Answer
It seems that I was not selecting correctly the text. I changed it to select the class that I chose for the text inside the nodes (.nodeText
) and it’s currently working as expected. In the following snippet you can see the changes
const DX = 120; const DY = 90; const RECT_WIDTH = 60; const RECT_HEIGHT = 20; // Zoom constants const MIN_ZOOM = 0.15; const MAX_ZOOM = Infinity; // Radius for the border of the rectangle of nodes const RECTANGLE_RADIUS = 3; // Time that takes to complete a change of state const TRANSITION_DURATION = 700; let data = { "name": "Nodo 1", "children": [ { "name": "Nodo 2", "children": [ { "name": "Nodo 3" }, { "name": "Nodo 4" } ] } ] }; // Get size of the screen available let width = $(document).width(); let height = $(document).height(); // New tree layout with default settings. Set the size of the nodes to DX in the // horizontal axis and to DY in the vertical axis let tree = d3.tree().nodeSize([DX, DY]); // Visualize links in a tree diagram rooted at the top let diagonal = d3.linkVertical() .x(d => d.x + RECT_WIDTH / 2) .y(d => d.y + RECT_HEIGHT / 2); /* Function that creates the first tree with the default layout */ function createRoot(data) { // Data is already in JSON format. We only need to create the hierarchy // based on it. let root = d3.hierarchy(data); root.x0 = DX / 2; root.y0 = 0; root.descendants().forEach((d, i) => { d.id = i; // Auxiliar variable to hide and show nodes when user clicks d._children = d.children; // Only the root is displayed at first sight if (d.depth >= 0) d.children = null; }); return root; } /* Function that updates all the nodes and links in a tree according to the click event */ function update(source) { // Get all nodes and links that are being shown const nodes = root.descendants().reverse(); const links = root.links(); // Compute the new tree layout tree(root); const transition = svg.transition() .duration(TRANSITION_DURATION) .tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle")); /*=============================NODE SECTION============================== */ // Obtain all the nodes const node = gNode.selectAll("g") .data(nodes, d => d.id); // Enter any new nodes at the parent's previous position. const nodeEnter = node.enter().append("g") .attr("class", "node") .attr("transform", d => `translate(${source.x0},${source.y0})`) .on("click", function (event, d) { if (d.children) // Node expanded -> Collapse collapse(d); else // Node collapsed -> Expand d.children = d._children update(d); centerNode(d); }) nodeEnter.append("rect") // Two different classes, one for the links that can be expanded and // another for the expanded ones or leafs. .attr("class", d => d._children && !d.children ? "notExpanded" : "expanded") .attr("height", RECT_HEIGHT) .attr("width", RECT_WIDTH) .attr("fill", d => d.color) .attr("rx", RECTANGLE_RADIUS) .attr("ry", RECTANGLE_RADIUS); // Inside node text nodeEnter.append("text") .attr("class", "nodeText") // The position of the text is at the top .attr("x", RECT_WIDTH / 2) .attr("y", RECT_HEIGHT / 2) .text(d => d.data.name); // Transition nodes to their new position (update) var nodeUpdate = node.merge(nodeEnter).transition(transition) .attr("transform", d => `translate(${d.x},${d.y})`) // Smooth transition from 0 to 1 and back to 0 when expanding/collapsing .attr("fill-opacity", 1) .attr("stroke-opacity", 1); // Update class of rectangles nodeUpdate.select('rect') .attr("class", d => d._children && !d.children ? "notExpanded" : "expanded"); nodeUpdate.select('.nodeText') .text(d => numberPressed == 0 ? d.data.name : `${d.data.name} - ${numberPressed}`); // Transition exiting nodes to the parent's new position node.exit().transition(transition).remove() .attr("transform", d => `translate(${source.x},${source.y})`) .attr("fill-opacity", 0) .attr("stroke-opacity", 0); /*=============================LINK SECTION============================== */ const link = gLink.selectAll("path") .data(links, d => d.target.id); // Enter any new links at the parent's previous position const linkEnter = link.enter().append("path") .attr("class", "link") .attr("x", RECT_WIDTH / 2) .attr("y", RECT_HEIGHT / 2) // d attribute defines a path to be drawn .attr("d", d => { const o = { x: source.x0, y: source.y0 }; // Diagonal actually paints a curve line between the source and the // target return diagonal({ source: o, target: o }); }); // Transition links to their new position link.merge(linkEnter).transition(transition) // In this case the link will be changed in order to have the arrows in // the correct position .attr("d", d => diagonal({ source: d.source, target: { x: d.target.x, y: d.target.y } })); // Transition exiting nodes to the parent's new position link.exit().transition(transition).remove() .attr("d", d => { const o = { x: source.x, y: source.y }; return diagonal({ source: o, target: o }); }); // Stash the old positions for transition root.eachBefore(d => { d.x0 = d.x; d.y0 = d.y; }); } /* Function that centers a given node in the screen. It will be used so that the node that has been clicked doesn't go out of sight */ function centerNode(source) { let scale = d3.zoomTransform(d3.select("svg").node()).k; let x = -source.x0 * scale + width / 2 - RECT_WIDTH / 2 * scale; let y = -source.y0 * scale + height / 2 - RECT_HEIGHT / 2 * scale; // Define the transition const transition = svg.transition() .duration(TRANSITION_DURATION) .tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle")); // Move all the nodes based on the previous parameters svg.transition(transition) .call(zoomBehaviours.transform, d3.zoomIdentity.translate(x, y).scale(scale)); } /* Function that collapses all the subtree of a given node. */ function collapse(node) { if (node.children) { // Expanded node.children = null; node._children.forEach(collapse) } } const root = createRoot(data); let numberPressed = 0; /* Function that will create the variable menu and add it to the window */ function createVariableMenu() { d3.select("body").append("button") .attr("class", "addButton") .text("Add") .on("click", function() { numberPressed++; update(root); }); } createVariableMenu(); // SVG variable that will contain all the configuration for the images. // We need to append it to the body const svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height) .attr("xmlns", "http://www.w3.org/2000/svg"); // HTML tooltip const div = d3.select("body").append("div") .attr("class", "tooltip") .style("opacity", 0); // HTML tooltip for vars const divVars = d3.select("body").append("div") .attr("class", "tooltipVar") .style("opacity", 0); // SVG group that will contain two groups declared below const g = svg.append("g"); // Two groups: One of links (and link labels) and another of nodes const gLink = g.append("g"); const gNode = g.append("g"); // Zoom configuration const zoomBehaviours = d3.zoom() .scaleExtent([MIN_ZOOM, MAX_ZOOM]) .on('zoom', (event) => { g.attr('transform', event.transform); }); // Add the zoom so that svg knows that it is available svg.call(zoomBehaviours); // We need to update the root first to generate the first tree, and center it update(root); centerNode(root);
.node { cursor: pointer; fill: lightblue; } .node .notExpanded { stroke: black; stroke-width: 1.2; } .node .nodeText { fill: black; font: 10px sans-serif; text-anchor: middle; text-align: center; dominant-baseline: central; } .addButton { position: absolute; cursor: pointer; font-size: 20px; } .link { fill: none; stroke: black; stroke-width: 1.5; stroke-opacity: 0.5; } body { overflow: hidden; cursor: grab; margin: 2px; } body:active { cursor: grabbing; }
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <link rel="stylesheet" href="style.css"> </head> <body></body> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <script src="https://d3js.org/d3.v6.min.js"></script> <script src="./src/main.js"></script> </html>