React + D3 强制布局——渲染后几秒内圆圈不再可拖动

IT技术 javascript reactjs d3.js
2021-05-05 10:49:17

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

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

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

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

您可以在此处找到实时示例:https : //codepen.io/vialito/pen/WMKwEr

请记住,圆圈只能点击几秒钟。然后他们会留在同一个地方。所有浏览器和每次页面刷新后的行为都是相同的。当您记录拖动功能时,您会看到它在拖动时确实分配了新坐标,但是圆圈不会显示在它的新位置。

我非常渴望了解此问题的原因,如果您甚至可以提出解决方案,那将是非常酷的。

应用程序.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>
        );
    }
}

节点.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>
        );
    }
}

链接.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);
};
1个回答

您在代码中定义了两次力模拟。第一次 - 代码笔中的字符串 7,第二次 - 字符串 113。您的dragStarteddragEnded函数(全局定义)使用来自字符串 7 的力模拟,但未指定(您没有将节点、链接和其他参数传递给它)。

在此处输入图片说明

当您定义和指定力模拟时,您应该将这些函数移动到方法中,因此组件的componentDidMount方法Graph应该如下所示(您还应该重写tick处理程序函数,并且只设置一次力参数(现在您在每个刻度上都设置一次),检查我的钢笔叉):

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