Voronoi 单元在力导向布局中超出比例
Voronoi cells are off-scale in a force-directed layout
我有一个可折叠的力导向布局,我从 here 中引用了它。
我修改了代码,在图中的每个节点后面添加了 Voronoi 单元。目前,它看起来像是在工作,因为我可以看到基于折叠状态的 Voronoi 单元 appear/disappear。然而,它是超标的。
我认为它与这条特定的线有关
node.select("path")
.data(voronoi.polygons(nodes))
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
但我有点不知道该怎么办。
任何人都可以提供有关为什么 voronoi 细胞不按比例缩放的指示吗?我如何确保它们以节点为中心并正确缩放?
问题出在这里:
node.attr('transform', function(d) { return `translate(${d.x}, ${d.y})`; });
如您所见,您正在对 node
组应用翻译。这本身不是问题。问题是那些相同的组包含 Voronoi 路径。因此,当您稍后更改 Voronoi 坐标时...
node.select("path")
.data(voronoi.polygons(nodes))
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z";
});
...您在已翻译的组内的路径上应用坐标。
解决方案:有很多不同的解决方案,比如为 Voronoi 路径创建不同的组。但是,这里最简单的解决方案是将翻译应用于圆圈,而不是包含组:
node.select("circle").attr('transform', function(d) {
return `translate(${d.x}, ${d.y})`;
});
这是您的代码,仅进行了更改:
const svg = d3.select('#voronoiSvg');
const transform = d3.zoomIdentity;
const width = 600;
const height = 300;
let node, link, simulation, root, voronoi, nodes;
let i = 0;
function loadVoronoi() {
const data = {
"name": "flare",
"children": [{
"name": "analytics",
"children": [{
"name": "cluster",
"children": [{
"name": "AgglomerativeCluster",
"size": 3938
},
{
"name": "CommunityStructure",
"size": 3812
},
{
"name": "HierarchicalCluster",
"size": 6714
},
{
"name": "MergeEdge",
"size": 743
}
]
},
{
"name": "graph",
"children": [{
"name": "BetweennessCentrality",
"size": 3534
},
{
"name": "LinkDistance",
"size": 5731
},
{
"name": "MaxFlowMinCut",
"size": 7840
},
{
"name": "ShortestPaths",
"size": 5914
},
{
"name": "SpanningTree",
"size": 3416
}
]
},
{
"name": "optimization",
"children": [{
"name": "AspectRatioBanker",
"size": 7074
}]
}
]
}]
}
root = d3.hierarchy(data);
svg
.attr("width", width)
.attr("height", height)
.on('dblclick.zoom', null);
simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(function(d) {
return d.id;
}))
.force('charge', d3.forceManyBody().strength(-10).distanceMax(300))
.force('center', d3.forceCenter(width / 2, height / 2))
.alphaTarget(1)
.on('tick', ticked);
voronoi = d3.voronoi()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.extent([
[-1, 1],
[width + 1, height + 1]
]);
update(root);
}
function update(root) {
nodes = flatten(root);
const links = root.links();
//links
link = svg.selectAll('.link')
.data(links, function(d) {
return d.target.id;
});
link.exit().remove();
const linkEnter = link.enter()
.append('line')
.attr('class', 'link')
.style('stroke', '#132')
.style('opacity', '1')
.style('stroke-width', 2);
link = linkEnter.merge(link);
//nodes
node = svg.selectAll('.node')
.data(nodes, function(d) {
return d.id;
});
node.exit().remove();
const nodeEnter = node.enter()
.append('g')
.attr('class', 'node')
.attr('stroke', '#666')
.attr('stroke-width', 2)
.style('fill', color)
.style('opacity', 1)
.on('click', clicked);
nodeEnter.append('circle')
.attr("r", 8)
.style('text-anchor', function(d) {
return d.children ? 'end' : 'start';
})
.text(function(d) {
return d.data.name;
});
nodeEnter.append("path").attr("class", "path");
nodeEnter.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node = nodeEnter.merge(node);
simulation.nodes(nodes);
simulation.force('link').links(links);
// simulation.alpha(1).restart();
}
function color(d) {
return d._children ? "#51A1DC" // collapsed package
:
d.children ? "#51A1DC" // expanded package
:
"#F94B4C"; // leaf node
}
function radius(d) {
return d._children ? 8 :
d.children ? 8 :
4;
}
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.select("circle").attr('transform', function(d) {
return `translate(${d.x}, ${d.y})`;
});
node.select("path")
.data(voronoi.polygons(nodes))
.attr("d", function(d) {
return d == null ? null : "M" + d.join("L") + "Z";
});
}
function clicked(d) {
if (!d3.event.defaultPrevented) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
console.log("clicked");
update(root);
}
}
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;
}
function flatten(root) {
const nodes = [];
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
else
++i;
nodes.push(node);
}
recurse(root);
return nodes;
}
function zoomed() {
svg.attr('transform', d3.event.transform);
}
loadVoronoi();
.node circle {
cursor: pointer;
stroke: #3182bd;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
pointer-events: none;
text-anchor: middle;
}
line.link {
fill: none;
stroke: #9ecae1;
stroke-width: 1.5px;
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active path {
fill: #111;
opacity: 0.05;
}
svg {
border: 1px solid #888;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="list-group voronoi_treemap" id="voronoiPanel">
<svg id="voronoiSvg"></svg>
</div>
我有一个可折叠的力导向布局,我从 here 中引用了它。
我修改了代码,在图中的每个节点后面添加了 Voronoi 单元。目前,它看起来像是在工作,因为我可以看到基于折叠状态的 Voronoi 单元 appear/disappear。然而,它是超标的。
我认为它与这条特定的线有关
node.select("path")
.data(voronoi.polygons(nodes))
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
但我有点不知道该怎么办。 任何人都可以提供有关为什么 voronoi 细胞不按比例缩放的指示吗?我如何确保它们以节点为中心并正确缩放?
问题出在这里:
node.attr('transform', function(d) { return `translate(${d.x}, ${d.y})`; });
如您所见,您正在对 node
组应用翻译。这本身不是问题。问题是那些相同的组包含 Voronoi 路径。因此,当您稍后更改 Voronoi 坐标时...
node.select("path")
.data(voronoi.polygons(nodes))
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z";
});
...您在已翻译的组内的路径上应用坐标。
解决方案:有很多不同的解决方案,比如为 Voronoi 路径创建不同的组。但是,这里最简单的解决方案是将翻译应用于圆圈,而不是包含组:
node.select("circle").attr('transform', function(d) {
return `translate(${d.x}, ${d.y})`;
});
这是您的代码,仅进行了更改:
const svg = d3.select('#voronoiSvg');
const transform = d3.zoomIdentity;
const width = 600;
const height = 300;
let node, link, simulation, root, voronoi, nodes;
let i = 0;
function loadVoronoi() {
const data = {
"name": "flare",
"children": [{
"name": "analytics",
"children": [{
"name": "cluster",
"children": [{
"name": "AgglomerativeCluster",
"size": 3938
},
{
"name": "CommunityStructure",
"size": 3812
},
{
"name": "HierarchicalCluster",
"size": 6714
},
{
"name": "MergeEdge",
"size": 743
}
]
},
{
"name": "graph",
"children": [{
"name": "BetweennessCentrality",
"size": 3534
},
{
"name": "LinkDistance",
"size": 5731
},
{
"name": "MaxFlowMinCut",
"size": 7840
},
{
"name": "ShortestPaths",
"size": 5914
},
{
"name": "SpanningTree",
"size": 3416
}
]
},
{
"name": "optimization",
"children": [{
"name": "AspectRatioBanker",
"size": 7074
}]
}
]
}]
}
root = d3.hierarchy(data);
svg
.attr("width", width)
.attr("height", height)
.on('dblclick.zoom', null);
simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(function(d) {
return d.id;
}))
.force('charge', d3.forceManyBody().strength(-10).distanceMax(300))
.force('center', d3.forceCenter(width / 2, height / 2))
.alphaTarget(1)
.on('tick', ticked);
voronoi = d3.voronoi()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.extent([
[-1, 1],
[width + 1, height + 1]
]);
update(root);
}
function update(root) {
nodes = flatten(root);
const links = root.links();
//links
link = svg.selectAll('.link')
.data(links, function(d) {
return d.target.id;
});
link.exit().remove();
const linkEnter = link.enter()
.append('line')
.attr('class', 'link')
.style('stroke', '#132')
.style('opacity', '1')
.style('stroke-width', 2);
link = linkEnter.merge(link);
//nodes
node = svg.selectAll('.node')
.data(nodes, function(d) {
return d.id;
});
node.exit().remove();
const nodeEnter = node.enter()
.append('g')
.attr('class', 'node')
.attr('stroke', '#666')
.attr('stroke-width', 2)
.style('fill', color)
.style('opacity', 1)
.on('click', clicked);
nodeEnter.append('circle')
.attr("r", 8)
.style('text-anchor', function(d) {
return d.children ? 'end' : 'start';
})
.text(function(d) {
return d.data.name;
});
nodeEnter.append("path").attr("class", "path");
nodeEnter.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node = nodeEnter.merge(node);
simulation.nodes(nodes);
simulation.force('link').links(links);
// simulation.alpha(1).restart();
}
function color(d) {
return d._children ? "#51A1DC" // collapsed package
:
d.children ? "#51A1DC" // expanded package
:
"#F94B4C"; // leaf node
}
function radius(d) {
return d._children ? 8 :
d.children ? 8 :
4;
}
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.select("circle").attr('transform', function(d) {
return `translate(${d.x}, ${d.y})`;
});
node.select("path")
.data(voronoi.polygons(nodes))
.attr("d", function(d) {
return d == null ? null : "M" + d.join("L") + "Z";
});
}
function clicked(d) {
if (!d3.event.defaultPrevented) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
console.log("clicked");
update(root);
}
}
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;
}
function flatten(root) {
const nodes = [];
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
else
++i;
nodes.push(node);
}
recurse(root);
return nodes;
}
function zoomed() {
svg.attr('transform', d3.event.transform);
}
loadVoronoi();
.node circle {
cursor: pointer;
stroke: #3182bd;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
pointer-events: none;
text-anchor: middle;
}
line.link {
fill: none;
stroke: #9ecae1;
stroke-width: 1.5px;
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active path {
fill: #111;
opacity: 0.05;
}
svg {
border: 1px solid #888;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="list-group voronoi_treemap" id="voronoiPanel">
<svg id="voronoiSvg"></svg>
</div>