关于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"><buttononclick="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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/19524.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】Git超详细教程:手把手教你(gitee版)--版本管理+远程仓库克隆(初学者必看!!!)

目录 一、前言 二、git 的深度理解 &#x1f95d; 什么是 git ? &#x1f347; git 的历史发展&#xff08;理解 git 的由来&#xff09; &#x1f34b; 感性理解 git 的版本管理 三、git 的安装 ✨Window 终端安装 ✨Linux 安装 四、git 的工作流程 五、如何在 Linux …

音视频开发—视频相关概念:YUV与RGB

文章目录 YUV相关概念组成部分优点常见的 YUV 格式数据量的计算YUV4:2:0 存储格式平面模式&#xff08;planar):打包模式&#xff08;packed&#xff09; RGB 和 YUV 的定义关系与转换RGB 到 YUV 的转换YUV 到 RGB 的转换 使用场景优缺点 YUV相关概念 YUV 是一种颜色编码格式&…

JVM-JAVA-类加载过程

JVM源码 类加载到 JVM 的过程通过 java 命令执行代码的流程 类加载到 JVM 的过程 在运行一个 main 函数启动程序是&#xff0c;首先需要类加载起把主类加载到 JVM 中 通过 java 命令执行代码的流程 loadClass的类加载过程有如下几步&#xff1a; 类被加载到方法区中后主要包…

Maven项目通过maven central 发布到中央仓库 https://repo.maven.apache.org/ 手把手教学 最新教学

一、注册maven central账号 ​ https://central.sonatype.com/publishing/namespaces 我这里直接使用github账号登录 &#xff0c;可以自己注册或者直接使用google账号或者github账号登录 这里github账号登录之后 应该只出现io.github 下面的io.gitee我也验证过 所以这里出…

Java时间类--JDK8

为什么JDK8会又新增时间相关类呢&#xff1f; ① JDK7的时间对象如果需要比较大小的话&#xff0c;必须都先转换成毫秒值&#xff1b;JDK8则不需要&#xff0c;可以直接比较。 ② JDK7的时间对象可以修改&#xff0c;在多线程环境下就会导致数据不安全&#xff1b;JDK8不能修改…

数据库open报ORA-600 kcratr_scan_lastbwr故障处理---惜分飞

由于断电&#xff0c;导致数据库正常open报ORA-600 kcratr_scan_lastbwr错误 Wed Jan 17 18:23:26 2024 ALTER DATABASE MOUNT Successful mount of redo thread 1, with mount id 1028618590 Database mounted in Exclusive Mode Lost write protection disabled Completed:…

【Git】在错误分支上开发了怎么办

情况一&#xff1a;还未提交 git add . 『暂存修改的代码』git stash 『把暂存的文件提交到git的暂存栈』git checkout 『本该提交代码的分支』git stash pop 『取出暂存栈中的代码』 情况二&#xff1a;已提交 git checkout 『不该提交代码提交了代码的分支』git reset HEA…

输入3个字符串,要求将字母按由小到大顺序输出

对于将3个整数按由小到大顺序输出&#xff0c;是很容易处理的。可以按照同样的算法来处理将3个字符串按大小顺序输出。可以直接写出程序。 编写程序&#xff1a; 运行结果&#xff1a; 这个程序是很好理解的。在程序中对字符串变量用关系运算符进行比较&#xff0c;如同对数值…

【Git 版本管理】合并 + 变更,看懂Git

看懂 Git 合并操作分离 HEAD分离 HEAD 测试 相对引用(^ || ~)操作符 ^相对引用 ^ 测试操作符 ~相对引用 ~ 测试 撤销变更Git ResetGit Revert撤销变更 测试 整理提交记录Git Cherry-pick测试 交互式 rebase交互式 rebase 测试 合并操作 关键字&#xff1a;commit、branch、merg…

Minio篇:初识MinIO

1. MinIO快速入门 1.1.MinIO核心概念 下面介绍MinIO中的几个核心概念&#xff0c;这些概念在所有的对象存储服务中也都是通用的。 对象&#xff08;Object&#xff09; 对象是实际的数据单元&#xff0c;例如我们上传的一个图片。 存储桶&#xff08;Bucket&#xff09; 存储…

基于单片机的机械臂的研究

摘要 &#xff1a; 工业机器人是现代科学技术的融合下的产物&#xff0c;属于机电自动化设备的一种。机械臂作为工业机器人的核心&#xff0c;在提高工业生产效率的同时&#xff0c;也保障了工作人员的身心安全。本文以某款基于单片机的机械臂为例&#xff0c;对机械臂的设计方…

【JAVA SE】多态

✨✨欢迎大家来到Celia的博客✨✨ &#x1f389;&#x1f389;创作不易&#xff0c;请点赞关注&#xff0c;多多支持哦&#x1f389;&#x1f389; 所属专栏&#xff1a;JAVA 个人主页&#xff1a;Celias blog~ 目录 引言 一、多态 1.1 多态的概念 1.2 多态的实现条件 1.3…

AI未来预测

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已经成为了当今世界的热门话题。从自动驾驶汽车到智能家居&#xff0c;从虚拟助手到机器人&#xff0c;AI的应用已经渗透到了我们生活的方方面面。在未来&#xff0c;AI将会继续改变我们的生活方式&#xff0…

深入探讨 Android 的 View 显示过程与源码分析

文章目录 1. 探讨 Android 的 View 显示过程1.1. onFinishInflate1.2. onAttachedToWindow1.3. onMeasure1.4. onSizeChanged1.5. onLayout1.6. onDraw 2. 系统代码分析1.1. onFinishInflate1.2. onAttachedToWindow1.3. onMeasure1.4. onSizeChanged1.5. onLayout1.6. onDraw …

【Spring Boot】SpringBoot 下在 yml 中的 logging 日志配置

文章目录 前言输出日志的级别日志输出的位置日志输出的格式日志文件的存储路径日志文件是否输出到控制台配置Logback 配置日志分组配置日志细粒度配置【logger】 前言 logging 配置主要用于控制应用程序的日志输出行为&#xff0c;可以通过配置定制日志的格式、级别、输出位置…

数字化浪潮中的TPM革新:打造高效生产新范式

在数字化浪潮席卷全球的今天&#xff0c;传统生产管理模式正面临前所未有的挑战与机遇。TPM&#xff08;全面生产维护&#xff09;作为一种先进的生产管理理念&#xff0c;如何在数字化驱动下焕发新的活力&#xff0c;成为制造业转型升级的关键一环。 数字化技术为TPM带来了前…

Ubuntu禁止内核自动更新

查看当前内核版本 uname -v #35~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue May 7 09:00:52 UTC 2 uname -a Linux GKJ 6.5.0-35-generic #35~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue May 7 09:00:52 UTC 2 x86_64 x86_64 x86_64 GNU/Linux uname -r 6.5.0-35-generic 方法1&am…

EureKa是什么?

Eureka 是一个源于 Netflix 公司的开源项目&#xff0c;主要用于实现服务注册和服务发现的功能。它是构建分布式系统中的微服务架构的一个关键组件。下面是对 Eureka 的解释&#xff1a; 基本概念 Eureka 是基于 REST 的服务&#xff0c;主要用于管理微服务架构中的服务实例的…

探秘Flask中的表单数据处理

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、Flask中的表单处理机制 三、Flask表单处理实战 四、处理表单数据的注意事项…

为什么要使用动态代理IP?

一、什么是动态代理IP&#xff1f; 动态代理IP是指利用代理服务器来转发网络请求&#xff0c;并通过不断更新IP地址来保护访问者的原始IP&#xff0c;从而达到匿名访问、保护隐私和提高访问安全性的目的。动态代理IP在多个领域中都有广泛的应用&#xff0c;能够帮助用户…