我正在尝试构建一种力的布局,使我可以可视化系统中对象的流动。
我想显示在特定状态下有多少个对象,以及当状态更改时我想更新我的图。
我已经built a prototype,但是我注意到D3.js正在重新计算每个节点的变换,即使它们不需要移动:
可以解决吗?也许有一个选项可以为更新添加最小值?
我以这种方式声明了强制布局:
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
将alphaTarget
更改为alpha
后,重新计算停止了,但是又出现了另一个错误:
我添加了拖动功能,但它停止了上述更改。
Here是具有固定重新计算但存在拖动问题的版本。
答案 0 :(得分:3)
罪魁祸首在您的restart()
函数中:
force.alphaTarget(0.3).restart();
通过设置.alphaTarget(0.3)
重新加热模拟的方式不正确。 alphaTarget
是一个配置参数,用于控制alpha
减小的方式。形象地说,只要alpha
大于alphaMin
,就朝着alphaTarget
前进。系统中的热量由alpha
测量,可以认为是动态数据。另一方面,alphaTarget
类似于或多或少的静态数据。
此外,将alphaTarget
设置为小于alphaMin
的值非常重要,否则您的模拟将无限期地运行,因为alpha
正在朝{{1 }}可以说永远不会小于alphaTarget
。
因此,如果要重新加热系统,则必须操纵alphaMin
而不是alpha
。将上面提到的行更改为以下内容即可获得所需的效果。
alphaTarget
看看下面的代码片段,它实际上是您的JSFiddle的一个分支,以了解它的实际作用。
force.alpha(0.3).restart();
document.getElementById("a").addEventListener("click", function() {
AddNewLink(null, 1);
});
document.getElementById("b").addEventListener("click", function() {
AddNewLink(1, 2);
});
document.getElementById("c").addEventListener("click", function() {
AddNewLink(2, 3);
});
document.getElementById("d").addEventListener("click", function() {
AddNewLink(1, 3);
});
document.getElementById("e").addEventListener("click", function() {
AddNewLink(3, 4);
});
document.getElementById("f").addEventListener("click", function() {
AddNewLink(4, 5);
});
function AddNewLink(from, to) {
var startNode;
var start = availableNodes.find(x => x.id === from);
if (start !== undefined) {
//check if this node is already added
var foundStart = nodes.find(x => x.id == start.id);
if (foundStart === undefined) {
nodes.push(start);
startNode = start;
} else {
foundStart.value--;
if (foundStart.value < 0) foundStart.value = 0;
startNode = foundStart;
}
}
var endNode;
var end = availableNodes.find(x => x.id === to);
if (end !== undefined) {
//check if this node is already added
var foundEnd = nodes.find(x => x.id == end.id);
if (foundEnd === undefined) {
nodes.push(end);
endNode = end;
end.value++;
} else {
foundEnd.value++;
endNode = foundEnd;
}
}
//console.log(startNode, endNode);
if (startNode !== undefined && endNode !== undefined) {
links.push({
source: startNode,
target: endNode
});
}
restart();
}
// set up SVG for D3
const width = 400;
const height = 400;
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select('svg')
.on('contextmenu', () => {
d3.event.preventDefault();
})
.attr('width', width)
.attr('height', height);
var availableNodes = [{
id: 1,
name: "Start",
value: 0,
reflexive: false
}, {
id: 2,
name: "Node 1",
value: 0,
reflexive: false
}, {
id: 3,
name: "Node 2",
value: 0,
reflexive: false
}, {
id: 4,
name: "Node 3",
value: 0,
reflexive: false
}, {
id: 5,
name: "Finish",
value: 0,
reflexive: false
}];
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black circle).
// - links are always source < target; edge directions are set by 'left' and 'right'.
let nodes = [
availableNodes[0], availableNodes[1], availableNodes[2]
];
let links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[1],
target: nodes[2]
}
];
// init D3 force layout
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// handles to link and node element groups
let path = svg.append('svg:g').attr('id', 'lines').selectAll('path');
let circle = svg.append('svg:g').attr('id', 'circles').selectAll('g');
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.attr('d', (d) => {
const deltaX = d.target.x - d.source.x;
const deltaY = d.target.y - d.source.y;
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const normX = deltaX / dist;
const normY = deltaY / dist;
const sourcePadding = d.left ? 17 : 12;
const targetPadding = d.right ? 17 : 12;
const sourceX = d.source.x + (sourcePadding * normX);
const sourceY = d.source.y + (sourcePadding * normY);
const targetX = d.target.x - (targetPadding * normX);
const targetY = d.target.y - (targetPadding * normY);
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
});
circle.attr('transform', (d) => `translate(${d.x},${d.y})`);
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// remove old links
path.exit().remove();
// add new links
path = path.enter().append('svg:path')
.attr('class', 'link')
.style('marker-end', 'url(#end-arrow)')
.merge(path);
// circle (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
circle = circle.data(nodes, (d) => d.id);
// update existing nodes (reflexive & selected visual states)
circle.selectAll('circle')
.style('fill', (d) => colors(d.id))
.classed('reflexive', (d) => d.reflexive);
circle.selectAll('text.value').text((d) => d.value);
// remove old nodes
circle.exit().remove();
// add new nodes
const g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.style('fill', (d) => colors(d.id))
.style('stroke', (d) => d3.rgb(colors(d.id)).darker().toString())
.classed('reflexive', (d) => d.reflexive)
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'value')
.text((d) => d.value);
g.append('svg:text')
.attr('x', 20)
.attr('y', 4)
.attr('class', 'name')
.text((d) => d.name);
circle = g.merge(circle);
// set the graph in motion
force
.nodes(nodes)
.force('link').links(links);
force.alpha(0.3).restart();
}
restart();
svg {
background-color: #FFF;
cursor: default;
user-select: none;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 3px;
cursor: default;
}
path.link.selected {
stroke-dasharray: 10, 2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
circle.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.value {
text-anchor: middle;
font-weight: bold;
}