React + D3 Force Layout, new nodes have no d.x and d.y

599 views
Skip to first unread message

Hayley Kwan

unread,
May 5, 2017, 10:51:21 AM5/5/17
to d3-js
Data for nodes and links are provided to the ForceLayout component via React props. D3 does the data calculation and rendering when new props are received, so that React does not have to go through rendering every tick.

D3 works well with a static layout. But when I receive new nodes and links at shouldComponentUpdate(nextProps) function, the nodes lack the following attributes: 
  • index - the zero-based index of the node within the nodes array.
  • x - the x-coordinate of the current node position.
  • y - the y-coordinate of the current node position.
  • As a result, all new nodes have <g tranform=translate(undefined, undefined)/> and are clustered at the left top corner. 
The way I update the props are by pushing new objects to the nodes array and links array. I don't understand why D3 doesn't assign d.x and d.y as it did for the initial setup at componentDidMount(). I have been struggling with this problem for days. Hope someone can help me out here.

ForceLayout.jsx loading on client side:
import React from 'react';
import * as d3 from 'd3';

export default class ForceLayout extends React.Component{
 constructor(props){
   super(props);
 }

  componentDidMount(){        // works fine here
   const nodes = this.props.nodes;
   const links = this.props.links;
   const width = this.props.width;
   const height = this.props.height;

    this.simulation = d3.forceSimulation(nodes)
     .force("link", d3.forceLink(links).distance(50))
     .force("charge", d3.forceManyBody().strength(-120))
     .force('center', d3.forceCenter(width / 2, height / 2));

    this.svg = d3.select('svg');
   this.svg.call(d3.zoom().on(
     "zoom", () => {
       this.svg.attr("transform", d3.event.transform)
     })
   );

    this.graph = d3.select(this.refs.graph);

    var node = this.graph.selectAll('.node')
     .data(nodes)
     .enter()
     .append('g')
     .attr("class", "node")
     .call(enterNode);

    var link = this.graph.selectAll('.link')
     .data(links)
     .enter()
     .call(enterLink);

    this.simulation.on('tick', () => {
     this.graph.call(updateGraph);
   });
 }

  shouldComponentUpdate(nextProps){    // TROUBLE IS HERE

    //only allow d3 to re-render if the nodes and links props are different
    if(nextProps.nodes !== this.props.nodes || nextProps.links !== this.props.links){
     console.log('should only appear when updating graph');

      this.simulation.stop();
     this.graph = d3.select(this.refs.graph);

      var d3Nodes = this.graph.selectAll('.node')
       .data(nextProps.nodes) //join
        .enter() //get new nodes
        .append('g')
       .attr("class", "node")
       .call(enterNode);
     d3Nodes.exit().remove(); //get nodes to be removed

      var d3Links = this.graph.selectAll('.link')
       .data(nextProps.links)
       .enter()
       .call(enterLink);
     d3Links.exit().remove();

      const newNodes = Object.assign({}, nextProps.nodes);
     const newLinks = Object.assign({}, nextProps.links);
     this.simulation.nodes(newNodes);
     this.simulation.force("link").links(newLinks);

      this.simulation.alpha(1).restart();

      this.simulation.on('tick', () => {
       this.graph.call(updateGraph);
     });
   }

    return false;
 }

  render(){
   return(
     <svg
       width={this.props.width}
       height={this.props.height}
       style={this.props.style}>
       <g ref='graph' />
     </svg>
   );
 }
}


//  d3 functions to manipulate attributes
var enterNode = (selection) => {
 selection.append('circle')
       .attr('r', 10)
       .style('fill', '#888888')
       .style('stroke', '#fff')
       .style('stroke-width', 1.5);

  selection.append("text")
       .attr("x", function(d){return 20})
        .attr("dy", ".35em") // vertically centre text regardless of font size
        .text(function(d) { return d.word });
};

var enterLink = (selection) => {
 selection.append('line')
   .attr("class", "link")
   .style('stroke', '#999999')
   .style('stroke-opacity', 0.6);
};

var updateNode = (selection) => {
 selection.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")");
};

var updateLink = (selection) => {
 selection.attr("x1", (d) => d.source.x)
   .attr("y1", (d) => d.source.y)
   .attr("x2", (d) => d.target.x)
   .attr("y2", (d) => d.target.y);
};

var updateGraph = (selection) => {
 selection.selectAll('.node')
   .call(updateNode);
 selection.selectAll('.link')
   .call(updateLink);
};



Server updating data nodes and links:
exports.update =  function(currentGraph, submitted, datamuseRe){

  // currentGraph.nodes are the array of nodes
  // currentGraph.links are the array of links
 
  // submitted is one object: word, num, deg
  // datamuseRe is array of objects: word, score,

  //if datamuseResponse is empty, return same graph
  if(datamuseRe.length === 0){
    console.log('Datamuse returns nothing. Returning same graph' + currentGraph);
    return currentGraph;
  }

  //else update graph

  //check if the submittedObject is an object from originalJsonObject
  if(indexOfWordInGraph(currentGraph, submitted) !== -1){
    //source is present, only need to add links
    const centreIndex = indexOfWordInGraph(currentGraph, submitted);
    currentGraph.nodes[centreIndex].score = 80;
    //for each datamuse response object
    //check if target exists in currentGraph
    //if it does, link centreIndex (src) and this index (target) up
    //if not, append new node, link centreIndex and this up

    for(var i = 0 ; i < datamuseRe.length ; i++){
      if(indexOfWordInGraph(currentGraph, datamuseRe[i]) !== -1){
        const targetIndex = indexOfWordInGraph(currentGraph, datamuseRe[i]);
        var link = {
          source: currentGraph.nodes[centreIndex],
          target: currentGraph.nodes[targetIndex]
        };
        currentGraph.links.push(link);
      } else {
        var node = {     //create new node
          word: datamuseRe[i].word,
          size: 50,
          score: 1
        };
        currentGraph.nodes.push(node);
        var link = {     //create new link
          source: currentGraph.nodes[centreIndex],
          target: node
        };
        currentGraph.links.push(link);
      }
    }
  } else {
    // not present, need to add new centre
    // create new centre node
    var centre = {
      word: submitted.word,
      size: 80,
      score: 1
    };
    currentGraph.nodes.push(centre);

    // for each datamuse response object
      //check if it exists in currentGraph
      //if it does, link centreIndex (src) and this index (target) up
      //if not, append new node, link centreIndex and this up
    for(var i = 0 ; i < datamuseRe.length ; i++){
      if(indexOfWordInGraph(currentGraph, datamuseRe[i]) !== -1){
        const targetIndex = indexOfWordInGraph(currentGraph, datamuseRe[i]);
        var link = {
          source: centre,
          target: currentGraph.nodes[targetIndex]
        };
        currentGraph.links.push(link);
      } else {
        var node = {     //create new node
          word: datamuseRe[i].word,
          size: 50,
          score: 1
        };
        currentGraph.nodes.push(node);
        var link = {     //create new link
          source: centre,
          target: node
        };
        currentGraph.links.push(link);
      }
    }
  }
  return currentGraph;
}

//if present, return index
//else return -1
function indexOfWordInGraph(currentGraph, obj){
  for(var i = 0 ; i < currentGraph.nodes.length ; i++){
    if(currentGraph.nodes[i].word === obj.word){
      return i;
    }
  }
  return -1;
}





Ram Tobolski

unread,
May 7, 2017, 2:16:04 PM5/7/17
to d3-js
Hi. If you add nodes to the graph, you have to call method simulation.nodes([nodes]) again.

See the documentation (https://github.com/d3/d3-force/blob/master/README.md):

"If the specified array of nodes is modified, such as when nodes are added to or removed from the simulation, this method must be called again with the new (or changed) array to notify the simulation and bound forces of the change"

Reply all
Reply to author
Forward
0 new messages