如何为多个力布局设置D3的刻度功能? [英] How to set D3's tick function for multiple force layouts?

查看:61
本文介绍了如何为多个力布局设置D3的刻度功能?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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.

我有同样的问题.由于d3.drag().tick()没有指向正确的d3.forceSimulation,因此出现了问题.他们指向另一个我在全局名称空间中错误声明的d3.forceSimulation.

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.

这一次我又有多个d3.forceSimulation,但这是因为我确实要渲染多个力的布局.

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

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

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

现在,对于所有数据,tick()应该仅被调用一次吗?还是分别为每个布局?滴答似乎只对最后一张图起作用.那么如何为所有force.simulation设置刻度?

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?

可以在此处找到一个在线示例

///////////////////////////////////////////////////////////
/////// 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>

推荐答案

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

For a quick answer to your question: you do need to use each individual force.on('tick',...) for each simulation, because the tick event doesn't fire after a period of inactivity. But your program is actually running into another problem that causes the other graphs to freeze.

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

The main issue is that you're creating multiple different force simulations, but attaching the same handler to all of them:

  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,因此它仅指的是最后一个图形:

When you look at the code, you can see that you're reassigning nsp.force for each graph so it's only referring to the last one:

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

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

This becomes a problem with your drag handler, which changes the alphaTarget, which more or less tells nsp.force (which now refers only to the last graph) that it needs to continuing simulating and to restart the simulation:

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

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

所以除了最后一个图形外,每个图形都在几秒钟后停止移动的原因是因为那些模拟认为它们是完整的,因为它们没有被拖动事件所撤销.

So the reason why every graph but the last stops moving after a few seconds is because those simulation think they are complete because they aren't rewoken with a drag event.

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

As I'm sure you've encountered, it's tricky managing multiple force simulations as one big object. Since they seem to be independent, it makes more sense to create a separate instance for the force simulation for each graph. In the following code snippet, I've changed the object FORCE, to be constructor Force, that creates an instance of a force simulation, which we'll create multiple times, one for each graph.

我还制作了ForceGraph这个React组件来保存图形和SVG,以分离出每个图形的元素,并使作用力仿真更容易进行.

I've also made ForceGraph, a React component to hold the graph and SVG, to separate out elements for each graph, and makes it easer for that force simulation to act on.

以下是结果:

///////////////////////////////////////////////////////////
/////// 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/

Update: It looks like if you try to update the data for the charts, it runs into some snags. I tinkered with it a little bit and came up with this fiddle. It's not perfect, but it's a start: https://jsfiddle.net/4pyzL0tq/

这篇关于如何为多个力布局设置D3的刻度功能?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆