React + D3力布局 - 渲染后几秒钟内圆圈不再可拖动 [英] React + D3 force layout -- Circles not draggable anymore a few seconds after render

查看:127
本文介绍了React + D3力布局 - 渲染后几秒钟内圆圈不再可拖动的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直试图在React中制作一个可拖动的d3力布局一段时间了。 React必须能够与图中的节点进行交互。例如,当你点击一个节点时,React应该能够在onClick上返回节点的id。



我根据Shirley Wu的一个例子制作了4个组件。一个App组件,用于保存图形数据的状态并呈现Graph组件。图形组件呈现节点和链接组件。这样,可点击节点部分就可以了。



当页面呈现时,节点将只能拖动几秒钟。渲染页面后,您可以立即拖动节点,然后突然,被拖动的节点完全停在一个位置。此时,其他节点也不能再被拖动。我希望能够随时拖动节点。



我可以在网上找到一些关于在图形后面创建画布,设置填充和指针事件的提示。关于letting或d3或React进行渲染和计算的讨论也很多。我尝试使用React的所有生命周期方法,但是我无法使用它。



你可以在这里找到一个实时样本:



您应该在定义和指定力模拟时将这些函数移动到方法中,以便 componentDidMount Graph 组件的方法应如下所示(您还应该重写 tick 处理函数,并仅设置force params一次(现在你在每个刻度上都这样做),检查我的笔尖) :

  componentDidMount(){
this.d3Graph = d3.select(ReactDOM.findDOMNode(this) );

var force = d3.forceSimulation(this.props.data.nodes)
.force(charge,d3.forceManyBody()。strength(-50))
.force(link,d3.forceLink(this.props.data.links).distance(90))
.force(center,d3.forceCenter()。x(width / 2).y (身高/ 2))
.force(碰撞,d3.forceCollide([5])。迭代([5]))

函数dragStarted(d){
if(!d3.event.active)force.alphaTarget(0.3).restart()
d.fx = dx
d.fy = dy

}

函数拖动(d){
d.fx = d3.event.x
d.fy = d3.event.y
}

function dragEnded(d){
if(!d3.event.active)force.alphaTarget(0)
d.fx = null
d.fy = null
}

const node = d3.selectAll('g.node')
.call(d3.drag()
.on(start,dragStarted)
.on (拖动,拖动)
.on(end,dragEnded)
);

force.on('tick',()=> {
this.d3Graph.call(updateGraph)
});
}


I've been trying to make a draggable d3 force layout in React for a while now. React has to be able to interact with the nodes in the graph. For example, when you click on a node, React should be able to return the node's id onClick.

I made 4 components according to one of Shirley Wu's examples. An App component that holds the graph data in it's state and renders the Graph component. The graph component renders a Node and a Link component. This way, the clickable nodes part worked out.

When the page renders, the nodes will be draggable only for a few seconds though. Immediately after rendering the page you can drag nodes, then suddenly, the node being dragged stops in one position completely. At this point the other nodes cannot be dragged anymore either. I expected to be able to drag the nodes at all times.

I could find a few hints online about creating a canvas behind the graph, setting fill and pointer-events. There are also many discussions about letting or d3 or React do the rendering and calculations. I tried playing with all of React's lifecycle methods, but I can't get it to work.

You can find a live sample over here: https://codepen.io/vialito/pen/WMKwEr

Remember, the circles will be clickable only for a few seconds. Then they'll stay put in the same place. The behavior is the same in all browsers and after every page refresh. When you log the drag function, you'll see that it does assign new coordinates when dragging, the circle won't be displayed in it's new position though.

I'm very eager to learn about the cause of this problem and it would be very cool if you could maybe even propose a solution.

App.js

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

  render() {
    return (
            <div className="graphContainer">
                <Graph data={this.state.data} />
            </div>
        )
    }
}

class Graph extends React.Component {

    componentDidMount() {
        this.d3Graph = d3.select(ReactDOM.findDOMNode(this));
        var force = d3.forceSimulation(this.props.data.nodes);
        force.on('tick', () => {
            force
            .force("charge", d3.forceManyBody().strength(-50))
            .force("link", d3.forceLink(this.props.data.links).distance(90))
            .force("center", d3.forceCenter().x(width / 2).y(height / 2))
            .force("collide", d3.forceCollide([5]).iterations([5]))

            const node = d3.selectAll('g')
                .call(drag)

            this.d3Graph.call(updateGraph)
        });
    }

    render() {
        var nodes = this.props.data.nodes.map( (node) => {
            return (
            <Node
                data={node}
                name={node.name}
                key={node.id}
            />);
        });
        var links = this.props.data.links.map( (link,i) => {
            return (
                <Link
                    key={link.target+i}
                    data={link}
                />);
        });
        return (
            <svg className="graph" width={width} height={height}>
                <g>
                    {nodes}
                </g>
                <g>
                    {links}
                </g>
            </svg>
        );
    }
}

Node.js

    class Node extends React.Component {

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

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

    handle(e){
        console.log(this.props.data.id + ' been clicked')
    }

    render() {
        return (
            <g className='node'>
                <circle ref="dragMe" onClick={this.handle.bind(this)}/>
                <text>{this.props.data.name}</text>
            </g>
        );
    }
}

Link.js

    class Link extends React.Component {

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

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

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

D3Functions.js

const width = 1080;
const height = 250;
const color = d3.scaleOrdinal(d3.schemeCategory10);
const force = d3.forceSimulation();

const drag = () => {
    d3.selectAll('g')
        .call(d3.drag()
            .on("start", dragStarted)
            .on("drag", dragging)
            .on("end", dragEnded));
};

function dragStarted(d) {
    if (!d3.event.active) force.alphaTarget(0.3).restart()
    d.fx = d.x
    d.fy = d.y

}

function dragging(d) {
    d.fx = d3.event.x
    d.fy = d3.event.y
}

function dragEnded(d) {
    if (!d3.event.active) force.alphaTarget(0)
    d.fx = null
    d.fy = null
}

const enterNode = (selection) => {
    selection.select('circle')
        .attr("r", 30)
        .style("fill", function(d) { return color(d.name) })


    selection.select('text')
        .attr("dy", ".35em")
        .style("transform", "translateX(-50%,-50%")
};

const updateNode = (selection) => {
    selection.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")

};

const enterLink = (selection) => {
    selection.attr("stroke-width", 2)
    .style("stroke","yellow")
        .style("opacity",".2")
};

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

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

解决方案

You define force simulation twice in your code. First time - string 7 in your codepen and second time - string 113. Your dragStarted and dragEnded functions (which are defined globally) use force simulation from string 7, but it not specified (you did not pass nodes, links and other params to it).

You should move these function into the method when you define and specify your force simulation so componentDidMount method for Graph component should look like this (you should also rewrite your tick handler function, and set force params only once (now you do it on each tick), check my fork of your pen):

componentDidMount() {
  this.d3Graph = d3.select(ReactDOM.findDOMNode(this));

  var force = d3.forceSimulation(this.props.data.nodes)
    .force("charge", d3.forceManyBody().strength(-50))
    .force("link", d3.forceLink(this.props.data.links).distance(90))
    .force("center", d3.forceCenter().x(width / 2).y(height / 2))
    .force("collide", d3.forceCollide([5]).iterations([5]))

  function dragStarted(d) {
      if (!d3.event.active) force.alphaTarget(0.3).restart()
      d.fx = d.x
      d.fy = d.y

  }

  function dragging(d) {
      d.fx = d3.event.x
      d.fy = d3.event.y
  }

  function dragEnded(d) {
      if (!d3.event.active) force.alphaTarget(0)
      d.fx = null
      d.fy = null
  }

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

    force.on('tick', () => {
        this.d3Graph.call(updateGraph)
    });
}

这篇关于React + D3力布局 - 渲染后几秒钟内圆圈不再可拖动的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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