reactjs - D3 + ReactJS 网络图在不断更新图后无限期挂起浏览器
问题描述
我目前正在学习 reactjs 和 d3 并研究如何使用网络图可视化 dfs 算法。在更新状态的一些迭代之后,浏览器挂起并且页面变得无响应。我认为我可能不会在更新之前清除 svg,但不知道该怎么做。
下面是调用ForceGraph函数的渲染器代码
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;
}
力图
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} />;
}
力图生成器
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;
}
希望我的问题很清楚,如果有人提出出了什么问题,那将是非常友好的。
解决方案
尝试将runForceGraph拆分为 2 个函数:createForceGraph和updateForceGraph
安装组件时调用 createForceGraph一次:
export function createForceGraph(...) {
...
const svg = d3
.select(container)
.append("svg")
...
svg
.append("g")
.attr("class", "labels")
...
}
每次更改数据时调用updateForceGraph :
export function updateForceGraph(...) {
...
const edge_label = d3
.select('.labels')
.selectAll("text")
.data(links)
.enter()
...