d3.js - d3 Force Directed Graph General Update Pattern - exit() 选择不起作用
问题描述
我有一个力导向图,其中我的一般更新模式似乎不起作用。特别是当我尝试更新时,未使用的节点/链接不会按预期消失,并且仍在数据中的节点/链接不会转换为新数据。相反,旧图只是冻结并在旧图上绘制新图。
我认为这是我的选择没有正确处理数据的问题,但是我尝试了很多调整并且无法弄清楚我的问题。我试图尽可能地效仿Bostock 的例子,但它没有按预期工作。这是一个运行可视化的代码笔(注意:使用并排编辑器查看可视化最容易)。
提前致谢!
这是代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Updating Graph</title>
<style>
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<button onclick=render(sp)>render new graph</button>
<div class="chart"></div>
<script>
const cb = {
"edges": [{
"source": "a",
"target": "b",
"value": 1
}, {
"source": "a",
"target": "c",
"value": 1
}, {
"source": "b",
"target": "c",
"value": 1
}, {
"source": "c",
"target": "d",
"value": 1
}],
"nodes": [{
"id": "a",
"pop": 12.00328963067508,
"size": 5
}, {
"id": "b",
"pop": 12.391087593534877,
"size": 5
}, {
"id": "c",
"pop": 12.384324067681156,
"size": 5
}, {
"id": "d",
"pop": 13.991090521661292,
"size": 6
}]
}
const sp = {
"edges": [{
"source": "a",
"target": "b",
"value": 1
}, {
"source": "a",
"target": "e",
"value": 1
}, {
"source": "b",
"target": "f",
"value": 1
}, {
"source": "e",
"target": "f",
"value": 1
}],
"nodes": [{
"id": "a",
"pop": 12.00328963067508,
"size": 5
}, {
"id": "b",
"pop": 12.391087593534877,
"size": 5
}, {
"id": "e",
"pop": 13.063656176168433,
"size": 6
}, {
"id": "f",
"pop": 12.52608275807238,
"size": 5
}]
}
// set margins and canvas size
const margin = {
top: 10,
right: 20,
bottom: 30,
left: 30
};
const width = 600 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// set up canvas
const svg = d3.select('.chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.call(responsivefy)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// set up selections
let links = svg.append("g")
.attr("class", "links")
.selectAll("line");
let nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle");
// set up color scale
const color = d3.scaleSequential()
.domain([8, 15])
.interpolator(d3.interpolateInferno);
// set up simulation basic parameters
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
function render(graph) {
// node selection and data handling
let node = nodes
.data(graph.nodes, function(d) {
return d.id;
});
// node general update pattern
node.exit()
.transition()
.attr("r", 0)
.remove();
node = node
.enter().append("circle")
.attr("r", function(d) {
return d.size;
})
.attr("fill", function(d) {
return color(d.pop);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.merge(node);
// give all nodes a title with their id for hover identification
node.append("title")
.text(function(d) {
return d.id;
});
// link selection, data handling
let link = links
.data(graph.edges, function(d) {
return d.source + "-" + d.target;
});
// link general update pattern with attrTween to keep links connected to disappearing nodes
link
.exit()
.transition()
.attr("stroke-opacity", 0)
.attrTween("x1", function(d) {
return function() {
return d.source.x;
};
})
.attrTween("x2", function(d) {
return function() {
return d.target.x;
};
})
.attrTween("y1", function(d) {
return function() {
return d.source.y;
};
})
.attrTween("y2", function(d) {
return function() {
return d.target.y;
};
})
.remove();
link = link
.enter().append("line")
.attr("stroke-width", function(d) {
return Math.sqrt(d.value);
})
.merge(link);
// add nodes and links to the siumlation
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.edges);
// restart the simulation
simulation.alpha(1).restart();
// set the ticked function to constantly update node and link position
function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
};
// initial render
render(cb)
// dragging functions
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// responsivefy from https://brendansudol.com/writing/responsive-d3
function responsivefy(svg) {
// get container + svg aspect ratio
const container = d3.select(svg.node().parentNode),
width = parseInt(svg.style("width")),
height = parseInt(svg.style("height")),
aspect = width / height;
// add viewBox and preserveAspectRatio properties,
// and call resize so that svg resizes on inital page load
svg.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMinYMid")
.call(resize);
// to register multiple listeners for same event type,
// you need to add namespace, i.e., 'click.foo'
// necessary if you call invoke this function for multiple svgs
// api docs: https://github.com/mbostock/d3/wiki/Selections#on
d3.select(window).on("resize." + container.attr("id"), resize);
// get width of container and resize svg to fit it
function resize() {
const targetWidth = parseInt(container.style("width"));
svg.attr("width", targetWidth);
svg.attr("height", Math.round(targetWidth / aspect));
}
}
</script>
</body>
</html>
解决方案
问题是您的选择nodes
是links
空的选择:
// set up selections
let links = svg.append("g")
.attr("class", "links")
.selectAll("line");
let nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle");
由于此时没有圆圈是线,并且 D3 选择是不可变的。这意味着无论何时您调用nodes.data()
或links.data()
输入数据数组中的所有项目,因为选择中没有相应的元素要更新或退出 - 选择保持为空(您可以运行 nodes.size() 每次更新以查看此内容)。
相反,您可以离开links
并nodes
作为 parent 的选择g
:
// set up selections
let links = svg.append("g")
.attr("class", "links");
let nodes = svg.append("g")
.attr("class", "nodes");
并在每次更新时选择所有链接/节点:
let node = nodes.selectAll("circle")
.data(graph.nodes, function(d) {
return d.id;
});
(链接也一样)
这样,您将选择任何现有的链接/圈子,并能够根据需要更新/退出/输入:
const cb = {
"edges": [{
"source": "a",
"target": "b",
"value": 1
}, {
"source": "a",
"target": "c",
"value": 1
}, {
"source": "b",
"target": "c",
"value": 1
}, {
"source": "c",
"target": "d",
"value": 1
}],
"nodes": [{
"id": "a",
"pop": 12.00328963067508,
"size": 5
}, {
"id": "b",
"pop": 12.391087593534877,
"size": 5
}, {
"id": "c",
"pop": 12.384324067681156,
"size": 5
}, {
"id": "d",
"pop": 13.991090521661292,
"size": 6
}]
}
const sp = {
"edges": [{
"source": "a",
"target": "b",
"value": 1
}, {
"source": "a",
"target": "e",
"value": 1
}, {
"source": "b",
"target": "f",
"value": 1
}, {
"source": "e",
"target": "f",
"value": 1
}],
"nodes": [{
"id": "a",
"pop": 12.00328963067508,
"size": 5
}, {
"id": "b",
"pop": 12.391087593534877,
"size": 5
}, {
"id": "e",
"pop": 13.063656176168433,
"size": 6
}, {
"id": "f",
"pop": 12.52608275807238,
"size": 5
}]
}
// set margins and canvas size
const margin = {
top: 10,
right: 20,
bottom: 30,
left: 30
};
const width = 600 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// set up canvas
const svg = d3.select('.chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.call(responsivefy)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// set up selections
let links = svg.append("g")
.attr("class", "links");
let nodes = svg.append("g")
.attr("class", "nodes");
// set up color scale
const color = d3.scaleSequential()
.domain([8, 15])
.interpolator(d3.interpolateInferno);
// set up simulation basic parameters
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
function render(graph) {
// node selection and data handling
let node = nodes.selectAll("circle")
.data(graph.nodes, function(d) {
return d.id;
});
// node general update pattern
node.exit()
.transition()
.attr("r", 0)
.remove();
node = node
.enter().append("circle")
.attr("r", function(d) {
return d.size;
})
.attr("fill", function(d) {
return color(d.pop);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.merge(node);
// give all nodes a title with their id for hover identification
node.append("title")
.text(function(d) {
return d.id;
});
// link selection, data handling
let link = links.selectAll("line")
.data(graph.edges, function(d) {
return d.source + "-" + d.target;
});
// link general update pattern with attrTween to keep links connected to disappearing nodes
link
.exit()
.transition()
.attr("stroke-opacity", 0)
.attrTween("x1", function(d) {
return function() {
return d.source.x;
};
})
.attrTween("x2", function(d) {
return function() {
return d.target.x;
};
})
.attrTween("y1", function(d) {
return function() {
return d.source.y;
};
})
.attrTween("y2", function(d) {
return function() {
return d.target.y;
};
})
.remove();
link = link
.enter().append("line")
.attr("stroke-width", function(d) {
return Math.sqrt(d.value);
})
.merge(link);
// add nodes and links to the siumlation
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.edges);
// restart the simulation
simulation.alpha(1).restart();
// set the ticked function to constantly update node and link position
function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
};
// initial render
render(cb)
// dragging functions
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// responsivefy from https://brendansudol.com/writing/responsive-d3
function responsivefy(svg) {
// get container + svg aspect ratio
const container = d3.select(svg.node().parentNode),
width = parseInt(svg.style("width")),
height = parseInt(svg.style("height")),
aspect = width / height;
// add viewBox and preserveAspectRatio properties,
// and call resize so that svg resizes on inital page load
svg.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMinYMid")
.call(resize);
// to register multiple listeners for same event type,
// you need to add namespace, i.e., 'click.foo'
// necessary if you call invoke this function for multiple svgs
// api docs: https://github.com/mbostock/d3/wiki/Selections#on
d3.select(window).on("resize." + container.attr("id"), resize);
// get width of container and resize svg to fit it
function resize() {
const targetWidth = parseInt(container.style("width"));
svg.attr("width", targetWidth);
svg.attr("height", Math.round(targetWidth / aspect));
}
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button onclick=render(sp)>render new graph</button>
<div class="chart"></div>
或者,更新的笔
推荐阅读
- tensorflow-quantum - tfq.layers.PQC 中的quantum_data 是什么?
- vue.js - Vue 将任何标签中的“open”属性替换为“open”值
- stata - Twoway estpost tabstat-esttab:保留变量标签
- c# - Azure Blob 在同一容器 C# 中移动文件夹
- javascript - UnhandledPromiseRejectionWarning:TypeError:无法读取未定义的属性“数据”
- angular - 如果我已经添加了 formControlName,是否需要在输入中添加名称?
- html - HTML 导航栏下拉菜单在尝试使其具有粘性时变得无响应
- javascript - 如何为html元素的属性设置文本节点类型的值
- java - 将字符串计算为双精度(后缀符号)
- list - 将 https 请求带入对象