如何在 d3.js 中同时自动移动节点和链接
How to automatically move nodes and links at the same time in d3.js
我正在尝试 d3.js(v7) 中的 force-directed example。
在此示例中,当我拖动一个节点时,链接和其他节点会一前一后地移动。
我希望所有节点始终随机移动,并且我希望其他链接和节点与它们一起移动,就像我拖动它们一样。
代码如下。 json 文件与示例相同。当我运行此代码时,节点移动,但链接不跟随移动并保持静止。
function ForceGraph({
nodes, // an iterable of node objects (typically [{id}, …])
links, // an iterable of link objects (typically [{source, target}, …])
}, {
nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
nodeGroup, // given d in nodes, returns an (ordinal) value for color
nodeGroups, // an array of ordinal values representing the node groups
nodeStrength,
linkSource = ({source}) => source, // given d in links, returns a node identifier string
linkTarget = ({target}) => target, // given d in links, returns a node identifier string
linkStrokeWidth = 10, // given d in links, returns a stroke width in pixels
linkStrength = 0.55,
colors = d3.schemeTableau10, // an array of color strings, for the node groups
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
invalidation // when this promise resolves, stop the simulation
} = {}) {
// Compute values.
const N = d3.map(nodes, nodeId).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({id: N[i], type: NODETYPES[i], tag: parsed_NODETAGS[i], texts: X[i]}));
links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));
// Compute default domains.
if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
// Construct the scales.
const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
// Construct the forces.
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);
const zoom = d3.zoom()
.scaleExtent([1, 40])
.on("zoom", zoomed);
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2.5, width, height])
.on("click", reset)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.call(zoom);
const g = svg.append("g");
const link = g.append("g")
.selectAll("line")
.data(links)
.join("line");
const simulation = d3.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter())
.on("tick", ticked);
const node = g.append("g")
.attr("class", "nodes")
.style("opacity", 1.0)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.call(drag(simulation));
if (W) link.attr("stroke-width", ({index: i}) => W[i]);
if (G) node.attr("fill", ({index: i}) => color(G[i]));
function random(){
node
.transition()
.duration(2000)
.attr("cx", function(d){
return d.x + Math.random()*80 - 40;
})
.attr("cy", function(d){
return d.y + Math.random()*80 - 40;
});
}
setInterval(random, 800);
if (invalidation != null) invalidation.then(() => simulation.stop());
function intern(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
}
function ticked() {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
function zoomed({transform}) {
g.attr("transform", transform);
}
return Object.assign(svg.node(), {});
}
在您的函数 random()
中,您不更改基础数据,只更改其表示方式。每个 circle
都包含对 nodes
数组中一个元素的引用,但是你在 random()
中设置了 cx
和 cy
,你没有更新基础数据d.x
和 d.y
。即便如此,圆圈对 cx
和 cy
的值也不是反应性的。也就是说,当 d.x
或 d.y
发生变化时,它们不会重新计算。
所以我会拆分你的代码。有一个函数 random()
,每 800 毫秒调用一次,并通过更改 d.x
和 d.y
来稍微打乱节点。然后 simulation
负责实际绘制圆圈和链接 - 就像它已经在做的那样。
const size = 500;
const nodes = [{
id: 'A',
x: 150,
y: 150
},
{
id: 'B',
x: 250,
y: 250
},
{
id: 'C',
x: 350,
y: 350
}
];
const links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[0],
target: nodes[2]
}
];
const svg = d3.select('body')
.append('svg')
.attr('width', size)
.attr('height', size)
.attr('border', 'solid 1px red')
const g = svg.append('g');
const node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5);
const link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.join("line");
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).strength(2))
.force("charge", d3.forceManyBody().strength(2))
.force("center", d3.forceCenter(size / 2, size / 2).strength(0.05))
.on("tick", ticked);
function ticked() {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function random() {
simulation.stop();
nodes.forEach(d => {
d.x += Math.random() * 80 - 40;
d.y += Math.random() * 80 - 40;
});
node
.transition(1000)
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.transition(1000)
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)
.on('end', () => { simulation.restart(); });
}
setInterval(random, 2000);
.links>line {
stroke: black;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js"></script>
我正在尝试 d3.js(v7) 中的 force-directed example。 在此示例中,当我拖动一个节点时,链接和其他节点会一前一后地移动。 我希望所有节点始终随机移动,并且我希望其他链接和节点与它们一起移动,就像我拖动它们一样。 代码如下。 json 文件与示例相同。当我运行此代码时,节点移动,但链接不跟随移动并保持静止。
function ForceGraph({
nodes, // an iterable of node objects (typically [{id}, …])
links, // an iterable of link objects (typically [{source, target}, …])
}, {
nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
nodeGroup, // given d in nodes, returns an (ordinal) value for color
nodeGroups, // an array of ordinal values representing the node groups
nodeStrength,
linkSource = ({source}) => source, // given d in links, returns a node identifier string
linkTarget = ({target}) => target, // given d in links, returns a node identifier string
linkStrokeWidth = 10, // given d in links, returns a stroke width in pixels
linkStrength = 0.55,
colors = d3.schemeTableau10, // an array of color strings, for the node groups
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
invalidation // when this promise resolves, stop the simulation
} = {}) {
// Compute values.
const N = d3.map(nodes, nodeId).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({id: N[i], type: NODETYPES[i], tag: parsed_NODETAGS[i], texts: X[i]}));
links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));
// Compute default domains.
if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
// Construct the scales.
const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
// Construct the forces.
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);
const zoom = d3.zoom()
.scaleExtent([1, 40])
.on("zoom", zoomed);
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2.5, width, height])
.on("click", reset)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.call(zoom);
const g = svg.append("g");
const link = g.append("g")
.selectAll("line")
.data(links)
.join("line");
const simulation = d3.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter())
.on("tick", ticked);
const node = g.append("g")
.attr("class", "nodes")
.style("opacity", 1.0)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.call(drag(simulation));
if (W) link.attr("stroke-width", ({index: i}) => W[i]);
if (G) node.attr("fill", ({index: i}) => color(G[i]));
function random(){
node
.transition()
.duration(2000)
.attr("cx", function(d){
return d.x + Math.random()*80 - 40;
})
.attr("cy", function(d){
return d.y + Math.random()*80 - 40;
});
}
setInterval(random, 800);
if (invalidation != null) invalidation.then(() => simulation.stop());
function intern(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
}
function ticked() {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
function zoomed({transform}) {
g.attr("transform", transform);
}
return Object.assign(svg.node(), {});
}
在您的函数 random()
中,您不更改基础数据,只更改其表示方式。每个 circle
都包含对 nodes
数组中一个元素的引用,但是你在 random()
中设置了 cx
和 cy
,你没有更新基础数据d.x
和 d.y
。即便如此,圆圈对 cx
和 cy
的值也不是反应性的。也就是说,当 d.x
或 d.y
发生变化时,它们不会重新计算。
所以我会拆分你的代码。有一个函数 random()
,每 800 毫秒调用一次,并通过更改 d.x
和 d.y
来稍微打乱节点。然后 simulation
负责实际绘制圆圈和链接 - 就像它已经在做的那样。
const size = 500;
const nodes = [{
id: 'A',
x: 150,
y: 150
},
{
id: 'B',
x: 250,
y: 250
},
{
id: 'C',
x: 350,
y: 350
}
];
const links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[0],
target: nodes[2]
}
];
const svg = d3.select('body')
.append('svg')
.attr('width', size)
.attr('height', size)
.attr('border', 'solid 1px red')
const g = svg.append('g');
const node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5);
const link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.join("line");
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).strength(2))
.force("charge", d3.forceManyBody().strength(2))
.force("center", d3.forceCenter(size / 2, size / 2).strength(0.05))
.on("tick", ticked);
function ticked() {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function random() {
simulation.stop();
nodes.forEach(d => {
d.x += Math.random() * 80 - 40;
d.y += Math.random() * 80 - 40;
});
node
.transition(1000)
.attr("cx", d => d.x)
.attr("cy", d => d.y);
link
.transition(1000)
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)
.on('end', () => { simulation.restart(); });
}
setInterval(random, 2000);
.links>line {
stroke: black;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js"></script>