reactjs - 数据更改时如何重新应用 d3.forceCollide
问题描述
我从 d3 中的 forceCollide 方法中看到了一个相当奇怪的效果。我第一次绘制图表时效果很好,但每次数据更改时,节点会越来越靠近并最终重叠。
这是一个大型的反应应用程序,所以我不能在这里发布所有代码。这是数据更改时运行的代码:
var simulation = forceSimulation;
if (!simulation) {
simulation = d3ForceSimulation();
setForceSimulation(simulation);
}
simulation
.nodes(nodes)
.force('charge', d3ForceManyBody())
.force('center', d3ForceCenter(width / 2, height / 2))
.force(
'link',
d3ForceLink(links).strength((link) => link.weight),
)
.force(
'collision',
d3ForceCollide((n) => nodeRadius(n) * (edgeLabels ? 2 : 1.4) * spacing),
)
.on('tick', tick)
.restart();
我尝试了各种方法——比如传递一个常数值forceCollide
而不是计算节点的半径,但我仍然看到了理智的效果。我还尝试.force('collision', null)
在添加新力量之前添加,以防它没有正确替换旧力量,但这也没有任何区别。
这是一个有效的事情吗?我forceCollide
没有改变,我是否应该仅在首次创建模拟时将其附加到模拟中?
作为参考,这是整个反应组件的源代码。
import React, { useEffect, useState } from 'react';
import { select as d3Select } from 'd3';
import { forceSimulation as d3ForceSimulation } from 'd3-force';
import { forceManyBody as d3ForceManyBody } from 'd3-force';
import { forceCenter as d3ForceCenter } from 'd3-force';
import { forceLink as d3ForceLink } from 'd3-force';
import { forceCollide as d3ForceCollide } from 'd3-force';
import usePrevious from '~util/usePrevious';
export const ForceNetworkDiagram = ({ view, graphData, width, height, zoom, offsetX, offsetY }) => {
const leftMargin = view.jsonLayout.diagram?.leftMargin || 30;
const topMargin = view.jsonLayout.diagram?.topMargin || 0;
const spacing = view.jsonLayout.diagram?.spacing || 1;
const nodeLabels = !!view.jsonLayout.diagram?.nodeLabels;
const edgeLabels = !!view.jsonLayout.diagram?.edgeLabels;
const [forceSimulation, setForceSimulation] = useState(null);
const [nodes, setNodes] = useState(null);
const [links, setLinks] = useState(null);
const prevNodeLabels = usePrevious(nodeLabels);
const prevEdgeLabels = usePrevious(edgeLabels);
const nodeRadius = (node) => node.weight * 30 + 10;
// This effect transforms graph data into the force simulation data structures
useEffect(() => {
setNodes(graphData.nodes);
const nodesById = {};
graphData.nodes.forEach((node) => {
nodesById[node.id] = node;
});
setLinks(
graphData.edges.map((edge) => ({
source: nodesById[edge.from],
target: nodesById[edge.to],
label: edge.label,
weight: edge.weight,
})),
);
}, [graphData]);
// This effect starts a force simulation when the data changes, and updates the
// drawing as the simulation plays out
useEffect(() => {
function link(d) {
return (
'M' +
d.source.x +
',' +
d.source.y +
'C' +
(d.source.x + d.target.x) / 2 +
',' +
d.source.y +
' ' +
(d.source.x + d.target.x) / 2 +
',' +
d.target.y +
' ' +
d.target.x +
',' +
d.target.y
);
}
function tick() {
d3Select('.nodes')
.selectAll('g.node')
.data(nodes)
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
const linkGroups = d3Select('.links')
.selectAll('g.link')
.data(links);
linkGroups.select('path').attr('d', link);
if (edgeLabels) {
linkGroups
.select('text')
.attr('x', (link) => (link.source.x + link.target.x) / 2)
.attr('y', (link) => (link.source.y + link.target.y) / 2);
}
}
if (!nodes || !links) return;
var simulation = forceSimulation;
if (!simulation) {
simulation = d3ForceSimulation();
setForceSimulation(simulation);
}
simulation
.nodes(nodes)
.force('charge', d3ForceManyBody())
.force('center', d3ForceCenter(width / 2, height / 2))
.force(
'link',
d3ForceLink(links).strength((link) => link.weight),
)
.force(
'collision',
d3ForceCollide((n) => nodeRadius(n) * (edgeLabels ? 2 : 1.4) * spacing),
)
.on('tick', tick)
.restart();
return () => {
simulation.stop();
};
}, [forceSimulation, nodes, links, width, height, nodeLabels, edgeLabels, spacing]);
// This effect draws cicles for each node
useEffect(() => {
function nodeClass(node) {
return node.tags ? 'node ' + node.tags.join(' ') : 'node';
}
if (!nodes) return;
// Bind data to nodes
const existingNodes = d3Select('.nodes')
.selectAll('g.node')
.data(nodes);
const newNodes = existingNodes.enter().append('g');
existingNodes.exit().remove();
// Set the classes on the node container
newNodes.attr('class', nodeClass);
existingNodes.attr('class', nodeClass);
// Draw or update a circle to represent the node
newNodes.append('circle').attr('r', nodeRadius);
existingNodes.select('circle').attr('r', nodeRadius);
// Draw or update text under the circle
if (nodeLabels) {
newNodes
.append('text')
.text((node) => node.name)
.attr('x', (node) => -0.8 * nodeRadius(node))
.attr('y', (node) => 4);
if (prevNodeLabels) {
existingNodes
.select('text')
.text((node) => node.name)
.attr('x', (node) => -0.8 * nodeRadius(node))
.attr('y', (node) => 4);
} else {
existingNodes
.append('text')
.text((node) => node.name)
.attr('x', (node) => -0.8 * nodeRadius(node))
.attr('y', (node) => 4);
}
} else {
existingNodes.select('text').remove();
}
}, [nodes, nodeLabels, prevNodeLabels]);
// This effect draws lines between the nodes
useEffect(() => {
function linkClass(link) {
return link ? 'link ' + link.label : 'link root';
}
function linkStyle(link) {
return link ? 'stroke-width:' + link.weight * 5 : 'stroke-width:1';
}
if (!links) return;
// Bind data to links between nodes
const existingLinks = d3Select('.links')
.selectAll('g.link')
.data(links);
const newLinks = existingLinks.enter().append('g');
existingLinks.exit().remove();
// Set the classes for the container
newLinks.attr('class', linkClass);
existingLinks.attr('class', linkClass);
// Draw or update the line between nodes
newLinks.append('path').attr('style', linkStyle);
existingLinks.select('path').attr('style', linkStyle);
// Label the line with the link label
if (edgeLabels) {
newLinks.append('text').text((link) => link.label);
if (prevEdgeLabels) {
existingLinks.select('text').text((link) => link.label);
} else {
existingLinks.append('text').text((link) => link.label);
}
} else {
existingLinks.select('text').remove();
}
}, [links, edgeLabels, prevEdgeLabels]);
return (
<>
<style dangerouslySetInnerHTML={{ __html: view.jsonLayout.styles }} />
<svg width={(width * zoom) / 100} height={(height * zoom) / 100}>
<g className="wrapper" transform={'translate(' + leftMargin + ',' + topMargin + ')'}>
<g className="links"></g>
<g className="nodes"></g>
</g>
</svg>
</>
);
};
export default ForceNetworkDiagram;
解决方案
事实证明,restart()
模拟的方法并没有做到罐头上所说的那样。如果你真的想重新开始你需要调用的模拟restart()
和alpha(1)
.
这对我来说似乎很违反直觉,但至少我的绘画现在按预期工作。更新后的代码是:
var simulation = forceSimulation;
if (!simulation) {
simulation = d3ForceSimulation();
setForceSimulation(simulation);
}
simulation
.nodes(nodes)
.force('charge', d3ForceManyBody())
.force('center', d3ForceCenter(width / 2, height / 2))
.force(
'link',
d3ForceLink(links).strength((link) => link.weight),
)
.force(
'collision',
d3ForceCollide((n) => nodeRadius(n) * (edgeLabels ? 2 : 1.4) * spacing),
)
.on('tick', tick)
.alpha(1)
.restart();
推荐阅读
- python - 在 Python 中从日志文件中读取数据
- flutter - Flutter 圆形 TabBar 边框指示器
- image - Stream Builder 导致无限循环抖动
- python - 如何使用决策树进行生存分析?
- postgresql - 如何在 Liquibase 中为已创建的表添加索引
- sql - 查询以选择接下来的几行
- forms - Python 美丽的汤和请求在帖子 url 上提交表单数据
- reactjs - react native - 如何将参数从一个组件传递到另一个组件?
- git - Git 对两个单独的提交进行更改
- assembly - 如何在 DOSBox 中运行的汇编程序中将文本插入剪贴板?