我正在尝试使用React和D3构建分层的树形可视化。我的组件以CSV格式接收props的分层数据,我通过D3的stratify
函数来获取树的根节点。根节点设置为状态。
当前,我正在使用单个useEffect(()=>{},[root])
来构建树,该树将在对根的任何更改后重新呈现。
这很混乱,因为所有操作都发生在useEffect()
内部。我想知道如何解耦update()
方法并单独使用。
[由于我是React和D3的初学者,所以我欢迎就如何处理状态,如何使其更具声明性等提出任何其他建议
这里是代码:
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]);
这里是一个主意。您可以在onMount上产生一次初始效果,然后仅在root
更改时进行更新,您可以在组件上使用两个useEffect
。
// 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]);