关于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不能修改…

输入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; 存储…

【JAVA SE】多态

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

深入探讨 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 …

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

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

探秘Flask中的表单数据处理

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

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

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

vue路由跳转之【编程式导航与传参】

vue路由有两种跳转方式 ----> 编程式与声明式&#xff0c;本文重点讲解vue路由的【编程式导航 】【编程式导航传参 ( 查询参数传参 & 动态路由传参 ) 】等内容&#xff0c;并结合具体案例让小伙伴们深入理解 &#xff0c;彻底掌握&#xff01;创作不易&#xff0c;需要的…

2023年全球DDoS攻击现状与趋势分析

天翼安全科技有限公司副总工程师、运营保障部总经理陈林表示&#xff0c;2023年扫段攻击频次快速增长&#xff0c;成为网络基础设施面临的最大威胁。为躲避防御&#xff0c;低速扫段攻击成为主流达到攻击总数的73.19%&#xff1b;43.26%的C段攻击持续时间小于5分钟&#xff0c;…

Linux中部署MinIO

Linux中部署MinIO 下载MinIO可执行程序&#xff1a; wget https://dl.min.io/server/minio/release/linux-amd64/minio 添加执行权限&#xff1a; chmod x minio 创建存储目录&#xff0c;例如/data&#xff1a; mkdir -p /data 运行MinIO服务器&#xff0c;需要设置MIN…

【设计模式】JAVA Design Patterns——Factory Method(虚拟构造器模式)

&#x1f50d;目的 为创建一个对象定义一个接口&#xff0c;但是让子类决定实例化哪个类。工厂方法允许类将实例化延迟到子类 &#x1f50d;解释 真实世界例子 铁匠生产武器。精灵需要精灵武器&#xff0c;而兽人需要兽人武器。根据客户来召唤正确类型的铁匠。 通俗描述 它为类…

三维前端项目中用THREEWebGLRenderer于创建渲染器对象

在三维前端项目开发中&#xff0c;可以使用THREE.WebGLRenderer创建渲染器对象。艾斯视觉作为行业ui设计与前端开发服务商很高兴能在这里与你共同探讨&#xff1a;THREE.WebGLRenderer是 Three.js 库中用于将 3D 场景渲染到屏幕上的类。以下是一个示例代码&#xff0c;展示了如…

element中table的selection-change监听改变的那条数据的下标

<el-table ref"table" :loading"loading" :data"tableData" selection-change"handleSelectionChange"></el-table>当绑定方法selection-change&#xff0c;当选择项发生变化时会触发该事件 // 多选框选中数据handleSele…

truncate IDL_UB1$导致数据库open hang---惜分飞

在一次数据库恢复中,发现IDL_UB1$表被truncate,然后数据库在open过程中会hang住,而且不报任何错误,这里通过试验进行重现.对于这类问题,以前有过类似处理测试&#xff1a;truncate IDL_UB1$恢复试验数据库版本 SQL> select * from v$version; BANNER ---------------------…