I would like to draw new treemap every time the data (data is generated based on the user interaction such as button clicks) is changed. At the moment every time the data is changed a new treemap is created but it is drawn on top of the previous treemap - meaning I can see both treemaps on the screen. Somehow the previous instance is not removed.
I have looked at this and this, and applied remove() function to remove the previous instance via d3. However, it does not work. I may be incorrectly used the remove function in the life cycle.
d3.select("treemap").remove(); is used in the second useEffect method.
export function Treemap({ width, height, data }) {
const ref = useRef();
useEffect(() => {
const svg = d3
.select(ref.current)
.attr("id", "treemap")
.attr("width", width)
.attr("height", height);
}, []);
useEffect(() => {
//THIS IS WHERE I USE REMOVE FUNCTION;
d3.select("treemap").remove();
draw();
}, [data]);
const draw = () => {
const svg = d3.select(ref.current);
// Give the data to this cluster layout:
var root = d3.hierarchy(data).sum(function (d) {
return d.value;
});
// initialize treemap
d3
.treemap()
.size([width, height])
.paddingTop(28)
.paddingLeft(0)
.paddingRight(0)
.paddingBottom(7)
.paddingInner(3)(root);
const color = d3
.scaleOrdinal()
.domain(["Diary", "Sweetner", "Fruit"])
.range(["#8FD175", "#402D54", "#E67E22"]);
const opacity = d3.scaleLinear().domain([10, 30]).range([0.5, 1]);
// Select the nodes
var nodes = svg.selectAll("rect").data(root.leaves());
// draw rectangles
nodes
.enter()
.append("rect")
.attr("x", function (d) {
return d.x0;
})
.attr("y", function (d) {
return d.y0;
})
.attr("width", function (d) {
return d.x1 - d.x0;
})
.attr("height", function (d) {
return d.y1 - d.y0;
})
.style("stroke", "black")
.style("fill", function (d) {
return color(d.parent.data.name);
})
.style("opacity", function (d) {
return opacity(d.data.value);
});
nodes.exit().remove();
// select node titles
var nodeText = svg.selectAll("text").data(root.leaves());
// add the text
nodeText
.enter()
.append("text")
.attr("x", function (d) {
return d.x0 + 5;
}) // +10 to adjust position (more right)
.attr("y", function (d) {
return d.y0 + 20;
}) // +20 to adjust position (lower)
.text(function (d) {
return d.data.name;
})
.attr("font-size", "19px")
.attr("fill", "white");
// select node titles
var nodeVals = svg.selectAll("vals").data(root.leaves());
// add the values
nodeVals
.enter()
.append("text")
.attr("x", function (d) {
return d.x0 + 5;
}) // +10 to adjust position (more right)
.attr("y", function (d) {
return d.y0 + 35;
}) // +20 to adjust position (lower)
.text(function (d) {
return d.data.value;
})
.attr("font-size", "11px")
.attr("fill", "white");
// add the parent node titles
svg
.selectAll("titles")
.data(
root.descendants().filter(function (d) {
return d.depth == 1;
})
)
.enter()
.append("text")
.attr("x", function (d) {
return d.x0;
})
.attr("y", function (d) {
return d.y0 + 21;
})
.text(function (d) {
return d.data.name;
})
.attr("font-size", "19px")
.attr("fill", function (d) {
return color(d.data.name);
});
};
return (
<div className="chart">
<svg ref={ref}></svg>
</div>
);
}
Normally, with d3, you never remove the already drawn nodes. It's much cheaper (computationally) and easier to just repurpose them!
If you really want to remove all nodes, d3.select("treemap") doesn't do anything, because "treemap" is not a valid selector. Try #treemap instead.
Or, if you want to repurpose the already drawn bits, consider the following:
function Treemap({
width,
height
}) {
const ref = React.useRef();
const [data, setData] = React.useState(`
{
"name": "Fruit",
"children": [
{ "name": "Apples", "value": 1 },
{ "name": "Oranges", "value": 1 },
{ "name": "Bananas", "value": 1 }
]
}
`);
React.useEffect(() => {
const svg = d3
.select(ref.current)
.attr("id", "treemap")
.attr("width", width)
.attr("height", height);
}, []);
React.useEffect(() => {
draw();
}, [data]);
const draw = () => {
const svg = d3.select(ref.current);
let parsedData
try {
parsedData = JSON.parse(data);
} catch (e) {
console.log(e);
return;
}
// Give the data to this cluster layout:
var root = d3.hierarchy(parsedData).sum(function(d) {
return d.value;
});
// initialize treemap
d3
.treemap()
.size([width, height])
.paddingTop(28)
.paddingLeft(0)
.paddingRight(0)
.paddingBottom(7)
.paddingInner(3)(root);
const color = d3
.scaleOrdinal()
.domain(["Diary", "Sweetner", "Fruit"])
.range(["#8FD175", "#402D54", "#E67E22"]);
const opacity = d3.scaleLinear().domain([10, 30]).range([0.5, 1]);
// Select the nodes
var nodes = svg.selectAll("rect").data(root.leaves());
// draw rectangles
var newNodes = nodes
.enter()
.append("rect")
.style("stroke", "black");
nodes.merge(newNodes)
.attr("x", function(d) {
return d.x0;
})
.attr("y", function(d) {
return d.y0;
})
.attr("width", function(d) {
return d.x1 - d.x0;
})
.attr("height", function(d) {
return d.y1 - d.y0;
})
.style("fill", function(d) {
return color(d.parent.data.name);
})
.style("opacity", function(d) {
return opacity(d.data.value);
});
nodes.exit().remove();
// select node titles
var nodeVals = svg.selectAll(".val").data(root.leaves());
nodeVals.exit().remove();
// add the values
var newNodeVals = nodeVals
.enter()
.append("text")
.classed("val", true)
.attr("font-size", "11px")
.attr("fill", "white");
nodeVals.merge(newNodeVals)
.attr("x", function(d) {
return d.x0 + 5;
}) // +10 to adjust position (more right)
.attr("y", function(d) {
return d.y0 + 35;
}) // +20 to adjust position (lower)
.text(function(d) {
return d.data.value;
});
// add the parent node titles
var titles = svg
.selectAll(".title")
.data(
root.descendants().filter(function(d) {
return d.depth == 1;
})
);
titles.exit().remove();
var newTitles = titles
.enter()
.append("text")
.classed("title", true)
.attr("font-size", "19px");
titles.merge(newTitles)
.attr("x", function(d) {
return d.x0 + 5;
})
.attr("y", function(d) {
return d.y0 + 21;
})
.text(function(d) {
return d.data.name;
})
.attr("fill", function(d) {
return color(d.data.name);
});
};
return ( <
div className = "chart" >
<
textarea onChange = {
(el) => setData(el.target.value)
}
value = {
data
}
rows = "20"
cols = "50" / >
<
svg ref = {
ref
} > < /svg> <
/div>
);
}
ReactDOM.render( <
Treemap width = "600"
height = "300" / > ,
document.getElementById('root')
)
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js"></script>
<div id="root"></div>
In any case, all your selectors were missing the .-class prefix. I'd recommend to read up on selectors, and on the enter(), exit(), merge() lifecycle.
Related
I'm trying to create a linechart in React using D3.js . The linechart should have Y on the y-axis, which is a date column and on the x-axis X1. This is a copy of my data in the csv:
and also posted the sample on kaggle (not sure if this is accessible)
link to kaggle
api: kaggle datasets download -d squishy1/dataui
I got it working with a csv via a url , but now that I try to do with a local csv, I can't read in my data the right way. I'm new to Javascript and React, coming from a Python background, it is not really intuitive yet.
In my log I do see that my data gets loaded, but when the visualization want to use it I get the following error:
Error: <path> attribute d: Expected number, "MNaN,205.40540540…".
The code that I have so far is:
import * as d3 from 'd3';
import {useEffect, useState} from 'react';
import csvFile from '../data/Data_UI.csv';
const SimpleLinechart = (props) => {
const [data, setData] = useState([]);
const {width, height } = props;
useEffect(()=>{
if (data.length > 0) {
drawChart();
console.log(data)
} else {
getCSVData();
}
},[data]);
const getCSVData = async () => {
const tempData = [];
await d3.csv(
csvFile,
function (d) {
tempData.push({
date: d3.timeParse("%m/%d/%Y")(d.Y),
value: Number(d.X1),
});
}
);
setData(tempData);
console.log(data.date)
};
const drawChart = () => {
const test = data.map(function(d) { return d.value });
console.log(test)
// create the chart area
const svg = d3.select('.svg-canvas')
svg.selectAll("*").remove();
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.csv(data, function(d) { return d.date; }))
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return + d.value; })])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// set line coordinates
const line = d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
// Add the line
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", line)
}
return (
<div>
<svg className="svg-canvas" width="1000px" height="600px" />
</div>
)
}
export default SimpleLinechart;
I'm trying to add a tooltip each time when hovering circles in my line chart.
As you can see in the picture below tooltip is exist in dom but I can't display it. Here I recreated my problem in sandbox:
https://codesandbox.io/s/serene-browser-t38ud6?file=/src/App.js
const tooltip = d3
.select(d3Chart.current)
.append("div")
.attr("class", "tooltip")
.style("opacity", "0")
.style("background-color", "red")
.html(`<p>test</p>`);
const mouseover = function (d) {
console.log("tooltip");
tooltip.transition().duration(200).style("opacity", 0.9);
};
svg
.append('g')
.selectAll('dot')
.data(data)
.enter()
.append('circle')
.attr('cx', function (d) {
return x(d.date);
})
.attr('cy', function (d) {
return y(d.balance);
})
.attr('r', 5)
.attr('fill', '#69b3a2')
.on('mouseover', mouseover);
I followed this article, and you just need to move the div outside the svg
create a ref to select the tooltip div
const tooltipRef = useRef();
add it to the HTML
...
</button>
<div className="tooltip" ref={tooltipRef} />
<svg id="chart" ref={d3Chart}></svg>
...
And change its style on the mouse events
const mouseover = function (event, d) {
console.log(d);
const tooltipDiv = tooltipRef.current;
if (tooltipDiv) {
d3.select(tooltipDiv).transition().duration(200).style("opacity", 0.9);
d3.select(tooltipDiv)
.html(d.balance)
// TODO: some logic when the tooltip could go out from container
.style("left", event.pageX + "px")
.style("top", event.pageY - 28 + "px");
}
};
const mouseout = () => {
const tooltipDiv = tooltipRef.current;
if (tooltipDiv) {
d3.select(tooltipDiv).transition().duration(500).style("opacity", 0);
}
};
...
.attr("fill", "#69b3a2")
.on("mouseover", mouseover)
.on("mouseout", mouseout);
https://codesandbox.io/s/youthful-cloud-tsu6sj?file=/src/App.js
Notice there are some styles for the tooltip in styles.css
I want to generate a static network graph, where the nodes only update the radio atributte and the links changes via props.
const NetworkGraph = props => {
const areaChart = useRef()
const dimensions = {width:400, height:200}
useEffect(() => {
const svg = d3.select(areaChart.current)
.attr('width', dimensions.width)
.attr('height', dimensions.height)
.style('background-color','white')
const nodo = svg
.selectAll("circle")
.data(props.data)
.enter()
.append("circle")
.attr("r", d => d.valor /* Math.floor((Math.random() * 40) + 1) */ )
.style("fill", "#69b3a2")
const link = svg
.selectAll("line")
.data(props.links)
.enter()
.append("line")
.style("stroke", "#aaa")
var simulation = d3.forceSimulation(data.nodes) // Force algorithm is applied to data.nodes
.force("link", d3.forceLink() // This force provides links between nodes
.id(function(d) { return d.id; }) // This provide the id of a node
.links(data.links) // and this the list of links
)
.force("charge", d3.forceManyBody().strength(-500)) // This adds repulsion between nodes. Play with the -400 for the repulsion strength
.force("center", d3.forceCenter(dimensions.width / 2, dimensions.height / 2)) // This force attracts nodes to the center of the svg area
.on("end", ticked);
//.on("tick", () => this.tick());
function ticked() {
link
.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; });
nodo
.attr("cx", function (d) { return d.x+6; })
.attr("cy", function(d) { return d.y-6; });
}
svg.selectAll("*").exit();
}, [props.data]);
return <svg ref={areaChart}> </svg>;
};
export default NetworkGraph;
I tried to use force, but i think i dont have to use it because i have to generate the new incoming graph every 1 second. Here add a image that i want to generate.
I am currently learning reactjs and d3 and working on how to visualize the dfs algorithm using network graph. After some iterations of updating the states, the browser hangs and page becomes unresponsive.
I think that I may not be clearing the svg before updating but not getting exactly how to do it.
Below is the renderer code which call the ForceGraph function
import { useEffect, useState } from "react";
import { ForceGraph } from "../forceGraph/forceGraph";
import { Container, Row, Button } from "react-bootstrap";
import "./renderer.css";
import data_dummy from "../../data/dummyGraph.json";
import OptionsButton from "../../components/buttons/optionsButton";
import DFS from "../../algorithms/graphTraversal/dfs";
function GraphOptionTab({
handleStartVisualization,
handleGenerateRandomGraph,
}) {
return (
<Container className="graph_option_container">
<Row className="graph_option_container_row">
<OptionsButton onClick={handleGenerateRandomGraph}>
Random graph
</OptionsButton>
<OptionsButton onClick={handleStartVisualization}>
Start visualization
</OptionsButton>
</Row>
</Container>
);
}
function generateRandomGraph() {
var numberOfNodes = Math.floor(Math.random() * 30);
var nodes = [];
var links = [];
var currentNode = Math.floor(Math.random() * numberOfNodes);
for (var i = 0; i < numberOfNodes; i++) {
var node = {};
node.id = i;
nodes.push({ ...node });
var links_for_each_node = Math.floor(Math.random() * 5);
for (var j = 0; j < links_for_each_node; j++) {
var link = {};
var source = i;
var target = i;
while (source === target) {
target = Math.floor(Math.random() * numberOfNodes);
}
link.source = source;
link.target = target;
link.weight = Math.floor(Math.random() * numberOfNodes);
links.push({ ...link });
}
}
var data = {};
data.nodes = [...nodes];
data.links = [...links];
return data;
}
export default function Renderer() {
const [renderer, setRenderer] = useState("graph");
const [data, setData] = useState(generateRandomGraph());
const [c, setC] = useState(0);
const [isRunning, setIsRunning] = useState(false);
function runAnimation() {
if (data === null) return;
var seq = DFS(data);
console.log(seq);
seq.forEach((a, i) => {
setTimeout(() => {
setData({ ...a });
}, i * 1000 * 2);
});
}
return renderer === "graph" ? (
<Container className="graph_container">
<Row className="graph_container_row1">
<GraphOptionTab
handleGenerateRandomGraph={() => {
setData({ ...generateRandomGraph() });
}}
handleStartVisualization={() => {
if (isRunning) {
runAnimation();
}
setIsRunning(!isRunning);
}}
></GraphOptionTab>
</Row>
<Row className="graph_container_row2">
<ForceGraph data={data}></ForceGraph>
</Row>
</Container>
) : null;
}
ForceGraph
import React from "react";
import { runForceGraph } from "./forceGraphGenerator";
import styles from "./forceGraph.module.css";
export function ForceGraph({ data }) {
const containerRef = React.useRef(null);
React.useEffect(() => {
if (data != null) {
if (containerRef.current) {
const { svg, simulation } = runForceGraph(
containerRef.current,
data.links,
data.nodes,
data.vis,
data.currentNode
);
return function cleanup(destroyFn) {
console.log("Cleanup called");
console.log(simulation);
console.log(svg);
simulation.stop();
svg.selectAll("*").remove();
svg.remove();
};
}
}
});
return <div ref={containerRef} className={styles.container} />;
}
ForceGraphGenerator
import * as d3 from "d3";
import "#fortawesome/fontawesome-free/css/all.min.css";
import styles from "./forceGraph.module.css";
export function runForceGraph(
container,
linksData,
nodesData,
vis,
currentNode
) {
const links = linksData.map((d) => Object.assign({}, d));
const nodes = nodesData.map((d) => Object.assign({}, d));
console.log(`hello current node = ${currentNode}`);
const containerRect = container.getBoundingClientRect();
const height = containerRect.height;
const width = containerRect.width;
const color = () => {
return "#29FF29";
};
const icon = (d) => {
return d.gender === "male" ? "\uf222" : "\uf221";
};
const getClass = (d) => {
return d.gender === "male" ? styles.male : styles.female;
};
const drag = (simulation) => {
const dragstarted = (event, d) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
};
const dragged = (event, d) => {
d.fx = event.x;
d.fy = event.y;
};
const dragended = (event, d) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
};
return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
};
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d) => {
return d.id;
})
.distance(200)
.strength(1)
)
.force("charge", d3.forceManyBody().strength(-1000))
.force("x", d3.forceX())
.force("y", d3.forceY());
simulation.tick(300);
const svg = d3
.select(container)
.append("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);
const link = svg
.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 1)
.attr("stroke-width", 2)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", (d) => Math.sqrt(d.value));
const node = svg
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 24)
.attr("fill", (d) => {
if (currentNode === d.id) return "#ff3b76";
if (vis && vis[d.id]) return "#fff";
return color();
})
.call(drag(simulation));
const edge_node = svg
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.selectAll("circle")
.data(links)
.join("circle")
.attr("r", 8)
.attr("fill", "#fff")
.call(drag(simulation));
const label = svg
.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodes)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("class", styles.node_text)
.attr("dominant-baseline", "central")
.text((d) => {
return d.id;
})
.call(drag(simulation));
const edge_label = svg
.append("g")
.attr("class", "labels")
.selectAll("text")
.data(links)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("class", styles.edge_label)
.attr("dominant-baseline", "central")
.text((d) => {
return d.weight;
})
.call(drag(simulation));
simulation.on("tick", () => {
//update link positions
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
// update node positions
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
// update label positions
label
.attr("x", (d) => {
return d.x;
})
.attr("y", (d) => {
return d.y;
});
edge_node
.attr("cx", (d) => Math.floor((d.source.x + d.target.x) / 2))
.attr("cy", (d) => Math.floor((d.source.y + d.target.y) / 2));
edge_label
.attr("x", (d) => {
return Math.floor((d.source.x + d.target.x) / 2);
})
.attr("y", (d) => {
return Math.floor((d.source.y + d.target.y) / 2);
});
});
return {
simulation,
svg,
};
}
DFS
vis[v] = true;
var state = {};
state.vis = [...vis];
state.currentNode = v;
state.nodes = data.nodes;
state.links = data.links;
seq.push({ ...state });
adjList[v].forEach((i) => {
if (!vis[i]) {
dfsWrapper(data, adjList, vis, i, seq);
}
});
}
export default function DFS(data) {
var seq = [];
var numberOfNodes = data.nodes.length;
var vis = new Array(numberOfNodes).fill(false);
var adjList = new Array(numberOfNodes);
for (var i = 0; i < adjList.length; i++) {
adjList[i] = new Array();
}
console.log(data.links);
for (var i = 0; i < data.links.length; i++) {
// console.log(data.links[i]["source"]);
adjList[data.links[i]["source"]].push(data.links[i]["target"]);
// console.log(adjList[data.links[i]["source"]]);
}
console.log(adjList);
for (var i = 0; i < numberOfNodes; i++) {
if (!vis[i]) {
dfsWrapper(data, adjList, vis, data.nodes[i].id, seq);
}
}
var state = {};
state.vis = [...vis];
state.currentNode = numberOfNodes + 1;
state.nodes = data.nodes;
state.links = data.links;
seq.push({ ...state });
return seq;
}
Hope that my question is clear and it would be very kind if anyone suggest what is going wrong.
Try to split runForceGraph into 2 functions: createForceGraph and updateForceGraph
Call createForceGraph once when you mount your component:
export function createForceGraph(...) {
...
const svg = d3
.select(container)
.append("svg")
...
svg
.append("g")
.attr("class", "labels")
...
}
Call updateForceGraph each time the data is changed:
export function updateForceGraph(...) {
...
const edge_label = d3
.select('.labels')
.selectAll("text")
.data(links)
.enter()
...
I am trying to build a hierarchical tree visualization using React and D3.
My component receives the hierarchical data in a CSV format as props, which I pass through the stratify function of D3 to obtain a root node to my tree. The root node is set as state.
Currently, I am using a single useEffect(()=>{},[root]) to build the tree, which re-renders on any changes to root.
It is messy as all the action happens inside this useEffect(). I want to know how can I decouple the update() method and use it separately.
As I am a beginner is both React and D3, I welcome any other suggestion on how to handle the state , how to make it more declarative et al.
Here is the code:
useEffect(() => {
if (root) {
//Declare a tree layout
//nodeSize ensure each node has it's own space and does not overlap
const tree = d3
.tree()
.nodeSize([
attributes.nodeWidth,
attributes.nodeHeight + attributes.veritcalNodeGap,
]);
root.x0 = 0;
root.y0 = attributes.width / 2;
//Set children of nodes deeper than 2 to null;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.child.length !== 7) d.children = null;
});
// append the svg object to the body of the page
// and define zoom behaviours
const svg = d3
.select(d3Ref.current)
.call(
d3
.zoom()
.scaleExtent([0.05, 3])
.on("zoom", () => svg.attr("transform", d3.event.transform))
)
.on("dblclick.zoom", null)
.append("svg")
.attr("viewBox", [0, 0, attributes.width, attributes.height])
.append("g")
.attr("transform", (d) => `translate(${attributes.width / 2},120)`);
//Group all links together
const gLink = svg
.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 1)
.attr("stroke-width", 1.5);
// .attr("x", "200 ");
//Group all nodes together
const gNode = svg
.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");
const diagonal = linkVertical()
.x((d) => d.x)
.y((d) => d.y);
update();
function update() {
const nodes = root.descendants().reverse();
const links = root.links();
tree(root);
//Define group and join the data
const node = gNode.selectAll("g").data(nodes, (d) => d.id);
let nodeEnter = node
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d) => `translate(${root.x0},${root.y0})`)
.on("click", (d) => {
d.children = d.children ? null : d._children;
update();
});
let nodeGroup = nodeEnter.append("g").attr("class", "node-group");
nodeEnter
.append("circle")
.attr("r", 7)
.attr("cursor", (d) => (d._children ? "pointer" : "none"))
.attr("fill", (d) => (d._children ? "lightsteelblue" : "#999"))
.attr("stroke", (d) => (d._children ? "steelblue" : "#999"))
.attr("stroke-width", 2);
//add text
nodeEnter
.append("text")
.attr("dy", ".35em")
.attr("x", 25)
.text((d) => d.data.child);
//Transition nodes to their new positions
const nodeUpdate = node //SVG.data()
.merge(nodeEnter)
.transition()
.duration(attributes.duration)
.attr("transform", (d) => `translate(${d.x},${d.y})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
//Transition exiting nodes to the parent's new position
const nodeExit = node
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr("transform", (d) => `translate(${root.x},${root.y})`);
// // Update the links…
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("d", (d) => {
const o = { x: root.x0, y: root.y0 };
return diagonal({ source: o, target: o });
});
// //Transition links to their new position
link
.merge(linkEnter)
.transition()
.duration(attributes.duration)
.attr("d", diagonal);
// // Transition exiting nodes to the parent's new position.
link
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr("d", (d) => {
const o = { x: root.x, y: root.y };
return diagonal({ source: o, target: o });
});
root.eachBefore((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
}
}
}, [root]);
Here is one idea.
You can have the initial effect onMount once, then updates only when root change,
You can use two useEffect on your component.
// hook on onMount, you prepare root, tree etc
let tree;
useEffect(() => {
if (root) {
//Declare a tree layout
//nodeSize ensure each node has it's own space and does not overlap
tree = d3
.tree()
.nodeSize([
attributes.nodeWidth,
attributes.nodeHeight + attributes.veritcalNodeGap,
]);
root.x0 = 0;
root.y0 = attributes.width / 2;
//Set children of nodes deeper than 2 to null;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.child.length !== 7) d.children = null;
});
// append the svg object to the body of the page
// and define zoom behaviours
const svg = d3
.select(d3Ref.current)
.call(
d3
.zoom()
.scaleExtent([0.05, 3])
.on('zoom', () => svg.attr('transform', d3.event.transform))
)
.on('dblclick.zoom', null)
.append('svg')
.attr('viewBox', [0, 0, attributes.width, attributes.height])
.append('g')
.attr('transform', (d) => `translate(${attributes.width / 2},120)`);
//Group all links together
const gLink = svg
.append('g')
.attr('fill', 'none')
.attr('stroke', '#555')
.attr('stroke-opacity', 1)
.attr('stroke-width', 1.5);
// .attr("x", "200 ");
//Group all nodes together
const gNode = svg
.append('g')
.attr('cursor', 'pointer')
.attr('pointer-events', 'all');
const diagonal = linkVertical()
.x((d) => d.x)
.y((d) => d.y);
}
}, []);
// here is the update only when [root] changes
useEffect(() => {
const nodes = root.descendants().reverse();
const links = root.links();
tree(root);
//Define group and join the data
const node = gNode.selectAll('g').data(nodes, (d) => d.id);
let nodeEnter = node
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', (d) => `translate(${root.x0},${root.y0})`)
.on('click', (d) => {
d.children = d.children ? null : d._children;
update();
});
let nodeGroup = nodeEnter.append('g').attr('class', 'node-group');
nodeEnter
.append('circle')
.attr('r', 7)
.attr('cursor', (d) => (d._children ? 'pointer' : 'none'))
.attr('fill', (d) => (d._children ? 'lightsteelblue' : '#999'))
.attr('stroke', (d) => (d._children ? 'steelblue' : '#999'))
.attr('stroke-width', 2);
//add text
nodeEnter
.append('text')
.attr('dy', '.35em')
.attr('x', 25)
.text((d) => d.data.child);
//Transition nodes to their new positions
const nodeUpdate = node //SVG.data()
.merge(nodeEnter)
.transition()
.duration(attributes.duration)
.attr('transform', (d) => `translate(${d.x},${d.y})`)
.attr('fill-opacity', 1)
.attr('stroke-opacity', 1);
//Transition exiting nodes to the parent's new position
const nodeExit = node
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr('transform', (d) => `translate(${root.x},${root.y})`);
// // Update the links…
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('d', (d) => {
const o = { x: root.x0, y: root.y0 };
return diagonal({ source: o, target: o });
});
// //Transition links to their new position
link
.merge(linkEnter)
.transition()
.duration(attributes.duration)
.attr('d', diagonal);
// // Transition exiting nodes to the parent's new position.
link
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr('d', (d) => {
const o = { x: root.x, y: root.y };
return diagonal({ source: o, target: o });
});
root.eachBefore((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
}, [root]);