【问题标题】:How to set D3's tick function for multiple force layouts?多力布局如何设置D3的刻度功能?
【发布时间】:2018-11-01 22:02:15
【问题描述】:

我正在尝试在一个页面上呈现多个 D3 强制布局。我设法最初渲染了布局,但只有最后一个图形的节点可以在渲染几秒钟后拖动。

我不久前有the same problem。问题出现是因为d3.drag().tick() 没有指向正确的d3.forceSimulation。他们指向另一个 d3.forceSimulation 我错误地在全局命名空间中声明。

这一次我又多了多个d3.forceSimulation,但那是因为我确实想渲染多个强制布局。

我尝试映射每个力布局的数据集,并为每个集合调用 d3.forceSimulationtick()

现在,是否应该为所有数据只调用一次 tick()?还是分别为每个布局?似乎刻度线仅对最后一张图有效。那么如何为所有force.simulation设置tick呢?

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;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></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 reactjs d3.js


    【解决方案1】:

    要快速回答您的问题:您确实需要为每个模拟使用每个 force.on('tick',...),因为在一段时间不活动后,tick 事件不会触发。但是您的程序实际上遇到了另一个导致其他图表冻结的问题。

    问题:

    主要问题是您正在创建多个不同的力模拟,但将相同的处理程序附加到所有这些:

      data.map(({
        nodes,
        links
      }) => (
        FORCE.initForce(nodes, links) // initializes multiple force simulations 
      ));
      FORCE.tick(this)
      FORCE.drag() // attaches only one handler to them all
    

    当您查看代码时,您可以看到您正在为每个图表重新分配 nsp.force,因此它仅指最后一个:

    initForce = (nodes, links) => {
      nsp.force = d3.forceSimulation(nodes)
          ...
    },
    

    这成为您的拖动处理程序的问题,它改变了 alphaTarget,它或多或少地告诉nsp.force(现在只指最后一张图)它需要继续模拟并重新启动模拟:

    dragStarted = (d) => {
      if (!d3.event.active) nsp.force.alphaTarget(0.3).restart();
      // ...
    },
    
    dragEnded = (d) => {
      if (!d3.event.active) nsp.force.alphaTarget(0);
      // ...
    },
    

    因此,除了最后一个之外,每张图都在几秒钟后停止移动的原因是因为那些模拟认为它们是完整的,因为它们没有被拖动事件重新唤醒。

    解决方案:

    我相信您一定遇到过,将多个力模拟作为一个大对象进行管理是很棘手的。由于它们似乎是独立的,因此为每个图的力模拟创建单独的实例更有意义。在下面的代码 sn-p 中,我将对象 FORCE 更改为构造函数 Force,它创建了一个力模拟实例,我们将多次创建该实例,每个图一个。

    我还制作了 ForceGraph,这是一个用于保存图形和 SVG 的 React 组件,用于分离每个图形的元素,并使该力模拟更容易起作用。

    结果如下:

    ///////////////////////////////////////////////////////////
    /////// Functions and variables
    ///////////////////////////////////////////////////////////
    
    function Force() {
      var width = 1080,
      height = 250,
      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);
      },
      color = d3.scaleOrdinal(d3.schemeCategory10),
      nsp = {},
          
      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]));
      },
    
      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 = (node) => {
        var d3Graph = d3.select(ReactDOM.findDOMNode(node)).select('svg');
        d3Graph.selectAll('g.node')
        .call(d3.drag()
          .on("start", dragStarted)
          .on("drag", dragging)
          .on("end", dragEnded));
      },
    
      tick = (node) => {
        var d3Graph = d3.select(ReactDOM.findDOMNode(node)).select('svg');
        nsp.force.on('tick', () => {
          d3Graph.call(updateGraph)
        });
      };
    
      nsp.enterNode = enterNode;
      nsp.updateNode = updateNode;
      nsp.enterLink = enterLink;
      nsp.updateLink = updateLink;
      nsp.width = width;
      nsp.height = height;
      nsp.initForce = initForce;
      nsp.dragStarted = dragStarted;
      nsp.dragging = dragging;
      nsp.dragEnded = dragEnded;
      nsp.drag = drag;
      nsp.tick = tick;
      
      return nsp
      
    }
    
    class ForceGraph extends React.Component {
        constructor(props) {
            super(props);
            this.data = props.data;
            this.force = Force();
        }
    
        initGraph() {
            const data = this.data;
            this.force.initForce(data.nodes, data.links)
            this.force.tick(this)
            this.force.drag(this)
    
        }
    
        componentDidMount() {
            this.initGraph();
        }
    
        componentDidUpdate(prevProps, prevState) {
            // TBD
        }
    
        render() {
            const {name, id, nodes, links} = this.data;
            const force = this.force;
            return (
                <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}
                                        force={force}
                                    />);
                            })}
                        </g>
                        <g>
                            {nodes.map((node) => {
                                return (
                                    <Node
                                        data={node}
                                        label={node.label}
                                        force={force}
                                        key={node.id}
                                    />);
                            })}
                        </g>
                    </svg>
                    </div>
                </div>
            );
          }
    
    
    };
    
    ////////////////////////////////////////////////////////////////////////////
    /////// 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
                  },
                ]
              }
            ]
          }
        }
        
        render() {
            const data = this.state.data;
            return (
                <div className="result__container">
                    <h5 className="result__header">Data</h5>
                    {data.map((graphData) => (<ForceGraph data={graphData} key={graphData.id} />))}
                </div>
            )
        }
    }
    
    ///////////////////////////////////////////////////////////
    /////// Link component
    ///////////////////////////////////////////////////////////
    
    class Link extends React.Component {
    
        componentDidMount() {
          this.d3Link = d3.select(ReactDOM.findDOMNode(this))
            .datum(this.props.data)
            .call(this.props.force.enterLink);
        }
      
        componentDidUpdate() {
          this.d3Link.datum(this.props.data)
            .call(this.props.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(this.props.force.enterNode)
        }
    
        componentDidUpdate() {
          this.d3Node.datum(this.props.data)
            .call(this.props.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;
    }
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></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>

    更新:看起来如果您尝试更新图表的数据,它会遇到一些障碍。我稍微修改了一下,就想出了这个小提琴。它并不完美,但它是一个开始:https://jsfiddle.net/4pyzL0tq/

    【讨论】:

    • 史蒂夫太棒了。我以前没有一次学到这么多。这需要付出很多努力,对此我非常感激。谢谢你的美丽例子。
    • 谢谢你的好话,文森特;很高兴我能帮上忙。我确实注意到更新图表上的数据看起来不起作用。我已经用您可能采取的方法示例更新了我的答案。它超出了这个问题的范围,但您可能会发现它很有用。
    • 你领先我一步,真是太好了!现在可以编辑单个图表。在结果页面上显示这些不同的图表是我遇到这个问题的地方。我还没有达到您可以编辑这些图表之一的地步,但这真的很酷!数据存储在数据库中并来自数据库。我必须从这些道具创建一个新对象并将其设置为App 中的状态。道具并没有让 D3 对其进行变异。很长一段时间以来,我一直在尝试用 D3 制作一些东西,我真的很喜欢它。这么大的帮助对我来说真的很重要,谢谢。
    猜你喜欢
    • 2012-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-25
    • 1970-01-01
    • 2016-12-02
    相关资源
    最近更新 更多