首页 > 解决方案 > 数据更改时如何重新应用 d3.forceCollide

问题描述

我从 d3 中的 forceCollide 方法中看到了一个相当奇怪的效果。我第一次绘制图表时效果很好,但每次数据更改时,节点会越来越靠近并最终重叠。

这是一个大型的反应应用程序,所以我不能在这里发布所有代码。这是数据更改时运行的代码:

    var simulation = forceSimulation;
    if (!simulation) {
      simulation = d3ForceSimulation();
      setForceSimulation(simulation);
    }
    simulation
      .nodes(nodes)
      .force('charge', d3ForceManyBody())
      .force('center', d3ForceCenter(width / 2, height / 2))
      .force(
        'link',
        d3ForceLink(links).strength((link) => link.weight),
      )
      .force(
        'collision',
        d3ForceCollide((n) => nodeRadius(n) * (edgeLabels ? 2 : 1.4) * spacing),
      )
      .on('tick', tick)
      .restart();

我尝试了各种方法——比如传递一个常数值forceCollide而不是计算节点的半径,但我仍然看到了理智的效果。我还尝试.force('collision', null)在添加新力量之前添加,以防它没有正确替换旧力量,但这也没有任何区别。

这是一个有效的事情吗?我forceCollide没有改变,我是否应该仅在首次创建模拟时将其附加到模拟中?

作为参考,这是整个反应组件的源代码。

import React, { useEffect, useState } from 'react';
import { select as d3Select } from 'd3';
import { forceSimulation as d3ForceSimulation } from 'd3-force';
import { forceManyBody as d3ForceManyBody } from 'd3-force';
import { forceCenter as d3ForceCenter } from 'd3-force';
import { forceLink as d3ForceLink } from 'd3-force';
import { forceCollide as d3ForceCollide } from 'd3-force';
import usePrevious from '~util/usePrevious';

export const ForceNetworkDiagram = ({ view, graphData, width, height, zoom, offsetX, offsetY }) => {
  const leftMargin = view.jsonLayout.diagram?.leftMargin || 30;
  const topMargin = view.jsonLayout.diagram?.topMargin || 0;
  const spacing = view.jsonLayout.diagram?.spacing || 1;
  const nodeLabels = !!view.jsonLayout.diagram?.nodeLabels;
  const edgeLabels = !!view.jsonLayout.diagram?.edgeLabels;

  const [forceSimulation, setForceSimulation] = useState(null);
  const [nodes, setNodes] = useState(null);
  const [links, setLinks] = useState(null);
  const prevNodeLabels = usePrevious(nodeLabels);
  const prevEdgeLabels = usePrevious(edgeLabels);

  const nodeRadius = (node) => node.weight * 30 + 10;

  // This effect transforms graph data into the force simulation data structures
  useEffect(() => {
    setNodes(graphData.nodes);

    const nodesById = {};
    graphData.nodes.forEach((node) => {
      nodesById[node.id] = node;
    });

    setLinks(
      graphData.edges.map((edge) => ({
        source: nodesById[edge.from],
        target: nodesById[edge.to],
        label: edge.label,
        weight: edge.weight,
      })),
    );
  }, [graphData]);

  // This effect starts a force simulation when the data changes, and updates the
  // drawing as the simulation plays out
  useEffect(() => {
    function link(d) {
      return (
        'M' +
        d.source.x +
        ',' +
        d.source.y +
        'C' +
        (d.source.x + d.target.x) / 2 +
        ',' +
        d.source.y +
        ' ' +
        (d.source.x + d.target.x) / 2 +
        ',' +
        d.target.y +
        ' ' +
        d.target.x +
        ',' +
        d.target.y
      );
    }

    function tick() {
      d3Select('.nodes')
        .selectAll('g.node')
        .data(nodes)
        .attr('transform', function(d) {
          return 'translate(' + d.x + ',' + d.y + ')';
        });

      const linkGroups = d3Select('.links')
        .selectAll('g.link')
        .data(links);
      linkGroups.select('path').attr('d', link);
      if (edgeLabels) {
        linkGroups
          .select('text')
          .attr('x', (link) => (link.source.x + link.target.x) / 2)
          .attr('y', (link) => (link.source.y + link.target.y) / 2);
      }
    }

    if (!nodes || !links) return;

    var simulation = forceSimulation;
    if (!simulation) {
      simulation = d3ForceSimulation();
      setForceSimulation(simulation);
    }
    simulation
      .nodes(nodes)
      .force('charge', d3ForceManyBody())
      .force('center', d3ForceCenter(width / 2, height / 2))
      .force(
        'link',
        d3ForceLink(links).strength((link) => link.weight),
      )
      .force(
        'collision',
        d3ForceCollide((n) => nodeRadius(n) * (edgeLabels ? 2 : 1.4) * spacing),
      )
      .on('tick', tick)
      .restart();

    return () => {
      simulation.stop();
    };
  }, [forceSimulation, nodes, links, width, height, nodeLabels, edgeLabels, spacing]);

  // This effect draws cicles for each node
  useEffect(() => {
    function nodeClass(node) {
      return node.tags ? 'node ' + node.tags.join(' ') : 'node';
    }

    if (!nodes) return;

    // Bind data to nodes
    const existingNodes = d3Select('.nodes')
      .selectAll('g.node')
      .data(nodes);
    const newNodes = existingNodes.enter().append('g');
    existingNodes.exit().remove();

    // Set the classes on the node container
    newNodes.attr('class', nodeClass);
    existingNodes.attr('class', nodeClass);

    // Draw or update a circle to represent the node
    newNodes.append('circle').attr('r', nodeRadius);
    existingNodes.select('circle').attr('r', nodeRadius);

    // Draw or update text under the circle
    if (nodeLabels) {
      newNodes
        .append('text')
        .text((node) => node.name)
        .attr('x', (node) => -0.8 * nodeRadius(node))
        .attr('y', (node) => 4);
      if (prevNodeLabels) {
        existingNodes
          .select('text')
          .text((node) => node.name)
          .attr('x', (node) => -0.8 * nodeRadius(node))
          .attr('y', (node) => 4);
      } else {
        existingNodes
          .append('text')
          .text((node) => node.name)
          .attr('x', (node) => -0.8 * nodeRadius(node))
          .attr('y', (node) => 4);
      }
    } else {
      existingNodes.select('text').remove();
    }
  }, [nodes, nodeLabels, prevNodeLabels]);

  // This effect draws lines between the nodes
  useEffect(() => {
    function linkClass(link) {
      return link ? 'link ' + link.label : 'link root';
    }

    function linkStyle(link) {
      return link ? 'stroke-width:' + link.weight * 5 : 'stroke-width:1';
    }

    if (!links) return;

    // Bind data to links between nodes
    const existingLinks = d3Select('.links')
      .selectAll('g.link')
      .data(links);
    const newLinks = existingLinks.enter().append('g');
    existingLinks.exit().remove();

    // Set the classes for the container
    newLinks.attr('class', linkClass);
    existingLinks.attr('class', linkClass);

    // Draw or update the line between nodes
    newLinks.append('path').attr('style', linkStyle);
    existingLinks.select('path').attr('style', linkStyle);

    // Label the line with the link label
    if (edgeLabels) {
      newLinks.append('text').text((link) => link.label);
      if (prevEdgeLabels) {
        existingLinks.select('text').text((link) => link.label);
      } else {
        existingLinks.append('text').text((link) => link.label);
      }
    } else {
      existingLinks.select('text').remove();
    }
  }, [links, edgeLabels, prevEdgeLabels]);

  return (
    <>
      <style dangerouslySetInnerHTML={{ __html: view.jsonLayout.styles }} />
      <svg width={(width * zoom) / 100} height={(height * zoom) / 100}>
        <g className="wrapper" transform={'translate(' + leftMargin + ',' + topMargin + ')'}>
          <g className="links"></g>
          <g className="nodes"></g>
        </g>
      </svg>
    </>
  );
};

export default ForceNetworkDiagram;

标签: reactjsd3.js

解决方案


事实证明,restart()模拟的方法并没有做到罐头上所说的那样。如果你真的想重新开始你需要调用的模拟restart()alpha(1).

这对我来说似乎很违反直觉,但至少我的绘画现在按预期工作。更新后的代码是:

    var simulation = forceSimulation;
    if (!simulation) {
      simulation = d3ForceSimulation();
      setForceSimulation(simulation);
    }
    simulation
      .nodes(nodes)
      .force('charge', d3ForceManyBody())
      .force('center', d3ForceCenter(width / 2, height / 2))
      .force(
        'link',
        d3ForceLink(links).strength((link) => link.weight),
      )
      .force(
        'collision',
        d3ForceCollide((n) => nodeRadius(n) * (edgeLabels ? 2 : 1.4) * spacing),
      )
      .on('tick', tick)
      .alpha(1)
      .restart();


推荐阅读