关于d3js生成节点画布的个人笔记

实现功能

  1. 根据鼠标位置生成节点
  2. 根据节点位置通过鼠标拖拽生成连线
  3. 实现自定义线段颜色功能
  4. 删除节点以及连线功能
  5. 实现单个节点拖动功能
  6. 实现整条线路的拖动功能
    界面如下:

主要模块介绍

绘制连线

const line = svg.selectAll(".line")
    .data(links, d => `${d.source}-${d.target}`);
line.enter()
    .append("line")
    .attr("class", "line")
    .attr("stroke", d => d.color)
    .attr("id", d => d.objId)
    .merge(line)
    .attr("x1", d => nodes.find(n => n.id === d.source).x)
    .attr("y1", d => nodes.find(n => n.id === d.source).y)
    .attr("x2", d => nodes.find(n => n.id === d.target).x)
    .attr("y2", d => nodes.find(n => n.id === d.target).y)
    });
line.exit().remove();

绘制节点

设置为四种节点类型,初始节点,杆塔,开关,电表箱

const node = svg.selectAll(".node")
    .data(nodes, d => d.id);
node.enter()
	// 这里可以换成circle,加入属性r,10换成原型节点,移除xlink
    .append("image")
    .attr("class", d => `node ${d.type}`)
    .attr("width", iconSize)
    .attr("height", iconSize)
    .attr("xlink:href", d => {
        if (d.type === 'pole') return './pole.png';
        if (d.type === 'meterBox') return './meterBox.png';
        if (d.type === 'switch') return './switch.png';
        if (d.type === 'init') return './init.png';
    })
    .merge(node)
    .attr("x", d => d.x - iconSize / 2)
    .attr("y", d => d.y - iconSize / 2)
    .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended)
    )
    .on("click", function(event, d) {
        if (isDeletingNode) {
            links = links.filter(link => link.source !== d.id && link.target !== d.id);
            nodes = nodes.filter(node => node !== d);
            update();
            // isDeletingNode = false;
        }else{
            console.log("点击了节点", d.name);
        }
    });
node.exit().remove();

添加节点

svg.on("click", function(event) {
    if (isAddingNode) {
        const coords = d3.pointer(event);
        const id = getUuid();
        const name = `Node${nodes.length + 1}`;
        const type = nodeType;
        nodes.push({ id, name, type, x: coords[0], y: coords[1] });
        update();
    }
});

完整实现如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive Line Loss Diagram with D3.js</title>
    <script src="https://d3js.org/d3.v6.min.js"></script>    
    <style>
        .pole {
            fill: orange;
            stroke: black;
            stroke-width: 1px;
        }
        .meterBox {
            fill: green;
            stroke: black;
            stroke-width: 1px;
        }
        .gateway {
            fill: blue;
            stroke: black;
            stroke-width: 1px;
        }
        .switch {
            fill: gray;
            stroke: black;
            stroke-width: 1px;
        }
        .line {
            stroke-width: 2px;
        }
        .label {
            font-size: 12px;
            text-anchor: middle;
        }
        .button-container {
            margin-bottom: 10px;
        }
        button {
            margin-right: 10px;
        }
        #colorPicker {
            display: none;
            position: absolute;
        }
    </style>
</head>
<body>
    <div class="button-container">
        <button id="addNodeButton" onclick="addNodeButton('pole')">新增节点</button>
        <button id="addNodeButton" onclick="addNodeButton('meterBox')">新增表箱</button>
        <button id="addNodeButton" onclick="addNodeButton('switch')">新增开关</button>
        <button id="addLinkButton">连线</button>
        <button id="deleteNodeButton">删除节点</button>
        <button id="deleteLinkButton">删除连线</button>
        <button onclick="logNodeButton()">打印节点</button>
        <button onclick="selectButton()">选择节点</button>
        <button onclick="selectAllButton()">整体拖动</button>
        <input type="color" id="colorPicker">
        <button
            onclick="updateColor()"
        >
            更新颜色
        </button>
    </div>
    </div>
    <svg width="800" height="600">
        <defs>
            <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
                <path d="M 20 0 L 0 0 0 20" fill="none" stroke="gray" stroke-width="0.5"/>
            </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#grid)" />
    </svg>
    <script src="test.js"></script>
    <script>
        const svg = d3.select("svg");
        // 节点以及连线的拖动行为
        const dragNode = d3.drag()
            .on("start", dragNodeStarted)
            .on("drag", draggedNode)
            .on("end", dragNodeEnded);
  
        function dragNodeStarted(event, d) {
            d3.select(this).raise().classed("active", true);
        }
  
        function draggedNode(event, d) {
            d.x = event.x;
            d.y = event.y;
  
            d3.select(this)
                .attr("cx", d.x)
                .attr("cy", d.y);
  
            // 更新连线的位置
            svg.selectAll(".line")
                .attr("x1", l => nodes.find(n => n.id === l.source).x)
                .attr("y1", l => nodes.find(n => n.id === l.source).y)
                .attr("x2", l => nodes.find(n => n.id === l.target).x)
                .attr("y2", l => nodes.find(n => n.id === l.target).y);
        }
  
        function dragNodeEnded(event, d) {
            d3.select(this).classed("active", false);
        }
        // let nodes = [];
        // let links = [];
        let links = [];
        let nodes = [{
            "id": "ec61a8f4-73ec-46fe-adb2-4473119a006c",
            "name": "init",
            "type": "init",
            "x": 119.99999237060547,
            "y": 43.71427917480469
        }];
  
        // 虚拟数据

//        nodes = jsonData.nodes;
//      links = jsonData.links;

  

        let isAddingNode = false;
        let isAddingLink = false;
        let isDeletingNode = false;
        let isDeletingLink = false;
        let sourceNode = null;
        let dragLine = null;
        let nodeType = null;
        let selectedLink   = null;
        let allDrag = false;
        const iconSize = 20;

  

        // 绘制节点和连线
        function update() {
            // 绘制连线
            const line = svg.selectAll(".line")
                .data(links, d => `${d.source}-${d.target}`);
            line.enter()
                .append("line")
                .attr("class", "line")
                .attr("stroke", d => d.color)
                .attr("id", d => d.objId)
                .merge(line)
                .attr("x1", d => nodes.find(n => n.id === d.source).x)
                .attr("y1", d => nodes.find(n => n.id === d.source).y)
                .attr("x2", d => nodes.find(n => n.id === d.target).x)
                .attr("y2", d => nodes.find(n => n.id === d.target).y)
                .on("click", function(event, d) {
                    if (isDeletingLink) {
                        links = links.filter(link => link !== d);
                        update();
                        // isDeletingLink = false;
                    } else{
                        selectedLink = d;
                        const colorPicker = document.getElementById("colorPicker");
                        colorPicker.style.display = "block";
                        colorPicker.style.left = `${event.pageX}px`;
                        colorPicker.style.top = `${event.pageY}px`;
                        colorPicker.value = d.color || "#000000";
                        colorPicker.focus();
                    }
                });

            line.exit().remove();

            // 绘制节点
            const node = svg.selectAll(".node")
                .data(nodes, d => d.id);
            node.enter()
                .append("image")
                .attr("class", d => `node ${d.type}`)
                .attr("width", iconSize)
                .attr("height", iconSize)
                .attr("xlink:href", d => {
                    if (d.type === 'pole') return './pole.png';
                    if (d.type === 'meterBox') return './meterBox.png';
                    if (d.type === 'switch') return './switch.png';
                    if (d.type === 'init') return './init.png';
                })
                .merge(node)
                .attr("x", d => d.x - iconSize / 2)
                .attr("y", d => d.y - iconSize / 2)
                .call(d3.drag()
                    .on("start", dragstarted)
                    .on("drag", dragged)
                    .on("end", dragended)
                )
                .on("click", function(event, d) {
                    if (isDeletingNode) {
                        links = links.filter(link => link.source !== d.id && link.target !== d.id);
                        nodes = nodes.filter(node => node !== d);
                        update();
                        // isDeletingNode = false;
                    }else{
                        console.log("点击了节点", d.name);
                    }
                });
            node.exit().remove();

            // 添加标签
            const label = svg.selectAll(".label")
                .data(nodes, d => d.id);
            label.enter()
                .append("text")
                .attr("class", "label")
                .merge(label)
                .attr("x", d => d.x)
                .attr("y", d => d.y - iconSize/2 - 5)
                // .text(d => d.name);
                // .text(d => d.type);
            label.exit().remove();
        }

  

        // 添加新节点

        svg.on("click", function(event) {
            if (isAddingNode) {
                const coords = d3.pointer(event);
                const id = getUuid();
                const name = `Node${nodes.length + 1}`;
                // const type = nodes.length % 3 === 0 ? 'tower' : (nodes.length % 3 === 1 ? 'station' : 'gateway');
                const type = nodeType;
                nodes.push({ id, name, type, x: coords[0], y: coords[1] });
                update();
                // isAddingNode = false; // Reset the flag
            }
        });

        function dragstarted(event, d) {
            if (isAddingLink) {
                sourceNode = d;
                let id = getUuid();
                sourceNode.objId = id;
                dragLine = svg.append("line")
                    .attr("class", "dragLine")
                    .attr("stroke", "gray")
                    .attr("stroke-width", 2)
                    .attr("x1", d.x)
                    .attr("y1", d.y)
                    .attr("x2", d.x)
                    .attr("y2", d.y)
                    .attr("id", id);
            }
            if (!isAddingNode && !isAddingLink && !isDeletingNode && !isDeletingLink) {
                d3.select(this).raise().classed("active", true);
            }
        }

  

        function dragged(event, d) {
            if (dragLine) {
                dragLine.attr("x2", event.x)
                        .attr("y2", event.y);
            }
            if (!isAddingNode && !isAddingLink && !isDeletingNode && !isDeletingLink) {
                d.x = event.x;
                d.y = event.y;

                d3.select(this)
                    .attr("x", d.x - iconSize / 2)
                    .attr("y", d.y - iconSize / 2);

                // 更新连线的位置
                svg.selectAll(".line")
                    .attr("x1", l => nodes.find(n => n.id === l.source).x)
                    .attr("y1", l => nodes.find(n => n.id === l.source).y)
                    .attr("x2", l => nodes.find(n => n.id === l.target).x)
                    .attr("y2", l => nodes.find(n => n.id === l.target).y);
            }
            if (allDrag) {
                const dx = event.dx;
                const dy = event.dy;

                // 更新被拖动节点的位置
                d.x += dx;
                d.y += dy;

                d3.select(this)
                    .attr("x", d.x - iconSize / 2)
                    .attr("y", d.y - iconSize / 2);

                // 递归更新与当前节点相连的所有节点和连线的位置
                const visitedNodes = new Set();
                updateConnectedNodes(d, dx, dy, visitedNodes);

                // 更新所有连线的位置
                svg.selectAll(".line")
                    .attr("x1", l => nodes.find(n => n.id === l.source).x)
                    .attr("y1", l => nodes.find(n => n.id === l.source).y)
                    .attr("x2", l => nodes.find(n => n.id === l.target).x)
                    .attr("y2", l => nodes.find(n => n.id === l.target).y);

                // 更新所有节点的位置
                svg.selectAll(".node")
                    .attr("x", n => n.x - iconSize / 2)
                    .attr("y", n => n.y - iconSize / 2);
            }
        }
  
        function dragended(event, d) {
            if (dragLine) {
                dragLine.remove();
                dragLine = null;
                const targetNode = nodes.find(n => Math.hypot(n.x - event.x, n.y - event.y) < 10);
                if (targetNode && targetNode !== sourceNode) {
                    const color = determineLinkColor(sourceNode, targetNode);
                    links.push({objId:sourceNode.id, source: sourceNode.id, target: targetNode.id, color: color });
                    update();
                }
                sourceNode = null;
                // isAddingLink = false; // Reset the flag
            }
            if (!isAddingNode && !isAddingLink && !isDeletingNode && !isDeletingLink) {
                d3.select(this).classed("active", false);
            }
        }
  
        /**
         * 递归更新与当前节点相连的所有节点的位置
         * @param {object} node - 当前节点
         * @param {number} dx - x方向的移动距离
         * @param {number} dy - y方向的移动距离
         * @param {Set} visitedNodes - 已访问的节点集合
         */
        function updateConnectedNodes(node, dx, dy, visitedNodes) {
            visitedNodes.add(node.id);
  
            links.forEach(link => {
                if (link.source === node.id && !visitedNodes.has(link.target)) {
                    const targetNode = nodes.find(n => n.id === link.target);
                    targetNode.x += dx;
                    targetNode.y += dy;
                    updateConnectedNodes(targetNode, dx, dy, visitedNodes);
                } else if (link.target === node.id && !visitedNodes.has(link.source)) {
                    const sourceNode = nodes.find(n => n.id === link.source);
                    sourceNode.x += dx;
                    sourceNode.y += dy;
                    updateConnectedNodes(sourceNode, dx, dy, visitedNodes);
                }
            });
        }
  
        function determineLinkColor(sourceNode, targetNode) {
            if (sourceNode.type === 'pole' && targetNode.type === 'pole') {
                return '#1e90ff';
            } else if (sourceNode.type === 'station' && targetNode.type === 'station') {
                return 'green';
            } else if (sourceNode.type === 'gateway' && targetNode.type === 'gateway') {
                return 'blue';
            } else {
                return '#7f8c8d';
            }
        }
        // 按钮事件处理
        function addNodeButton(type) {
            nodeType = type;
            isAddingNode = true;
            isAddingLink = false;
            isDeletingNode = false;
            isDeletingLink = false;
            allDrag = false;
        };
  
        d3.select("#addLinkButton").on("click", function() {
            isAddingLink = true;
            isAddingNode = false;
            isDeletingNode = false;
            isDeletingLink = false;
            allDrag = false;
        });

        d3.select("#deleteNodeButton").on("click", function() {
            isDeletingNode = true;
            isAddingNode = false;
            isAddingLink = false;
            isDeletingLink = false;
            allDrag = false;
        });
  
        d3.select("#deleteLinkButton").on("click", function() {
            isDeletingLink = true;
            isAddingNode = false;
            isAddingLink = false;
            isDeletingNode = false;
            allDrag = false;
        });
        // 整体拖动
        function selectAllButton() {
            isDeletingLink = false;
            isAddingNode = false;
            isAddingLink = false;
            isDeletingNode = false;
            allDrag = true;
        }
  
        update();
        function logNodeButton() {
            console.log("Node type:", nodeType);
            console.log("Adding node:", isAddingNode);
            console.log("Adding link:", isAddingLink);
            console.log("Deleting node:", isDeletingNode);
            console.log("Deleting link:", isDeletingLink);
            console.log("Nodes:", nodes);
            console.log("Links:", links);
        }
        function getUuid () {
            if (typeof crypto === 'object') {
              if (typeof crypto.randomUUID === 'function') {
                return crypto.randomUUID();
              }
              if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
                const callback = (c) => {
                  const num = Number(c);
                  return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16);
                };
                return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, callback);
              }
            }
            let timestamp = new Date().getTime();
            let perforNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0;
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
              let random = Math.random() * 16;
              if (timestamp > 0) {
                random = (timestamp + random) % 16 | 0;
                timestamp = Math.floor(timestamp / 16);
              } else {
                random = (perforNow + random) % 16 | 0;
                perforNow = Math.floor(perforNow / 16);
              }
              return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16);
            });
        };
        function selectButton() {
            isDeletingLink = false;
            isAddingNode = false;
            isAddingLink = false;
            isDeletingNode = false;
            allDrag = false;
        }
        /**
         * 更新指定线段的stroke属性
         * @param {string} sourceId - 连线起点节点的ID
         * @param {string} targetId - 连线终点节点的ID
         * @param {string} color - 新的颜色值
         */
         function updateLinkStroke(sourceId, targetId, color) {
            // 查找指定起点和终点的连线
            const link = links.find(l => l.source === sourceId && l.target === targetId);
            if (link) {
                // 更新连线的颜色属性
                link.color = color;
                // 使用D3.js选择器选择指定的连线,并更新其stroke属性
                d3.selectAll(".line")
                    .filter(d => d.source === sourceId && d.target === targetId)
                    .attr("stroke", color);
            }
        }

         // 颜色选择器事件处理
        function updateColor() {
            const colorPicker = document.getElementById("colorPicker");
            selectedLink.color = colorPicker.value;
            updateLinkStroke(selectedLink.source, selectedLink.target, selectedLink.color);
            colorPicker.style.display = 'none';
        }
    </script>
</body>
</html>
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值