首页 > 解决方案 > 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;
}

希望我的问题很清楚,如果有人提出出了什么问题,那将是非常友好的。

标签: reactjsd3.jsgraph

解决方案


尝试将runForceGraph拆分为 2 个函数:createForceGraphupdateForceGraph

安装组件时调用 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()
 ...
 

推荐阅读