I would like to add a background rectangle to group 2, the idea is add a g element and append all group 2 nodes to the g element, then use g element bbox to draw a rectangle.
But I don't know how to move exist nodes to g element! (Maybe not possible?).
Example code as below:
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>
The force simulation doesn't use the DOM for anything. It merely calculates where nodes should be, how you render them, if you render them, is up to you. So putting some nodes in a
gbut not others is not a problem. For example, we could add agfor group 2, run through all the nodes, detach them from the DOM if they are from group 2 and reappend them to the newg:Then all we need to do is create a background rectangle:
And update it on tick with a new bounding box of the
g, as shown below.If you wanted more than one group, or had dynamic data, this approach isn't ideal - the join or the data structure would need to be modified a bit to make a more canonical approach work - I might revisit it later tonight with an alternative. As is, this solution is likely the least invasive with respect to your existing code.