SOURCE

console 命令行工具 X clear

                    
>
console
const width = 600,height=400;

const r = 10;
const gap = 15;
const size = 20;
const halfSize = Math.floor(size/2);

const hubSize = 3;
const hubs = new Array(hubSize).fill("").map((d,i)=>{
    if(hubSize%2 !== 0){
        return {
            x:width/2,
            y:height/2 - (2*r + gap)*(Math.floor(hubSize/2) - i)
        }
    }else{
        return {
            x:width/2,
            y:height/2 + ((i-(hubSize-i-1))*(r+gap/2))
        }
    }
})

const isOdd = size%2 === 0;
const leftSpokenY = i=> halfSize === 1 ? height/2 : (height-40)/(halfSize-1)*(i%(halfSize))+20;
const rightSpokeY =i=>  halfSize === 0?  height/2 :(height-40)/(halfSize)*(i%(halfSize+1))+20
const spokeY = i=> isOdd ? leftSpokenY(i)  : rightSpokeY(i);
const spokes = new Array(size).fill("").map((d,i)=>({
    x:i<halfSize ? 20:width-20,
    y:i<halfSize ? leftSpokenY(i)  : spokeY(i),
    name:`device${i+1}`
}));



const leftDevice = spokes.slice(0,halfSize);
const rightDevice = spokes.slice(halfSize);





const links = hubs.map(hub=>{
    return spokes.map(d=>{
    return {
     source:{
        x:hub.x,
        y:hub.y
    },
    target:{
        x:d.x,y:d.y
    }   
    }
})
})


const g = d3.select(svg).attr("width",width).attr("height",height).append("g");

const diagonal = d3.linkHorizontal().x(d => d.x).y(d => d.y);

    const link = g.selectAll("path")
      .data(links.flat());

    // Enter any new links at the parent's previous position.
   link.enter().append("path")
        .attr("d", d => {
          return diagonal({source: d.source, target: d.target});
        })
        .attr("fill","none")
        .attr("stroke","red");

g.selectAll(".hub").data(hubs).join("circle")
    .attr("cx",d=>d.x).attr("cy",d=>d.y).attr("r",10);

g.selectAll(".leftSpoke")
    .data(leftDevice).join("circle").attr("cx",d=>d.x)
    .attr("cy",d=>d.y).classed("leftSpoke",true).attr("r",10);;

g.selectAll(".rightSpoke")
    .data(rightDevice).join("circle").attr("cx",d=>d.x)
    .attr("cy",d=>d.y).classed("rightSpoke",true).attr("r",10);



    var svgElement = document.getElementsByTagName('svg')[0];
    var svgContent = new XMLSerializer().serializeToString(svgElement);

    var canvas = document.getElementById('c');


    var ctx = canvas.getContext('2d');
    var img = new Image();
    img.onload = function() {
        const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
        const w = img.width * scale;
        const h = img.height * scale;
        const x = (canvas.width - w) / 2;
        const y = (canvas.height - h) / 2;

        ctx.drawImage(img, x, y, w, h);
    };
    img.src = 'data:image/svg+xml;base64,' + btoa(svgContent);
<svg id="svg" ></svg>
<canvas id="c" width="100" height="100"></canvas>
svg{
    background-color: rgba(0,0,0,0.2);
}
#c{
    background-color: #ccc;
    width: 100px;
    height: 100px;
}

本项目引用的自定义外部资源