| <!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"); |
| |
| |
| |
| let links = []; |
| let nodes = [{ |
| "id": "ec61a8f4-73ec-46fe-adb2-4473119a006c", |
| "name": "init", |
| "type": "init", |
| "x": 119.99999237060547, |
| "y": 43.71427917480469 |
| }]; |
| |
| |
| |
| |
| |
| |
| 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(); |
| |
| } 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(); |
| |
| }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) |
| |
| |
| 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 = nodeType; |
| nodes.push({ id, name, type, x: coords[0], y: coords[1] }); |
| update(); |
| |
| } |
| }); |
| |
| 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; |
| |
| } |
| 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; |
| } |
| |
| |
| 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.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'; |
| } |
| update(); |
| </script> |
| </body> |
| </html> |