I create a force directed layout graph. I then create a composite node. The composite node looks like:
I would like to create an edge between one of the circle nodes to the composite node. I would like the composite node to participate in the force directed layout.
How can I do this?
I figure I need to merge nodes and labelNodes together and pass them into .forceSimulation(nodes). However, I am not sure how to do that.
I am sure this is straightforward, but I am getting lost in the documentation and am missing some key concept that would point me to the solution.
//
// Create the force directed graph
//
const width = 1000
const height = 400
const node_data = Array.from({ length: 5 }, () => ({
group: Math.floor(Math.random() * 3),
}))
const edge_data = Array.from({ length: 10 }, () => ({
source: Math.floor(Math.random() * 5),
target: Math.floor(Math.random() * 5),
value: Math.floor(Math.random() * 10) + 1,
}))
const links = edge_data.map((d) => ({ ...d }))
const nodes = node_data.map((d, index) => ({ id: index, ...d }))
const color = d3.scaleOrdinal(d3.schemeCategory10)
const svg = d3.select('#chart')
const simulation = d3
.forceSimulation(nodes)
.force(
'link',
d3
.forceLink(links)
.id((d) => d.id)
.distance((d) => 100)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked)
const link = svg
.append('g')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.selectAll()
.data(links)
.join('line')
.attr('stroke-width', (d) => Math.sqrt(d.value))
const node = svg
.append('g')
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.selectAll()
.data(nodes)
.join('circle')
.attr('r', 16)
.attr('fill', (d) => color(d.group))
node.append('title').text((d) => `hello ${d.id}`)
function ticked() {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
}
node.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
}
function dragged(event) {
event.subject.fx = event.x
event.subject.fy = event.y
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
}
//
// Create the composite node
//
const labelNodes = [
{
id: 0,
name: 'A title',
},
]
let composite = svg
.append('g')
.attr('id', 'composite')
.selectAll('g')
.data(labelNodes, (d) => d.id)
const g = composite.enter()
const rectangularNode = g
.append('rect')
.attr('class', 'node')
.attr('rx', '15')
.attr('x', (d) => 0)
.attr('y', (d) => 0)
.attr('width', () => 200)
.attr('height', () => 25)
.attr('fill', '#dceed3')
var outerNodebbox = rectangularNode.node().getBBox()
const label = g
.append('text')
.attr('class', 'name')
.attr('ref', 'name')
.attr('id', 'name')
.attr('dominant-baseline', 'middle')
.attr('font-size', '10px')
.attr('x', () => 13)
.attr('y', () => 13)
.text((d) => {
return d['name']
})
composite = g.merge(composite)
// ******************************************************
// What goes here to add the composite node to the graph?
// ******************************************************
//
// ??
.graph {
width: 1000px;
height: 400px;
}
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg ref="chart" id="chart" class="graph"></svg>
Making the display of composite nodes data-driven. Example:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<style>
.graph {
width: 1000px;
height: 400px;
}
</style>
</head>
<body>
<svg ref="chart" id="chart" class="graph"></svg>
<script>
//
// Create the force directed graph
//
const width = 1000;
const height = 400;
const node_data = Array.from({ length: 5 }, () => ({
group: Math.floor(Math.random() * 3),
isComposite: Math.random() > 0.5,
}));
const edge_data = Array.from({ length: 10 }, () => ({
source: Math.floor(Math.random() * 5),
target: Math.floor(Math.random() * 5),
value: Math.floor(Math.random() * 10) + 1,
}));
const links = edge_data.map((d) => ({ ...d }));
const nodes = node_data.map((d, index) => ({ id: index, ...d }));
const color = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select('#chart');
const simulation = d3
.forceSimulation(nodes)
.force(
'link',
d3
.forceLink(links)
.id((d) => d.id)
.distance((d) => 100)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked);
const link = svg
.append('g')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.selectAll()
.data(links)
.join('line')
.attr('stroke-width', (d) => Math.sqrt(d.value));
const node = svg
.append('g')
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.selectAll()
.data(nodes)
.join('g');
node
.filter((d) => !d.isComposite)
.append('circle')
.attr('r', 16)
.attr('fill', (d) => color(d.group));
const composite = node
.filter((d) => d.isComposite)
.append('g')
.attr('class', 'composite');
composite
.append('rect')
.attr('class', 'node')
.attr('rx', '15')
.attr('width', () => 200)
.attr('height', () => 25)
.attr('fill', '#dceed3');
composite
.append('text')
.attr('class', 'name')
.attr('ref', 'name')
.attr('id', 'name')
.attr('dominant-baseline', 'middle')
.attr('font-size', '10px')
.attr('x', () => 13)
.attr('y', () => 13)
.text((d, i) => {
return 'name ' + i;
});
node.append('title').text((d) => `hello ${d.id}`);
function ticked() {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
node
.selectAll('circle')
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
node
.selectAll('.composite')
.attr(
'transform',
(d) => 'translate(' + (d.x - 100) + ',' + d.y + ')'
);
}
node.call(
d3
.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
);
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
</script>
</body>
</html>