reactjsd3.jsd3-force-directed

React + D3 + Force-Directed Tree + Adjustable Link Strength


I'm trying to make Force-Directed Tree with React and it works. But I cannot modify "link strength", if I pass it outside component through the props.

Honestly, I can change "strength", but I need to append d3 svg to my react ref div after that to see the changes. And whole graph will be redrawn.

I find example by Mike Bostock. He advice to modify the parameters of a force-directed graph with reheat the simulation using simulation.alpha and simulation.restart. But I cannot make it works with react. Nothing happens.

Here is my code:

export default function Hierarchy(props) {
  const {
    strength,
    lineColor,
    lineStroke,
    width,
    height,
    nodeSize,
    nodeColor,
  } = props;

  const root = d3.hierarchy(data);
  const links = root.links();
  const nodes = root.descendants();

  const svg = d3.create("svg");

  const link = svg
    .append("g")
    .selectAll("line")
    .data(links)
    .join("line");

  const node = svg
    .append("g")
    .selectAll("circle")
    .data(nodes)
    .join("circle");

  function applyStyle(selectionSVG) {
    selectionSVG
      .attr("width", width)
      .attr("height", height)
      .attr("viewBox", [-width / 2, -height / 2, width, height]);

    selectionSVG
      .selectAll("circle")
      .attr("r", nodeSize)
      .attr("fill", nodeColor)

    selectionSVG
      .selectAll("line")
      .attr("stroke", lineColor)
      .attr("stroke-width", lineStroke);
  }

  applyStyle(svg);

  const divRef = React.useRef(null);

  const linkForce = d3
    .forceLink(links)
    .id(d => d.id)
    .distance(0)
    .strength(strength);

  const simulation = d3
    .forceSimulation(nodes)
    .force("link", linkForce)
    .force("charge", d3.forceManyBody().strength(-500))
    .force("x", d3.forceX())
    .force("y", d3.forceY());

  simulation.on("tick", () => {
    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);

    node.attr("cx", d => d.x).attr("cy", d => d.y);
  });

  //ComponentDidMount
  useEffect(() => {
    //Append d3 svg to ref div
    var div = d3.select(divRef.current);
    if (div.node().firstChild) {
      div.node().removeChild(div.node().firstChild);
    }
    div.node().appendChild(svg.node());
  }, []);

  //ComponentDidUpdate
  useEffect(() => {
    simulation.force("link").strength(strength);
    simulation.alpha(1).restart();
  }, [strength]);

  //ComponentDidUpdate
  useEffect(() => {
    var div = d3.select(divRef.current);
    applyStyle(div.select("svg"));
  });

  //Render
  return <div id="hierarchyTree" ref={divRef} />;
}

Here is Sandbox.


Solution

  • I find solution, if anybody interesting.

    The fact is simulation was not saved when component was updated. So I create ref for it.

    const simulationRef = React.useRef(simulation)
    

    and replace it in useEffect section

    //ComponentDidUpdate
    useEffect(() => {
        simulationRef.current.force("link").strength(strength)
        simulationRef.current.alpha(1).restart()
        console.log(simulationRef.current)
    }, [strength])
    

    After that everything works fine.