最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to create a d3 force layout graph using React - Stack Overflow

programmeradmin5浏览0评论

I would like to create a d3 force layout graph using ReactJS.

I've created other graphs using React + d3 such as pie charts, line graphs, histograms. Now I wonder how to build a svg graphic like the d3 force layout which involves physics and user interaction.

Here is an example of what I want to build

I would like to create a d3 force layout graph using ReactJS.

I've created other graphs using React + d3 such as pie charts, line graphs, histograms. Now I wonder how to build a svg graphic like the d3 force layout which involves physics and user interaction.

Here is an example of what I want to build http://bl.ocks/mbostock/4062045

Share Improve this question edited May 19, 2015 at 18:14 Robert Longson 124k27 gold badges267 silver badges253 bronze badges asked May 19, 2015 at 16:02 cuadramancuadraman 15.2k7 gold badges29 silver badges32 bronze badges 3
  • 2 It should be exactly the same as for any other D3 graph. – Lars Kotthoff Commented May 19, 2015 at 16:28
  • what about the physics animations? – cuadraman Commented May 20, 2015 at 16:55
  • 1 What about it? As far as I can see that shouldn't interfere. – Lars Kotthoff Commented May 20, 2015 at 17:13
Add a ment  | 

3 Answers 3

Reset to default 11

Since D3 and React haven't decreased in popularity the last three years, I figured a more concrete answer might help someone here who wants to make a D3 force layout in React.

Creating a D3 graph can be exactly the same as for any other D3 graph. But you can also use React to replace D3's enter, update and exit functions. So React takes care of rendering the lines, circles and svg.

This could be helpfull when a user should be able to interact a lot with the graph. Where it would be possible for a user to add, delete, edit and do a bunch of other stuff to the nodes and links of the graph.

There are 3 ponents in the example below. The App ponent holds the app's state. In particular the 2 standard arrays with node and link data that should be passed to D3's d3.forceSimulation function.

Then there's one ponent for the links and one ponent for the nodes. You can use React to do anything you want with the lines and circles. You could use React's onClick, for example.

The functions enterNode(selection) and enterLink(selection) render the lines and circles. These functions are called from within the Node and Link ponents. These ponents take the nodes' and links' data as prop before they pass it to these enter functions.

The functions updateNode(selection) and updateLink(selection) update the nodes' and links' positions. They are called from D3's tick function.

I used these functions from a React + D3 force layout example from Shirley Wu.

It's only possible to add nodes in the example below. But I hope it shows how to make the force layout more interactive using React.

Codepen live example

///////////////////////////////////////////////////////////
/////// 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 / 2).y(nsp.height / 2))
        .force("collide", d3.forceCollide([5]).iterations([5]));
    },

    enterNode = (selection) => {
      var circle = selection.select('circle')
        .attr("r", 25)
        .style("fill", function (d) {
            if (d.id > 3) {
                return 'darkcyan'
            } else { return '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 ponent of Link and Node
////////////////////////////////////////////////////////////////////////////

class App extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        addLinkArray: [],
        name: "",
        nodes: [{
            "name": "fruit",
            "id": 0
          },
          {
            "name": "apple",
            "id": 1
          },
          {
            "name": "orange",
            "id": 2
          },
          {
            "name": "banana",
            "id": 3
          }
        ],
        links: [{
            "source": 0,
            "target": 1,
            "id": 0
          },
          {
            "source": 0,
            "target": 2,
            "id": 1
          },
          {
            "source": 0,
            "target": 3,
            "id": 2
          }
        ]
      }
      this.handleAddNode = this.handleAddNode.bind(this)
      this.addNode = this.addNode.bind(this)
    }

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

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

    handleAddNode(e) {
      this.setState({
        [e.target.name]: e.target.value
      });
    }

    addNode(e) {
      e.preventDefault();
      this.setState(prevState => ({
        nodes: [...prevState.nodes, {
          name: this.state.name,
          id: prevState.nodes.length + 1,
          x: FORCE.width / 2,
          y: FORCE.height / 2
        }],
        name: ''
      }));
    }

    render() {
        var links = this.state.links.map((link) => {
            return ( <
              Link key = {
                link.id
              }
              data = {
                link
              }
              />);
            });
          var nodes = this.state.nodes.map((node) => {
              return ( <
                Node data = {
                  node
                }
                name = {
                  node.name
                }
                key = {
                  node.id
                }
                />);
              });
            return ( <
              div className = "graph__container" >
              <
              form className = "form-addSystem"
              onSubmit = {
                this.addNode.bind(this)
              } >
              <
              h4 className = "form-addSystem__header" > New Node < /h4> <
              div className = "form-addSystem__group" >
              <
              input value = {
                this.state.name
              }
              onChange = {
                this.handleAddNode.bind(this)
              }
              name = "name"
              className = "form-addSystem__input"
              id = "name"
              placeholder = "Name" / >
              <
              label className = "form-addSystem__label"
              htmlFor = "title" > Name < /label> < /
              div > <
              div className = "form-addSystem__group" >
              <
              input className = "btnn"
              type = "submit"
              value = "add node" / >
              <
              /div> < /
              form > <
              svg className = "graph"
              width = {
                FORCE.width
              }
              height = {
                FORCE.height
              } >
              <
              g > {
                links
              } <
              /g> <
              g > {
                nodes
              } <
              /g> < /
              svg > <
              /div>
            );
          }
        }

        ///////////////////////////////////////////////////////////
        /////// Link ponent
        ///////////////////////////////////////////////////////////

        class Link extends React.Component {

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

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

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

        ///////////////////////////////////////////////////////////
        /////// Node ponent
        ///////////////////////////////////////////////////////////

        class Node extends React.Component {

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

          ponentDidUpdate() {
            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'))
.graph__container {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.graph {
  background-color: steelblue;
}

.form-addSystem {
  display: grid;
  grid-template-columns: min-content min-content;
  background-color: aliceblue;
  padding-bottom: 15px;
  margin-right: 10px;
}

.form-addSystem__header {
  grid-column: 1/-1;
  text-align: center;
  margin: 1rem;
  padding-bottom: 1rem;
  text-transform: uppercase;
  text-decoration: none;
  font-size: 1.2rem;
  color: steelblue;
  border-bottom: 1px dotted steelblue;
  font-family: cursive;
}

.form-addSystem__group {
  display: grid;
  margin: 0 1rem;
  align-content: center;
}

.form-addSystem__input,
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
  outline: none;
  border: none;
  border-bottom: 3px solid teal;
  padding: 1.5rem 2rem;
  border-radius: 3px;
  background-color: transparent;
  color: steelblue;
  transition: all .3s;
  font-family: cursive;
  transition: background-color 5000s ease-in-out 0s;
}

.form-addSystem__input:focus {
  outline: none;
  background-color: platinum;
  border-bottom: none;
}

.form-addSystem__input:focus:invalid {
  border-bottom: 3px solid steelblue;
}

.form-addSystem__input::-webkit-input-placeholder {
  color: steelblue;
}

.btnn {
  text-transform: uppercase;
  text-decoration: none;
  border-radius: 10rem;
  position: relative;
  font-size: 12px;
  height: 30px;
  align-self: center;
  background-color: cadetblue;
  border: none;
  color: aliceblue;
  transition: all .2s;
}

.btnn:hover {
  transform: translateY(-3px);
  box-shadow: 0 1rem 2rem rgba(0, 0, 0, .2)
}

.btnn:hover::after {
  transform: scaleX(1.4) scaleY(1.6);
  opacity: 0;
}

.btnn:active,
.btnn:focus {
  transform: translateY(-1px);
  box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .2);
  outline: 0;
}

.form-addSystem__label {
  color: lightgray;
  font-size: 20px;
  font-family: cursive;
  font-weight: 700;
  margin-left: 1.5rem;
  margin-top: .7rem;
  display: block;
  transition: all .3s;
}

.form-addSystem__input:placeholder-shown+.form-addSystem__label {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4rem);
}

.form-addSystem__link {
  grid-column: 2/4;
  justify-self: center;
  align-self: center;
  text-transform: uppercase;
  text-decoration: none;
  font-size: 1.2rem;
  color: steelblue;
}
<script crossorigin src="https://unpkg./react@16/umd/react.development.js"></script>
</script>
<script crossorigin src="https://unpkg./react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/d3/4.13.0/d3.js"></script>

<div id="root"></div>

Colin Megill has a great blog post on this: http://formidable./blog/2015/05/21/react-d3-layouts/. There is also a working jsbin http://jsbin./fanofa/14/embed?js,output. There is a b.locks account, JMStewart, who has an interesting implementation that wraps React in d3 code: http://bl.ocks/JMStewart/f0dc27409658ab04d1c8.

Everyone who implements force-layouts in React notices a minor performance loss. For plex charts (beyond 100 nodes) this bees much more severe.

Note: There is an open issue on react-motion for applying forces (which would otherwise be a good react solution to this) but its gone silent.

**THIS IS NOT AN ANSWER BUT STACKOVERFLOW DOES NOT HAVE THE FACILITY TO ADD A COMMENT FOR ME. **

My question is to vincent. The code piles perfectly but when i run it the background gets drawn with the blue color but the graph actually renders as 4 dots on the top left corner. That is all gets drawn. I have tried may approaches but always seem to be getting the same results just 4 dots on the top left corner. My email id is [email protected]. Would appreciate it if you could let me know if you had this problem

///////////////////////////////////////////////////////////
/////// 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 / 2).y(nsp.height / 2))
        .force("collide", d3.forceCollide([5]).iterations([5]));
    },

    enterNode = (selection) => {
      var circle = selection.select('circle')
        .attr("r", 25)
        .style("fill", function (d) {
            if (d.id > 3) {
                return 'darkcyan'
            } else { return '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 ponent of Link and Node
////////////////////////////////////////////////////////////////////////////

class App extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        addLinkArray: [],
        name: "",
        nodes: [{
            "name": "fruit",
            "id": 0
          },
          {
            "name": "apple",
            "id": 1
          },
          {
            "name": "orange",
            "id": 2
          },
          {
            "name": "banana",
            "id": 3
          }
        ],
        links: [{
            "source": 0,
            "target": 1,
            "id": 0
          },
          {
            "source": 0,
            "target": 2,
            "id": 1
          },
          {
            "source": 0,
            "target": 3,
            "id": 2
          }
        ]
      }
      this.handleAddNode = this.handleAddNode.bind(this)
      this.addNode = this.addNode.bind(this)
    }

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

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

    handleAddNode(e) {
      this.setState({
        [e.target.name]: e.target.value
      });
    }

    addNode(e) {
      e.preventDefault();
      this.setState(prevState => ({
        nodes: [...prevState.nodes, {
          name: this.state.name,
          id: prevState.nodes.length + 1,
          x: FORCE.width / 2,
          y: FORCE.height / 2
        }],
        name: ''
      }));
    }

    render() {
        var links = this.state.links.map((link) => {
            return ( <
              Link key = {
                link.id
              }
              data = {
                link
              }
              />);
            });
          var nodes = this.state.nodes.map((node) => {
              return ( <
                Node data = {
                  node
                }
                name = {
                  node.name
                }
                key = {
                  node.id
                }
                />);
              });
            return ( <
              div className = "graph__container" >
              <
              form className = "form-addSystem"
              onSubmit = {
                this.addNode.bind(this)
              } >
              <
              h4 className = "form-addSystem__header" > New Node < /h4> <
              div className = "form-addSystem__group" >
              <
              input value = {
                this.state.name
              }
              onChange = {
                this.handleAddNode.bind(this)
              }
              name = "name"
              className = "form-addSystem__input"
              id = "name"
              placeholder = "Name" / >
              <
              label className = "form-addSystem__label"
              htmlFor = "title" > Name < /label> < /
              div > <
              div className = "form-addSystem__group" >
              <
              input className = "btnn"
              type = "submit"
              value = "add node" / >
              <
              /div> < /
              form > <
              svg className = "graph"
              width = {
                FORCE.width
              }
              height = {
                FORCE.height
              } >
              <
              g > {
                links
              } <
              /g> <
              g > {
                nodes
              } <
              /g> < /
              svg > <
              /div>
            );
          }
        }

        ///////////////////////////////////////////////////////////
        /////// Link ponent
        ///////////////////////////////////////////////////////////

        class Link extends React.Component {

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

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

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

        ///////////////////////////////////////////////////////////
        /////// Node ponent
        ///////////////////////////////////////////////////////////

        class Node extends React.Component {

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

          ponentDidUpdate() {
            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'))
.graph__container {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.graph {
  background-color: steelblue;
}

.form-addSystem {
  display: grid;
  grid-template-columns: min-content min-content;
  background-color: aliceblue;
  padding-bottom: 15px;
  margin-right: 10px;
}

.form-addSystem__header {
  grid-column: 1/-1;
  text-align: center;
  margin: 1rem;
  padding-bottom: 1rem;
  text-transform: uppercase;
  text-decoration: none;
  font-size: 1.2rem;
  color: steelblue;
  border-bottom: 1px dotted steelblue;
  font-family: cursive;
}

.form-addSystem__group {
  display: grid;
  margin: 0 1rem;
  align-content: center;
}

.form-addSystem__input,
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
  outline: none;
  border: none;
  border-bottom: 3px solid teal;
  padding: 1.5rem 2rem;
  border-radius: 3px;
  background-color: transparent;
  color: steelblue;
  transition: all .3s;
  font-family: cursive;
  transition: background-color 5000s ease-in-out 0s;
}

.form-addSystem__input:focus {
  outline: none;
  background-color: platinum;
  border-bottom: none;
}

.form-addSystem__input:focus:invalid {
  border-bottom: 3px solid steelblue;
}

.form-addSystem__input::-webkit-input-placeholder {
  color: steelblue;
}

.btnn {
  text-transform: uppercase;
  text-decoration: none;
  border-radius: 10rem;
  position: relative;
  font-size: 12px;
  height: 30px;
  align-self: center;
  background-color: cadetblue;
  border: none;
  color: aliceblue;
  transition: all .2s;
}

.btnn:hover {
  transform: translateY(-3px);
  box-shadow: 0 1rem 2rem rgba(0, 0, 0, .2)
}

.btnn:hover::after {
  transform: scaleX(1.4) scaleY(1.6);
  opacity: 0;
}

.btnn:active,
.btnn:focus {
  transform: translateY(-1px);
  box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .2);
  outline: 0;
}

.form-addSystem__label {
  color: lightgray;
  font-size: 20px;
  font-family: cursive;
  font-weight: 700;
  margin-left: 1.5rem;
  margin-top: .7rem;
  display: block;
  transition: all .3s;
}

.form-addSystem__input:placeholder-shown+.form-addSystem__label {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4rem);
}

.form-addSystem__link {
  grid-column: 2/4;
  justify-self: center;
  align-self: center;
  text-transform: uppercase;
  text-decoration: none;
  font-size: 1.2rem;
  color: steelblue;
}
<script crossorigin src="https://unpkg./react@16/umd/react.development.js"></script>
</script>
<script crossorigin src="https://unpkg./react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/d3/4.13.0/d3.js"></script>

<div id="root"></div>

发布评论

评论列表(0)

  1. 暂无评论