d3.js - d3.js - 在力有向图组上添加背景矩形
问题描述
我想在第 2 组中添加一个背景矩形,想法是添加 ag 元素并将所有第 2 组节点附加到 g 元素,然后使用 g 元素 bbox 绘制一个矩形。
但我不知道如何将现有节点移动到 g 元素!(也许不可能?)。
示例代码如下:
var graph = {
nodes:[
{id: "A",name:'AAAA', group: 1},
{id: "B", name:'BBBB',group: 2},
{id: "C", name:'CCCC',group: 2},
{id: "D", name:'DDDD',group: 2},
{id: "E", name:'EEEE',group: 2},
{id: "F", name:'FFFF',group: 3},
{id: "G", name:'GGGG',group: 3},
{id: "H", name:'HHHH',group: 3},
{id: "I", name:'IIII',group: 3}
],
links:[
{source: "A", target: "B", value: 1},
{source: "A", target: "C", value: 1},
{source: "A", target: "D", value: 1},
{source: "A", target: "E", value: 1},
{source: "A", target: "F", value: 1},
{source: "A", target: "G", value: 1},
{source: "A", target: "H", value: 1},
{source: "A", target: "I", value: 1},
]
};
var width = 400
var height = 200
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
.style('border','1px solid red')
var color = d3.scaleOrdinal(d3.schemeCategory10);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(100))
.force("charge", d3.forceManyBody())
.force("x", d3.forceX(function(d){
if(d.group === 2){
return width/3
} else if (d.group === 3){
return 2*width/3
} else {
return width/2
}
}))
.force("y", d3.forceY(height/2))
.force("center", d3.forceCenter(width / 2, height / 2));
var g = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
var w = 80
var txts = g.append('text')
.attr('class','text')
.attr('text-anchor','middle')
.attr("dominant-baseline", "central")
.attr('fill','black')
.text(d => d.name)
.each((d,i,n) => {
var bbox = d3.select(n[i]).node().getBBox()
var margin = 4
bbox.x -= margin
bbox.y -= margin
bbox.width += 2*margin
bbox.height += 2*margin
if (bbox.width < w) {
bbox.width = w
}
d.bbox = bbox
})
var node = g
.insert('rect','text')
.attr('stroke','black')
.attr('width', d => d.bbox.width)
.attr('height',d => d.bbox.height)
.attr("fill", function(d) { return color(d.group); })
.attr('fill-opacity',0.3)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var link = svg.append("g")
.attr("class", "links")
.attr('stroke','black')
.selectAll("line")
.data(graph.links)
.enter().append("path")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
link
.attr("d", function(d) {
var ax = d.source.x
var ay = d.source.y
var bx = d.target.x
var by = d.target.y
if (bx < ax) {
ax -= w/2
bx += w/2
}else{
ax += w/2
bx -= w/2
}
var path = ['M',ax,ay,'L',bx,by]
return path.join(' ')
})
txts.attr('x',d => d.x)
.attr('y',d => d.y)
node
.attr("x", function(d) { return d.x - d.bbox.width/2; })
.attr("y", function(d) { return d.y - d.bbox.height/2; });
}
function dragstarted(event,d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event,d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
解决方案
力模拟不使用 DOM 做任何事情。它只是计算节点应该在哪里,如何渲染它们,如果你渲染它们,取决于你。因此,将一些节点放入 ag
而不是其他节点不是问题。例如,我们可以g
为组 2 添加一个,遍历所有节点,如果它们来自组 2,则将它们与 DOM 分离,然后将它们重新附加到新的g
:
var parent = d3.select("g").append("g").lower();
node.each(function(d) {
if (d.group == 2) {
d3.select(this).remove();
parent.append((d)=>this);
}
})
然后我们需要做的就是创建一个背景矩形:
var background = d3.select("g")
.append("rect")
.lower() // so it is behind the nodes.
....
并使用 的新边界框更新它g
,如下所示。
var graph = {
nodes:[
{id: "A",name:'AAAA', group: 1},
{id: "B", name:'BBBB',group: 2},
{id: "C", name:'CCCC',group: 2},
{id: "D", name:'DDDD',group: 2},
{id: "E", name:'EEEE',group: 2},
{id: "F", name:'FFFF',group: 3},
{id: "G", name:'GGGG',group: 3},
{id: "H", name:'HHHH',group: 3},
{id: "I", name:'IIII',group: 3}
],
links:[
{source: "A", target: "B", value: 1},
{source: "A", target: "C", value: 1},
{source: "A", target: "D", value: 1},
{source: "A", target: "E", value: 1},
{source: "A", target: "F", value: 1},
{source: "A", target: "G", value: 1},
{source: "A", target: "H", value: 1},
{source: "A", target: "I", value: 1},
]
};
var width = 400
var height = 200
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
.style('border','1px solid red')
var color = d3.scaleOrdinal(d3.schemeCategory10);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(100))
.force("charge", d3.forceManyBody())
.force("x", d3.forceX(function(d){
if(d.group === 2){
return width/3
} else if (d.group === 3){
return 2*width/3
} else {
return width/2
}
}))
.force("y", d3.forceY(height/2))
.force("center", d3.forceCenter(width / 2, height / 2));
var g = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
var w = 80
var txts = g.append('text')
.attr('class','text')
.attr('text-anchor','middle')
.attr("dominant-baseline", "central")
.attr('fill','black')
.text(d => d.name)
.each((d,i,n) => {
var bbox = d3.select(n[i]).node().getBBox()
var margin = 4
bbox.x -= margin
bbox.y -= margin
bbox.width += 2*margin
bbox.height += 2*margin
if (bbox.width < w) {
bbox.width = w
}
d.bbox = bbox
})
var node = g
.insert('rect','text')
.attr('stroke','black')
.attr('width', d => d.bbox.width)
.attr('height',d => d.bbox.height)
.attr("fill", function(d) { return color(d.group); })
.attr('fill-opacity',0.3)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Start Changes 1/2
var parent = d3.select("g").append("g").lower();
node.each(function(d) {
if (d.group == 2) {
d3.select(this).remove();
parent.append((d)=>this);
}
})
var background = d3.select("g")
.append("rect")
.lower()
.attr("ry", 5)
.attr("rx", 5)
.attr("fill","#ccc")
.attr("stroke","#999")
.attr("stroke-width", 1);
// End Changes 1/2
var link = svg.append("g")
.attr("class", "links")
.attr('stroke','black')
.selectAll("line")
.data(graph.links)
.enter().append("path")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
link
.attr("d", function(d) {
var ax = d.source.x
var ay = d.source.y
var bx = d.target.x
var by = d.target.y
if (bx < ax) {
ax -= w/2
bx += w/2
}else{
ax += w/2
bx -= w/2
}
var path = ['M',ax,ay,'L',bx,by]
return path.join(' ')
})
txts.attr('x',d => d.x)
.attr('y',d => d.y)
node
.attr("x", function(d) { return d.x - d.bbox.width/2; })
.attr("y", function(d) { return d.y - d.bbox.height/2; });
// Start changes 2/2
var box = parent.node().getBBox()
background.attr("width", box.width+10)
.attr("height",box.height+10)
.attr("x", box.x-5)
.attr("y", box.y-5);
//End Changes 2/2
}
function dragstarted(event,d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event,d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.js"></script>
如果您想要多个组,或者有动态数据,那么这种方法并不理想 - 连接或数据结构需要稍微修改以使更规范的方法工作 -我可能会在今晚晚些时候用另一种方法重新审视它. 事实上,这个解决方案可能是对您现有代码的侵入性最小的。
推荐阅读
- c++ - 在构造另一个对象期间存储对象名称
- javascript - 尝试将 base64 img 加载到画布中
- rxjs - 您将如何订阅史诗?
- vue.js - 如何在 vue 实例中调用(vanilla)websocket.onmessage 方法
- javascript - 原始图像损坏时显示默认图像
- ios - 使用深度链接分享用户资料
- azure - 您可以使用现有的 JSON 克隆管道吗?
- java - 数据不正确时的 Java showInputDialog
- c# - UWP C# TryGetItemAsync - 呼叫第三次挂起
- python - Pandas Series.apply - 使用另一个系列的参数?