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

javascript - React + D3 force layout -- Circles not draggable anymore a few seconds after render - Stack Overflow

programmeradmin3浏览0评论

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 ponents according to one of Shirley Wu's examples. An App ponent that holds the graph data in it's state and renders the Graph ponent. The graph ponent renders a Node and a Link ponent. 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 pletely. 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:

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 {

    ponentDidMount() {
        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 {

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

    ponentDidUpdate() {
        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 {

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

    ponentDidUpdate() {
        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);
};

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 ponents according to one of Shirley Wu's examples. An App ponent that holds the graph data in it's state and renders the Graph ponent. The graph ponent renders a Node and a Link ponent. 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 pletely. 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 {

    ponentDidMount() {
        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 {

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

    ponentDidUpdate() {
        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 {

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

    ponentDidUpdate() {
        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);
};
Share Improve this question edited Feb 25, 2018 at 8:51 Vialito asked Feb 23, 2018 at 5:01 VialitoVialito 5238 silver badges29 bronze badges 1
  • 1 I took a look at your question, it seems like the simulation is not started again after it's initial rendering. What makes it a bit plicated to debug is that you're rendering DOM elements with both React and D3. Your on tick function also always sets the simulation forces over and over again (you could do that once, when you initialise the force). – pasql Commented Feb 25, 2018 at 11:32
Add a ment  | 

1 Answer 1

Reset to default 7 +100

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 ponentDidMount method for Graph ponent 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):

ponentDidMount() {
  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)
    });
}
发布评论

评论列表(0)

  1. 暂无评论