使用 Canvas 和 JavaScript 使多人关系图具有拖放和事件等交互性

发布时间 2023-05-24 11:34:01作者: 清风引佩下瑶台

一些多人关系图谱是分析数据的可视化工具,对分析数据有很大作用,下面手写一个简单的demo

<!DOCTYPE html>
<html>
<head>
    <title>多人关系图</title>
    <style>
        canvas {
            border: 1px solid #000;
        } 
    </style>
</head>
<body>
    <canvas id="myCanvas" width="1900" height="800"></canvas>

    <script>
        var canvas = document.getElementById("myCanvas");
        var ctx = canvas.getContext("2d");

        // 画布背景
        ctx.fillStyle = "#fff";
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // 定义节点
        var nodes = [
            { id: "A", x: 0, y: 0 },
            { id: "B", x: 0, y: 0 },
            { id: "C", x: 0, y: 0 },
            { id: "D", x: 0, y: 0 },
            { id: "E", x: 0, y: 0 },
            { id: "F", x: 0, y: 0 },
            { id: "G", x: 0, y: 0 },
            { id: "H", x: 0, y: 0 },
            { id: "I", x: 0, y: 0 },
            { id: "J", x: 0, y: 0 },
            { id: "K", x: 0, y: 0 },
            { id: "L", x: 0, y: 0 },
            { id: "M", x: 0, y: 0 },
            { id: "N", x: 0, y: 0 },
            { id: "O", x: 0, y: 0 },
            { id: "P", x: 0, y: 0 },
            { id: "Q", x: 0, y: 0 },
            { id: "R", x: 0, y: 0 },
            { id: "S", x: 0, y: 0 },
            { id: "T", x: 0, y: 0 },
            { id: "U", x: 0, y: 0 },
            { id: "V", x: 0, y: 0 },
            { id: "W", x: 0, y: 0 },
            { id: "X", x: 0, y: 0 },
            { id: "Y", x: 0, y: 0 },
            { id: "Z", x: 0, y: 0 },
        ];
        nodesArea = [
            ['A','B','C','D','E'],
            ['F','G','H'],
            ['I','J','K','L'],
            ['M','N','O','P','Q'],
            ['R','S','T','U','V','W'],
            ['X','Y','Z'],
        ]
        nodesAreaDataX = [-1,1,-1,1,-1.2,1.2,] // 模拟2134象限x  是基于默认初始值位置(可以设画布中间),负数为初始左,正为右,数值越大偏移越多
        nodesAreaDataY = [1,1,-1,-1,1.2,-1.2,] // 模拟2134象限y  同上
        nodes.forEach((item,index) => {
            nodesArea.forEach((val,num) => {
                if(val.includes(item.id)) {
                    let jo = 1.5
                    if(index%2){ // 奇数偶数切换
                        jo = 1
                    } else {
                        jo = -1
                    }
                    // if(index > 30&&index < 60) { // 
                    //     index = index - 30
                    // }
                    // if(index > 60&&index < 90) {
                    //     index = index - 60
                    // }
                    item.x = 600+nodesAreaDataX[num]*120+(10*index*jo)+Math.round(Math.random()*50);
                    item.y = 300+nodesAreaDataY[num]*120+(5*index*jo)+Math.round(Math.random()*20);
                }
            })
        })
        console.log(nodes,'nodes');
        // 定义连线
        var links = [
            { source: "A", target: "B" },
            { source: "B", target: "C" },
            { source: "C", target: "A" },
            { source: "G", target: "C" },
            { source: "G", target: "K" },
            { source: "F", target: "A" },
            { source: "A", target: "H" },
            { source: "C", target: "M" },
            { source: "I", target: "A" },
            { source: "G", target: "A" },
            { source: "O", target: "Q" },
            { source: "J", target: "M" },
            { source: "P", target: "L" },
            { source: "X", target: "Y" },
            { source: "Y", target: "Z" },
            { source: "O", target: "R" },
            { source: "U", target: "V" },
            { source: "X", target: "C" },
            { source: "Y", target: "A" },
            { source: "Z", target: "V" },
            { source: "S", target: "T" },
            { source: "Z", target: "G" },
            { source: "Z", target: "H" },
        ];

        // 绘制节点和连线
        draw();

        // 添加拖动事件
        var selectedNode = null;
        canvas.addEventListener("mousedown", function(event) {
            selectedNode = getNodeAtPosition(event.offsetX, event.offsetY);
        });
        canvas.addEventListener("mousemove", function(event) {
            
            if (selectedNode != null) {
                selectedNode.x = event.offsetX;
                selectedNode.y = event.offsetY;
                draw();
            }
        });
        canvas.addEventListener("mouseup", function(event) {
            selectedNode = null;
        });

        // 获取位置上的节点
        function getNodeAtPosition(x, y) {
            for (var i = 0; i < nodes.length; i++) {
                var node = nodes[i];
                var distance = Math.sqrt((node.x - x) * (node.x - x) + (node.y - y) * (node.y - y));
                if (distance <= 20) {
                    return node;
                }
            }
            return null;
        }

        // 绘制节点和连线的函数
        function draw() {
            // 清空画布
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // 绘制连线
            ctx.strokeStyle = "#000";
            ctx.lineWidth = 1;
            links.forEach(function(link) {
                var sourceNode = nodes.find(function(node) { return node.id == link.source });
                var targetNode = nodes.find(function(node) { return node.id == link.target });

                ctx.beginPath();
                ctx.moveTo(sourceNode.x, sourceNode.y);
                ctx.lineTo(targetNode.x, targetNode.y);
                ctx.stroke();
            });

            // 绘制节点
            // ctx.fillStyle = 'rgba(255,153,0,1.00)';
            nodes.forEach(function(node) {
                ctx.beginPath();
                
                
                // 绘制文本
                new Promise((resolve, reject) => {
                    ctx.arc(node.x, node.y, 10, 0, 2 * Math.PI);
                    ctx.fillStyle = 'rgba(255,153,0,1.00)';
                    ctx.fill();
                    resolve();
                }).then((res) => {
                    ctx.font = "12px Arial";
                    ctx.textAlign = "center";
                    ctx.textBaseline = "middle";
                    ctx.fillStyle = 'rgba(0,191,255,1.00)';
                    ctx.fillText(node.id, node.x, node.y);
                });
                
            });
        }
    </script>
</body>
</html>

更多具体的欢迎交流开发,不使用插件的情况下,这种支撑数据应该不会太多,大量数据需要使用插件,支撑十万百万数据。设想过一种立体渲染模式,应该能支撑很多数据,设想原型复杂,没有实际操作,不亚于一个插件库,有大神有兴趣可以交流。