SOURCE

console 命令行工具 X clear

                    
>
console
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>D3.js Canvas Example</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <style>
        .node-image {
            width: 40px;
            height: 40px;
            cursor: pointer;
        }
        .link {
            stroke: #999;
            stroke-opacity: 0.6;
            marker-end: url(#arrowhead);
        }
        .canvas-popover {
            position: absolute;
            background-color: white;
            border: 1px solid #ccc;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
            padding: 5px 10px;
            z-index: 1000;
            display: block; /* Always display popover */
            word-wrap: break-word;
            overflow-wrap: break-word;
            white-space: pre-wrap;
        }
        .canvas {
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div id="canvas-container" style="width: 100%; height: 100vh;">
        <svg id="canvas1" class="canvas" width="100%" height="300" style="background-color: #f0f0f0;"></svg>
        <svg id="canvas2" class="canvas" width="100%" height="300" style="background-color: #e0e0e0;"></svg>
        <svg id="canvas3" class="canvas" width="100%" height="300" style="background-color: #d0d0d0;"></svg>
    </div>
    <script>
        const width = 100;
        const height = 100;

        const canvas1 = d3.select("#canvas1");
        const canvas2 = d3.select("#canvas2");
        const canvas3 = d3.select("#canvas3");

        // Define arrowhead marker
        function defineArrowhead(svg) {
            svg.append("defs").append("marker")
                .attr("id", "arrowhead")
                .attr("viewBox", "-0 -5 10 10")
                .attr("refX", 20)
                .attr("refY", 0)
                .attr("orient", "auto")
                .attr("markerWidth", 10)
                .attr("markerHeight", 10)
                .append("path")
                .attr("d", "M0,-5L10,0L0,5")
                .attr("fill", "#999");
        }

        defineArrowhead(canvas1);
        defineArrowhead(canvas2);
        defineArrowhead(canvas3);

        // Function to convert percentage to pixels
        function percentToPixels(percent, dimension) {
            return (percent / 100) * dimension;
        }

        // 第一个画布
        const nodesCanvas1 = [
            { id: 0, x: 10, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 1" },
            { id: 1, x: 30, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 2" },
            { id: 2, x: 50, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 3" },
            { id: 3, x: 50, y: 30, imageUrl: "https://via.placeholder.com/40", title: "Node 4" }
        ];

        const nodeGroup1 = canvas1.selectAll(".node")
            .data(nodesCanvas1)
            .enter()
            .append("g")
            .attr("class", "node")
            .attr("transform", (d, i, nodes) => {
                const svgWidth = nodes[i].getBoundingClientRect().width;
                const svgHeight = nodes[i].getBoundingClientRect().height;
                const x = percentToPixels(d.x, svgWidth);
                const y = percentToPixels(d.y, svgHeight);
                return `translate(${x},${y})`;
            });

        nodeGroup1.append("image")
            .attr("x", -20)
            .attr("y", -20)
            .attr("width", 40)
            .attr("height", 40)
            .attr("xlink:href", d => d.imageUrl);

        nodeGroup1.each(function(d, i) {
            const node = d3.select(this);
            const nodeScreenCoords = node.node().getBoundingClientRect();

            const popover = d3.select("#canvas-container").append("div")
                .attr("class", "canvas-popover")
                .style("left", nodeScreenCoords.left + nodeScreenCoords.width / 2 + "px")
                .style("top", nodeScreenCoords.top - 50 + "px");

            // 添加 popover 内容
            popover.append("p")
                .text(`Click to jump from ${d.title}:`);
            popover.append("a")
                .attr("href", "#")
                .text("Go to Link")
                .on("click", (event) => {
                    event.preventDefault();
                    alert(`Clicked on ${d.title}`);
                });
        });

        // 第一个画布内的连线
        nodesCanvas1.forEach((d, i) => {
            if (i < nodesCanvas1.length - 1) {
                const source = nodesCanvas1[i];
                const target = nodesCanvas1[i + 1];
                canvas1.append("line")
                    .attr("x1", percentToPixels(source.x, canvas1.node().getBoundingClientRect().width))
                    .attr("y1", percentToPixels(source.y, canvas1.node().getBoundingClientRect().height))
                    .attr("x2", percentToPixels(target.x, canvas1.node().getBoundingClientRect().width))
                    .attr("y2", percentToPixels(target.y, canvas1.node().getBoundingClientRect().height))
                    .attr("class", "link");
            }
        });

        canvas1.append("line")
            .attr("x1", percentToPixels(nodesCanvas1[0].x, canvas1.node().getBoundingClientRect().width))
            .attr("y1", percentToPixels(nodesCanvas1[0].y, canvas1.node().getBoundingClientRect().height))
            .attr("x2", percentToPixels(nodesCanvas1[0].x, canvas1.node().getBoundingClientRect().width))
            .attr("y2", percentToPixels(nodesCanvas1[3].y, canvas1.node().getBoundingClientRect().height))
            .attr("class", "link");

        // 第二个画布
        const nodesCanvas2 = [
            { id: 0, x: 25, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 5" },
            { id: 1, x: 25, y: 30, imageUrl: "https://via.placeholder.com/40", title: "Node 6" },
            { id: 2, x: 25, y: 50, imageUrl: "https://via.placeholder.com/40", title: "Node 7" }
        ];

        const nodeGroup2 = canvas2.selectAll(".node")
            .data(nodesCanvas2)
            .enter()
            .append("g")
            .attr("class", "node")
            .attr("transform", (d, i, nodes) => {
                const svgWidth = nodes[i].getBoundingClientRect().width;
                const svgHeight = nodes[i].getBoundingClientRect().height;
                const x = percentToPixels(d.x, svgWidth);
                const y = percentToPixels(d.y, svgHeight);
                return `translate(${x},${y})`;
            });

        nodeGroup2.append("image")
            .attr("x", -20)
            .attr("y", -20)
            .attr("width", 40)
            .attr("height", 40)
            .attr("xlink:href", d => d.imageUrl);

        nodeGroup2.each(function(d, i) {
            const node = d3.select(this);
            const nodeScreenCoords = node.node().getBoundingClientRect();

            const popover = d3.select("#canvas-container").append("div")
                .attr("class", "canvas-popover")
                .style("left", nodeScreenCoords.left + nodeScreenCoords.width / 2 + "px")
                .style("top", nodeScreenCoords.top - 50 + "px");

            // 添加 popover 内容
            popover.append("p")
                .text(`Click to jump from ${d.title}:`);
            popover.append("a")
                .attr("href", "#")
                .text("Go to Link")
                .on("click", (event) => {
                    event.preventDefault();
                    alert(`Clicked on ${d.title}`);
                });
        });

        // 第二个画布内的连线
        nodesCanvas2.forEach((d, i) => {
            if (i < nodesCanvas2.length - 1) {
                const source = nodesCanvas2[i];
                const target = nodesCanvas2[i + 1];
                canvas2.append("line")
                    .attr("x1", percentToPixels(source.x, canvas2.node().getBoundingClientRect().width))
                    .attr("y1", percentToPixels(source.y, canvas2.node().getBoundingClientRect().height))
                    .attr("x2", percentToPixels(target.x, canvas2.node().getBoundingClientRect().width))
                    .attr("y2", percentToPixels(target.y, canvas2.node().getBoundingClientRect().height))
                    .attr("class", "link");
            }
        });

        // 第三个画布
        const nodesCanvas3 = [
            { id: 0, x: 10, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 8" },
            { id: 1, x: 30, y: 10, imageUrl: "https://via.placeholder.com/40", title: "Node 9" },
            { id: 2, x: 30, y: 30, imageUrl: "https://via.placeholder.com/40", title: "Node 10" }
        ];

        const nodeGroup3 = canvas3.selectAll(".node")
            .data(nodesCanvas3)
            .enter()
            .append("g")
            .attr("class", "node")
            .attr("transform", (d, i, nodes) => {
                const svgWidth = nodes[i].getBoundingClientRect().width;
                const svgHeight = nodes[i].getBoundingClientRect().height;
                const x = percentToPixels(d.x, svgWidth);
                const y = percentToPixels(d.y, svgHeight);
                return `translate(${x},${y})`;
            });

        nodeGroup3.append("image")
            .attr("x", -20)
            .attr("y", -20)
            .attr("width", 40)
            .attr("height", 40)
            .attr("xlink:href", d => d.imageUrl);

        nodeGroup3.each(function(d, i) {
            const node = d3.select(this);
            const nodeScreenCoords = node.node().getBoundingClientRect();

            const popover = d3.select("#canvas-container").append("div")
                .attr("class", "canvas-popover")
                .style("left", nodeScreenCoords.left + nodeScreenCoords.width / 2 + "px")
                .style("top", nodeScreenCoords.top - 50 + "px");

            // 添加 popover 内容
            popover.append("p")
                .text(`Click to jump from ${d.title}:`);
            popover.append("a")
                .attr("href", "#")
                .text("Go to Link")
                .on("click", (event) => {
                    event.preventDefault();
                    alert(`Clicked on ${d.title}`);
                });
        });

        // 第三个画布内的连线
        nodesCanvas3.forEach((d, i) => {
            if (i < nodesCanvas3.length - 1) {
                const source = nodesCanvas3[i];
                const target = nodesCanvas3[i + 1];
                canvas3.append("line")
                    .attr("x1", percentToPixels(source.x, canvas3.node().getBoundingClientRect().width))
                    .attr("y1", percentToPixels(source.y, canvas3.node().getBoundingClientRect().height))
                    .attr("x2", percentToPixels(target.x, canvas3.node().getBoundingClientRect().width))
                    .attr("y2", percentToPixels(target.y, canvas3.node().getBoundingClientRect().height))
                    .attr("class", "link");
            }
        });

        // 连接三个画布的连线
        const canvasPositions = [
            { x: 50, y: 50 },
            { x: 50, y: 50 },
            { x: 50, y: 50 }
        ];

        d3.select("#canvas-container")
            .selectAll(".canvas-line")
            .data(canvasPositions.slice(0, 2))
            .enter()
            .append("line")
            .attr("class", "canvas-line")
            .attr("x1", (d, i) => percentToPixels(canvasPositions[i].x, canvas1.node().getBoundingClientRect().width) + 50)
            .attr("y1", (d, i) => percentToPixels(canvasPositions[i].y, canvas1.node().getBoundingClientRect().height) + 150)
            .attr("x2", (d, i) => percentToPixels(canvasPositions[i + 1].x, canvas2.node().getBoundingClientRect().width) + 50)
            .attr("y2", (d, i) => percentToPixels(canvasPositions[i + 1].y, canvas2.node().getBoundingClientRect().height) + 150)
            .attr("class", "link");
    </script>
</body>
</html>