console
const width = 600,height=400;
const deviceNum = 2000;
const nodes = new Array(deviceNum).fill("").map((d,i)=>{
return {
id:i+1,
name:`device${i+1}`,
}
});
const links = [{source:1,target:2},{source:1,target:3},{source:1,target:6},{source:1,target:11},
{source:1,target:4},{source:7,target:8},
{source:8,target:5},{source:7,target:5},
{source:9,target:10}
]
const connectedNodeIds = new Set();
links.forEach(link => {
connectedNodeIds.add(link.source.id || link.source);
connectedNodeIds.add(link.target.id || link.target);
});
const linkNodes =[],standaloneNodes = [];
nodes.forEach(node => {
if(connectedNodeIds.has(node.id)){
linkNodes.push(node);
}else{
standaloneNodes.push(node)
}
});
const zoom = d3.zoom().scaleExtent([0.1,10]).on('zoom',zoomHandler);
const g = d3.select(svg)
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;")
.call(zoom);
let container_wrap = g.append('g');
function zoomHandler(){
let {x,y,k} = d3.event.transform;
container_wrap.attr("transform","translate("+x+","+ y+ ")scale("+ k +")")
}
function linkArc(d) {
const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);
return `
M${d.source.x},${d.source.y}
L${d.target.x},${d.target.y}
`;
}
function attractTo(x, y) {
return () => {
for (const node of nodes) {
if (node.isIsolated) {
node.vx += (x - node.x) * 0.001;
node.vy += (y - node.y) * 0.001;
}
}
};
}
const simulation = d3.forceSimulation(linkNodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(80))
.force("center", d3.forceCenter(width/2, height/2))
.force("charge", d3.forceManyBody().strength(-400))
.force("x", d3.forceX())
.force("y", d3.forceY());
simulation.stop();
for (let i = 0; i < 300; ++i) simulation.tick();
const link = container_wrap.append("g")
.attr("fill", "none")
.attr("stroke-width", 1.5)
.selectAll("path")
.data(links)
.join("path")
.attr("stroke", "#09c")
.attr("d", linkArc);
const node = container_wrap.append("g")
.attr("fill", "currentColor")
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.selectAll("g")
.data(linkNodes)
.join("g")
.attr("transform", d => `translate(${d.x},${d.y})`);
node.append("circle")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.attr("r", 4);
node.append("text")
.attr("x", 8)
.attr("y", "0.31em")
.text(d => d.id)
.clone(true).lower()
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 3);
const col = 4,nodeH = 30,nodeW= 30;
const standaloneNode = container_wrap.append("g")
.attr("fill", "currentColor")
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.attr("transform","translate(10,10)")
.selectAll("g")
.data(standaloneNodes)
.join("g")
.attr("transform", (d,i) => `translate(${(i%col)*30},${height - ((Math.ceil(standaloneNodes.length/col) - Math.floor(i/col))*30)})`);
standaloneNode.append("circle")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.attr("r", 4);
standaloneNode.append("text")
.attr("x", 8)
.attr("y", "0.31em")
.text(d => d.id)
.clone(true).lower()
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 3);
<svg id="svg" ></svg>
svg{
background-color: rgba(0,0,0,0.2);
}