javascript - 如何使用 React 创建 d3 力布局图

标签 javascript svg d3.js reactjs

我想创建一个d3 force layout graph使用ReactJS。

我使用 React + d3 创建了其他图表,例如饼图、折线图、直方图。现在我想知道如何构建一个像 d3 力布局这样的 svg 图形,它涉及物理和用户交互。

这是我想要构建的示例 http://bl.ocks.org/mbostock/4062045

最佳答案

由于 D3 和 React 在过去三年中的受欢迎程度并没有下降,我认为更具体的答案可能会帮助这里想要在 React 中进行 D3 强制布局的人。

创建 D3 图与创建任何其他 D3 图完全相同。但你也可以使用React来替代D3的进入、更新和退出功能。因此 React 负责渲染直线、圆和 svg。

当用户应该能够与图表进行大量交互时,这可能会很有帮助。用户可以对图表的节点和链接添加、删除、编辑和执行许多其他操作。

下面的示例中有 3 个组件。 App 组件保存应用程序的状态。特别是带有节点和链接数据的 2 个标准数组,应将其传递给 D3 的 d3.forceSimulation 函数。

然后有一个用于链接的组件和一个用于节点的组件。您可以使用 React 对直线和圆圈执行任何您想要的操作。例如,您可以使用 React 的 onClick

函数enterNode(selection)enterLink(selection)渲染直线和圆。这些函数是从 Node 和 Link 组件中调用的。这些组件将节点和链接的数据作为属性,然后将其传递给这些输入函数。

函数updateNode(selection)updateLink(selection)更新节点和链接的位置。它们是从 D3 的 tick 函数调用的。

我使用 React + D3 force layout example from Shirley Wu 中的这些函数.

只能在下面的示例中添加节点。但我希望它展示了如何使用 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 component 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)
    }

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

    componentDidUpdate(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 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'))
.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.com/react@16/umd/react.development.js"></script>
</script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>

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

关于javascript - 如何使用 React 创建 d3 力布局图,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30330646/

相关文章:

javascript - Google map fitBounds 是否在标记标签上抛出错误?

javascript - 在 Angular 切换开关上停止传播

javascript - 未设置 <svg> 上的宽度/高度属性时获取 svg 图形大小

javascript - 在 PDF 文件上打印生成的 QR 码

javascript - data.key 未定义 : how to parse json data

javascript - jQuery 验证消息问题

javascript - HTML5输入类型="file"不会触发handleFileSelect()

svg - Graphviz -> svg (->pdf) 文本对一些观众来说开箱即用

javascript - D3 stack() 跳过数据数组中的第一个对象

javascript - D3.js 条形图 - 轴和标签不起作用/转换