Home > Software engineering >  How to automatically move nodes and links at the same time in d3.js
How to automatically move nodes and links at the same time in d3.js

Time:12-13

I'm trying the force-directed example in d3.js(v7). In this sample, when I drag a node, the links and other nodes move in tandem. I want all nodes to move randomly at all times, and I want other links and nodes to move in tandem with them, just as if I were dragging them. The code is below. The json file is the same as the sample. When I run this code, the nodes move, but the links don't follow the movement and remain stationary.

function ForceGraph({
    nodes, // an iterable of node objects (typically [{id}, …])
    links, // an iterable of link objects (typically [{source, target}, …])
  }, {
    nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
    nodeGroup, // given d in nodes, returns an (ordinal) value for color
    nodeGroups, // an array of ordinal values representing the node groups
    nodeStrength,
    linkSource = ({source}) => source, // given d in links, returns a node identifier string
    linkTarget = ({target}) => target, // given d in links, returns a node identifier string
    linkStrokeWidth = 10, // given d in links, returns a stroke width in pixels
    linkStrength = 0.55,
    colors = d3.schemeTableau10, // an array of color strings, for the node groups
    width = 640, // outer width, in pixels
    height = 400, // outer height, in pixels
    invalidation // when this promise resolves, stop the simulation
  } = {}) {
    // Compute values.
    const N = d3.map(nodes, nodeId).map(intern);
    const LS = d3.map(links, linkSource).map(intern);
    const LT = d3.map(links, linkTarget).map(intern);
  
    if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
    const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
    const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
    const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
  
    // Replace the input nodes and links with mutable objects for the simulation.
    nodes = d3.map(nodes, (_, i) => ({id: N[i], type: NODETYPES[i], tag: parsed_NODETAGS[i], texts: X[i]}));
    links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));
  
    // Compute default domains.
    if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
  
    // Construct the scales.
    const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
  
    // Construct the forces.
    const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
    if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
    if (linkStrength !== undefined) forceLink.strength(linkStrength);
  
    const zoom = d3.zoom()
        .scaleExtent([1, 40])
        .on("zoom", zoomed);
  
    const svg = d3.create("svg")
        .attr("viewBox", [-width / 2, -height / 2.5, width, height])
        .on("click", reset)
        .attr("style", "max-width: 100%; height: auto; height: intrinsic;");
    
    svg.call(zoom);
    const g = svg.append("g");
  
    const link = g.append("g")
      .selectAll("line")
      .data(links)
      .join("line");
  
    const simulation = d3.forceSimulation(nodes)
      .force("link", forceLink)
      .force("charge", d3.forceManyBody())
      .force("center",  d3.forceCenter())
      .on("tick", ticked);
    
    const node = g.append("g")
        .attr("class", "nodes")
        .style("opacity", 1.0)
      .selectAll("circle")
      .data(nodes)
      .join("circle")
        .attr("r", 5)
        .call(drag(simulation));
  
    if (W) link.attr("stroke-width", ({index: i}) => W[i]);
    if (G) node.attr("fill", ({index: i}) => color(G[i]));

    function random(){
        node
          .transition()
          .duration(2000)
          .attr("cx", function(d){
            return d.x   Math.random()*80 - 40;
          })
          .attr("cy", function(d){
            return d.y   Math.random()*80 - 40;
          });
      }
    setInterval(random, 800);
    if (invalidation != null) invalidation.then(() => simulation.stop());
  
    function intern(value) {
      return value !== null && typeof value === "object" ? value.valueOf() : value;
    }
  
    function ticked() {
      node
        .attr("cx", d => d.x)
        .attr("cy", d => d.y);
  
      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);
    }

    function drag(simulation) {    
      function dragstarted(event) {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        event.subject.fx = event.subject.x;
        event.subject.fy = event.subject.y;
      }
      function dragged(event) {
        event.subject.fx = event.x;
        event.subject.fy = event.y;
      }
      function dragended(event) {
        if (!event.active) simulation.alphaTarget(0);
        event.subject.fx = null;
        event.subject.fy = null;
      }
      return d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended);
    }

    function zoomed({transform}) {
      g.attr("transform", transform);
    }
  
    return Object.assign(svg.node(), {});
}

CodePudding user response:

In your function random(), you don't change the underlying data, you only change how it is represented. Each circle holds a reference to an element in the nodes array, but you set cx and cy within random(), you don't update the underlying data d.x and d.y. And even then, the values the circle has for cx and cy are not reactive. That is, they are not re-evaluated when d.x or d.y changes.

So I would split your code. Have a function random() that is called every 800ms and shuffles the nodes around a bit by changing d.x and d.y. And then the simulation is in charge of actually drawing the circles and the links - the way it already seems to be doing.

const size = 500;

const nodes = [{
    id: 'A',
    x: 150,
    y: 150
  },
  {
    id: 'B',
    x: 250,
    y: 250
  },
  {
    id: 'C',
    x: 350,
    y: 350
  }
];
const links = [{
    source: nodes[0],
    target: nodes[1]
  },
  {
    source: nodes[0],
    target: nodes[2]
  }
];

const svg = d3.select('body')
  .append('svg')
  .attr('width', size)
  .attr('height', size)
  .attr('border', 'solid 1px red')
const g = svg.append('g');

const node = g.append("g")
  .attr("class", "nodes")
  .selectAll("circle")
  .data(nodes)
  .join("circle")
  .attr("r", 5);

const link = g.append("g")
  .attr("class", "links")
  .selectAll("line")
  .data(links)
  .join("line");

const simulation = d3.forceSimulation(nodes)
  .force("link", d3.forceLink(links).strength(2))
  .force("charge", d3.forceManyBody().strength(2))
  .force("center", d3.forceCenter(size / 2, size / 2).strength(0.05))
  .on("tick", ticked);

function ticked() {
  node
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
  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);
}

function random() {
  simulation.stop();
  nodes.forEach(d => {
    d.x  = Math.random() * 80 - 40;
    d.y  = Math.random() * 80 - 40;
  });

  node
    .transition(1000)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
  link
    .transition(1000)
    .attr("x1", d => d.source.x)
    .attr("y1", d => d.source.y)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y)
    .on('end', () => { simulation.restart(); });
}
setInterval(random, 2000);
.links>line {
  stroke: black;
  stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js"></script>

  • Related