我正在使用强制导向图,其中我的一般更新模式似乎不起作用。特别是当我尝试更新时,未使用的节点/链接不会按预期消失,并且仍在数据中的节点/链接不会转换为新数据。相反,旧图只是冻结,并在旧图上绘制新图。
我认为这是一个问题,我的选择没有正确处理数据,但我尝试了很多调整,但无法弄清楚我的问题。我尽可能地密切关注Bostock's example,但它没有按预期工作。运行可视化的Here's a codepen(注意:最简单的方法是使用并排编辑器查看可视化)。
提前致谢!
以下是代码:
<!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>
答案 0 :(得分:1)
问题在于您的选择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()
,都会输入数据数组中的所有项目,因为选择中没有相应的元素可以更新或退出 - 选择仍为空(您可以运行节点。 size()每次更新都能看到这个)。
相反,您可以将links
和nodes
作为父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));
}
}
&#13;
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
&#13;
<script src="https://d3js.org/d3.v4.min.js"></script>
<button onclick=render(sp)>render new graph</button>
<div class="chart"></div>
&#13;
或者,更新了pen