#StackBounty: #javascript #reactjs #d3.js How to set D3's tick function for multiple force layouts?

Bounty: 100

I am trying to render multiple D3 force layouts on a page. I managed to initially render the layouts, but only the last graphs’ nodes can be dragged a few seconds after render.

I had the same problem a while ago. The problem came up because d3.drag() and .tick() weren’t pointing to the right d3.forceSimulation. They were pointing to another d3.forceSimulation I mistakenly declared in the global namespace.

This time around I have multiple d3.forceSimulation again, but that’s because I do want to render multiple force layouts.

I tried to map over each force layout’s dataset and call d3.forceSimulation and tick() with each set.

Now, should tick() be called only once for all the data? Or for each layout seperately? It seems as if tick keeps working for the last graph only. So how can tick be set for all force.simulation?

A live example can be found be here

///////////////////////////////////////////////////////////
/////// Functions and variables
///////////////////////////////////////////////////////////

var FORCE = (function(nsp) {

  var
    width = 1080,
    height = 250,
    color = d3.scaleOrdinal(d3.schemeCategory10),

    initForce = (nodes, links) => {
      nsp.force = d3.forceSimulation(nodes)
        .force("charge", d3.forceManyBody().strength(-200))
        .force("link", d3.forceLink(links).distance(70))
        .force("center", d3.forceCenter().x(nsp.width / 5).y(nsp.height / 2))
        .force("collide", d3.forceCollide([5]).iterations([5]));
    },

    enterNode = (selection) => {
      var circle = selection.select('circle')
        .attr("r", 25)
        .style("fill", 'tomato')
        .style("stroke", "bisque")
        .style("stroke-width", "3px")

      selection.select('text')
        .style("fill", "honeydew")
        .style("font-weight", "600")
        .style("text-transform", "uppercase")
        .style("text-anchor", "middle")
        .style("alignment-baseline", "middle")
        .style("font-size", "10px")
        .style("font-family", "cursive")
    },

    updateNode = (selection) => {
      selection
        .attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")
        .attr("cx", function(d) {
          return d.x = Math.max(30, Math.min(width - 30, d.x));
        })
        .attr("cy", function(d) {
          return d.y = Math.max(30, Math.min(height - 30, d.y));
        })
    },

    enterLink = (selection) => {
      selection
        .attr("stroke-width", 3)
        .attr("stroke", "bisque")
    },

    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);
    },

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

    dragStarted = (d) => {
      if (!d3.event.active) nsp.force.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y
    },

    dragging = (d) => {
      d.fx = d3.event.x;
      d.fy = d3.event.y
    },

    dragEnded = (d) => {
      if (!d3.event.active) nsp.force.alphaTarget(0);
      d.fx = null;
      d.fy = null
    },

    drag = () => d3.selectAll('g.node')
    .call(d3.drag()
      .on("start", dragStarted)
      .on("drag", dragging)
      .on("end", dragEnded)
    ),

    tick = (that) => {
      that.d3Graph = d3.select(ReactDOM.findDOMNode(that));
      nsp.force.on('tick', () => {
        that.d3Graph.call(updateGraph)
      });
    };

  nsp.width = width;
  nsp.height = height;
  nsp.enterNode = enterNode;
  nsp.updateNode = updateNode;
  nsp.enterLink = enterLink;
  nsp.updateLink = updateLink;
  nsp.updateGraph = updateGraph;
  nsp.initForce = initForce;
  nsp.dragStarted = dragStarted;
  nsp.dragging = dragging;
  nsp.dragEnded = dragEnded;
  nsp.drag = drag;
  nsp.tick = tick;

  return nsp

})(FORCE || {})

////////////////////////////////////////////////////////////////////////////
/////// class App is the parent component of Link and Node
////////////////////////////////////////////////////////////////////////////

class App extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        data: [{
            name: "one",
            id: 65,
            nodes: [{
                "name": "fruit",
                "id": 0
              },
              {
                "name": "apple",
                "id": 1
              },
              {
                "name": "orange",
                "id": 2
              },
              {
                "name": "banana",
                "id": 3
              }
            ],
            links: [{
                "source": 0,
                "target": 1,
                "lineID": 1
              },
              {
                "source": 0,
                "target": 2,
                "lineID": 2
              },
              {
                "source": 3,
                "target": 0,
                "lineID": 3
              }
            ]
          },
          {
            name: "two",
            id: 66,
            nodes: [{
                "name": "Me",
                "id": 0
              },
              {
                "name": "Jim",
                "id": 1
              },
              {
                "name": "Bob",
                "id": 2
              },
              {
                "name": "Jen",
                "id": 3
              }
            ],
            links: [{
                "source": 0,
                "target": 1,
                "lineID": 1
              },
              {
                "source": 0,
                "target": 2,
                "lineID": 2
              },
              {
                "source": 1,
                "target": 2,
                "lineID": 3
              },
              {
                "source": 2,
                "target": 3,
                "lineID": 4
              },
            ]
          }
        ]
      }
    }

    componentDidMount() {
      const data = this.state.data;
      data.map(({
        nodes,
        links
      }) => (
        FORCE.initForce(nodes, links)
      ));
      FORCE.tick(this)
      FORCE.drag()
    }

    componentDidUpdate(prevProps, prevState) {
      if (prevState.nodes !== this.state.nodes || prevState.links !== this.state.links) {
        const data = this.state.data;
        data.map(({
          nodes,
          links
        }) => (
          FORCE.initForce(nodes, links)
        ));
        FORCE.tick(this)
        FORCE.drag()
      }
    }

    render() {
      const data = this.state.data;

      return (

        <
        div className = "result__container" >

        <
        h5 className = "result__header" > Data < /h5> {
        data.map(({
            name,
            id,
            nodes,
            links
          }) => ( <
            div className = "result__box"
            key = {
              id
            }
            value = {
              name
            } >
            <
            h5 className = "result__name" > {
              name
            } < /h5> <
            div className = {
              "container__graph"
            } >
            <
            svg className = "graph"
            width = {
              FORCE.width
            }
            height = {
              FORCE.height
            } >
            <
            g > {
              links.map((link) => {
                  return ( <
                    Link key = {
                      link.lineID
                    }
                    data = {
                      link
                    }
                    />);
                  })
              } <
              /g> <
              g > {
                nodes.map((node) => {
                    return ( <
                      Node data = {
                        node
                      }
                      label = {
                        node.label
                      }
                      key = {
                        node.id
                      }
                      />);
                    })
                } <
                /g> < /
                svg > <
                /div> < /
                div >
              ))
          } <
          /div>
        )
      }
    }

    ///////////////////////////////////////////////////////////
    /////// Link component
    ///////////////////////////////////////////////////////////

    class Link extends React.Component {

      componentDidMount() {
        this.d3Link = d3.select(ReactDOM.findDOMNode(this))
          .datum(this.props.data)
          .call(FORCE.enterLink);
      }

      componentDidUpdate() {
        this.d3Link.datum(this.props.data)
          .call(FORCE.updateLink);
      }

      render() {
        return ( <
          line className = 'link' / >
        );
      }
    }

    ///////////////////////////////////////////////////////////
    /////// Node component
    ///////////////////////////////////////////////////////////

    class Node extends React.Component {

      componentDidMount() {
        this.d3Node = d3.select(ReactDOM.findDOMNode(this))
          .datum(this.props.data)
          .call(FORCE.enterNode)
      }

      componentDidUpdate() {
        this.d3Node.datum(this.props.data)
          .call(FORCE.updateNode)
      }

      render() {
        return ( <
          g className = 'node' >
          <
          circle onClick = {
            this.props.addLink
          }
          /> <
          text > {
            this.props.data.name
          } < /text> < /
          g >
        );
      }
    }

    ReactDOM.render( < App / > , document.querySelector('#root'))
.container__graph {
  background-color: lightsteelblue;
}

.result__header {
  background-color: aliceblue;
  text-align: center;
  color: cadetblue;
  text-transform: uppercase;
  font-family: cursive;
}

.result__name {
  background-color: bisque;
  text-align: center;
  text-transform: uppercase;
  color: chocolate;
  font-family: cursive;
  margin-bottom: 10px;
  padding: 6px;
}
https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js
https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.