先看图:

需求如上图所示,为了不占用太多的空间,展示没有固定的方向,前期去找了很多antv/G6,echarts等插件,一个graph只有一个方向,要么从上往下,要么从左到右,都不满足需求,于是直接用canvas手写了。我设计的参数如下:

思路如下:

  1. 如何渲染出节点(包含矩形框和⚪,看作一体),计算摆放位置;

    1. 由于设计的数据结构是tree结构的,有child字段一级包裹一级,先处理一份同级数据。(此部分代码见代码1,全部代码见最后)
    2. 根据当前节点及兄弟节点的个数处理居中摆放,根据direction判断是从左往右还是从上至下;(此部分代码见代码2,全部代码见最后)
  2. 如何渲染线段,把节点相连接;
    1. 根据第一步节点的坐标来画连接线(此部分代码见代码3,全部代码见最后)
  3. 由于节点内的内容复杂,标题,内容,参数都有一定的样式,于是就想到用div覆盖在节点的位置上,把原节点隐藏。(此部分代码见代码4,全部代码见最后)

代码1:

  //画画,计算摆放位置function flatten2(data) {return data.reduce((arr,{id,parentId,label,direction,level,xspacing,connectNode,autoUp,child = [],},currentIndex,parentIdArr,) => {let childNums = child.length; //当前级下的子集个数return arr.concat([{id,parentId,label,direction,level,xspacing,connectNode,autoUp,childNums,currentIndex,parentIdArr,},],flatten2(child, id),);},[],);}let OnlyLevelArr = flatten2(grathdata); //将tree转为平级结构let obj = {};OnlyLevelArr = OnlyLevelArr.reduce(function (item, next) {obj[next.id] ? '' : (obj[next.id] = true && item.push(next));return item;}, []);let mutlLevelArr = [[], [], [], [], [], [], [], [], [], []]; //目前支持10级层级 按level分级OnlyLevelArr.forEach((e, i) => {mutlLevelArr[e.level].push(e);});
}

代码2:

function renderRect(datas) {//画矩形框 计算摆放位置let childx, childy;let location = defaultLocation,tmp,futmp;if (datas && datas.length && datas.length >= 1) {datas.forEach((element) => {let notChildIndex = 0;if (element && element.length) {element.forEach((elem, indx) => {let halfLength1 = Number(element.length) / 2;let halfLengthFloor1 = Math.floor(halfLength1);let isOdd1 = element.length % 2; //是否是奇数ctx.lineWidth = 1;if (elem.level !== 0) {tmp = OnlyLevelArr.find(function (e) {return elem.parentId === e.id;}); //拿到父级定位futmp = OnlyLevelArr.find(function (e) {return tmp.parentId === e.id;}); //拿到父级的父级定位location = tmp.location? [tmp.location[0] + 0.5 * rectWidth, tmp.location[1]]: defaultLocation;//处理多个父级时不应该一第一个父级的坐标来计算 应该取多个父级的平均值if (tmp.direction === 'BT' &&elem.parentIdArr &&elem.parentIdArr.length &&elem.parentIdArr.length >= 1) {location = [tmp.location[0] +(elem.parentIdArr.length / 2 - 1) *(rectWidth + tmp.xspacing),tmp.location[1],];}elem.brotherNums = tmp.childNums;halfLength1 = Number(elem.brotherNums / 2);halfLengthFloor1 = Math.floor(halfLength1);isOdd1 = elem.brotherNums % 2; //是否是奇数if (tmp.direction === 'BT') {if (futmp && futmp.direction == 'LR') {//当前级是BT,父级是LR的 情况location = tmp.location? [tmp.location[0] + rectWidth, tmp.location[1]]: defaultLocation;(childx =location[0] -0.5 * rectWidth +elem.currentIndex * elem.xspacing +(elem.currentIndex + 1) * rectWidth),(childy = location[1] + 40);// ctx.strokeRect(childx,childy,rectWidth,rectHeight);} else {if (isOdd1) {//nodes长度为奇数时(childx =location[0] +(elem.currentIndex - halfLength1) * rectWidth +(elem.currentIndex - halfLengthFloor1) * elem.xspacing),(childy = location[1] + (rectHeight + yspace));// ctx.strokeRect(childx,childy,rectWidth,rectHeight);} else {//nodes长度为偶数时(childx =location[0] +(elem.currentIndex - halfLength1) * rectWidth +(elem.currentIndex - halfLength1 + 0.5) *elem.xspacing),(childy = location[1] + (rectHeight + yspace));// ctx.strokeRect(childx,childy,rectWidth,rectHeight);}}} else if (tmp.direction === 'LR') {//从左往右摆放// 处理3级及一下节点 如果没有自节点情况下 且父节点是LR 合并垂直BT显示 且 connectNodeif (elem.level >= 2 &&elem.childNums === 0 &&!elem.connectNode &&tmp.autoUp) {++notChildIndex;(childx =location[0] +0.5 * rectWidth +(notChildIndex - 1) * (rectWidth + elem.xspacing)),(childy =location[1] +(yspace + rectHeight + 25) * (tmp.childNums - 1));} else {if (futmp && futmp.direction == 'LR') {//当前级是BT,父级是LR的 情况(childx = location[0] + rectWidth),(childy =location[1] +(yspace + rectHeight) *(elem.currentIndex - notChildIndex) +0.5 * rectHeight);// ctx.strokeRect(childx,childy,rectWidth,rectHeight);} else {(childx = location[0] + 0.5 * rectWidth),(childy =location[1] +(yspace + rectHeight + 20) *(elem.currentIndex + 1 - notChildIndex));// ctx.strokeRect(childx,childy,rectWidth,rectHeight);}}if (elem.currentIndex === elem.brotherNums - 1) {notChildIndex = 0;}}} else {(childx =location[0] +(elem.currentIndex - halfLength1) * rectWidth +(elem.currentIndex - halfLengthFloor1) * elem.xspacing),(childy = location[1]);// ctx.strokeRect(childx,childy,rectWidth,rectHeight);}elem.location = [childx, childy];OnlyLevelArr.forEach((onele) => {if (elem.id === onele.id) {onele.location = [childx, childy];return;}});let obj = { childx, childy, ...elem };elemList.push(obj);});}});}setDIVList(elemList);}

代码3:

 function renderEdge(edgeDatas, x, y) {//渲染线条if (edgeDatas.length && edgeDatas.length >= 1) {ctx.strokeStyle = '#9eb0c4';edgeDatas.forEach((edEle) => {// 绘制连接线let tmp = OnlyLevelArr.find(function (e) {return edEle.id === e.id;}); //拿到定位let futmp = OnlyLevelArr.find(function (e) {return edEle.parentId === e.id;}); //拿到定位let yetmp = OnlyLevelArr.find(function (e) {return futmp?.parentId === e.id;}); //拿到父级的父级定位edEle.location = tmp.location || defaultLocation;let center = [x + 0.5 * rectWidth, y + rectHeight],childCenter = [edEle.location[0] + 0.5 * rectWidth,edEle.location[1],]; //设定两个矩形连接点if (edEle.level !== 0) {if (futmp.direction === 'BT') {if (yetmp && yetmp.direction === 'LR') {ctx.beginPath();ctx.moveTo(center[0] + 0.5 * rectWidth,center[1] - rectHeight + 20,);ctx.lineTo(childCenter[0], center[1] - rectHeight + 20);ctx.lineTo(childCenter[0], childCenter[1]);ctx.stroke();} else {ctx.beginPath();ctx.moveTo(center[0], center[1]);ctx.lineTo(center[0],center[1] + (childCenter[1] - center[1]) / 2,);ctx.lineTo(childCenter[0],center[1] + (childCenter[1] - center[1]) / 2,);ctx.lineTo(childCenter[0], childCenter[1]);ctx.stroke();}} else if (futmp.direction === 'LR') {if (yetmp && yetmp.direction === 'LR') {ctx.beginPath();ctx.moveTo(center[0] + 0.5 * rectWidth,center[1] - rectHeight + 20,);ctx.lineTo(center[0] + 0.5 * rectWidth + 20,center[1] - rectHeight + 20,);ctx.lineTo(center[0] + 0.5 * rectWidth + 20,childCenter[1] - 20,);ctx.lineTo(center[0] + 0.5 * rectWidth + 20,childCenter[1] - 20,);ctx.lineTo(childCenter[0], childCenter[1] - 20);ctx.lineTo(childCenter[0], childCenter[1]);ctx.stroke();} else {ctx.beginPath();ctx.moveTo(center[0], center[1]);ctx.lineTo(center[0], childCenter[1] - 20);ctx.lineTo(childCenter[0], childCenter[1] - 20);ctx.lineTo(childCenter[0], childCenter[1]);ctx.stroke();}}}// 连接同级节点if (tmp.connectNode) {let needconnectNode = OnlyLevelArr.find(function (e) {return tmp.connectNode === e.id;}); //拿到要连接点的定位if (tmp.location[0] === needconnectNode.location[0]) {//x值相等 把x+20 然后相连ctx.beginPath();ctx.moveTo(tmp.location[0] + 0.5 * rectWidth,tmp.location[1] + rectHeight,);ctx.lineTo(tmp.location[0] + 0.5 * rectWidth,tmp.location[1] + rectHeight + 20,);ctx.lineTo(needconnectNode.location[0] + rectWidth + 20,tmp.location[1] + rectHeight + 20,);ctx.lineTo(needconnectNode.location[0] + rectWidth + 20,needconnectNode.location[1] + 20,);ctx.stroke();} else if (tmp.location[1] === needconnectNode.location[1]) {//y值相等 把y+20 然后连接ctx.beginPath();ctx.moveTo(tmp.location[0] + 0.5 * rectWidth,tmp.location[1] + rectHeight,);ctx.lineTo(tmp.location[0] + 0.5 * rectWidth,tmp.location[1] + rectHeight + 20,);ctx.lineTo(needconnectNode.location[0] + 0.5 * rectWidth,tmp.location[1] + rectHeight + 20,);ctx.lineTo(needconnectNode.location[0] + 0.5 * rectWidth,needconnectNode.location[1] + rectHeight,);ctx.stroke();}}if (edEle.child && edEle.child.length && edEle.child.length >= 1) {renderEdge(edEle.child, edEle.location[0], edEle.location[1]);} else {return;}});}}

代码4:

 <div className={styles.Diagram}><divref={containerRef}style={{width: `100%`,height: `100%`,overflow: 'auto',position: 'relative',}}><canvasref={graphRef}width={mycanvas.width}height={mycanvas.height}style={{ display: 'block', position: 'absolute', top: 0, left: 0 }}></canvas>{DIVList &&DIVList.length >= 1 &&DIVList.map((ele) => (<divclassName={styles.DIVList}style={{position: 'absolute',top: `${ele.childy}px`,left: `${ele.childx}px`,zIndex: '999',width: `${rectWidth}px`,height: `${rectHeight}px`,}}><div className={styles.topPart}><div className={styles.title}>{ele?.label}</div><div className={styles.area}>{ele.id}</div><div className={styles.content}>{ele?.label}</div></div><div className={styles.linePart}></div><div className={styles.btmPart}><div className={styles.ltYuan}>20</div><div className={styles.rtYuan}>2.5</div><div className={styles.lbYuan}>50</div><div className={styles.rbYuan}>20.8</div></div></div>))}{/* 左上角 图例 */}<div className={styles.wraplegend}><div className={classNames([styles.legend])}><div className={styles.topPart}><div className={styles.title}>22</div><div className={styles.area}>22</div><div className={styles.content}>22</div></div><div className={styles.linePart}></div><div className={styles.btmPart}><div className={styles.ltYuan}>20</div><div className={styles.rtYuan}>2.5</div><div className={styles.lbYuan}>50</div><div className={styles.rbYuan}>20.8</div></div><div className={styles.textName}>入口表</div></div><div className={classNames([styles.legend])}><div className={styles.topPart}><div className={styles.title}>22</div><div className={styles.area}>22</div><div className={styles.content}>22</div></div><div className={styles.linePart}></div><div className={styles.btmPart}><div className={styles.ltYuan}>20</div><div className={styles.rtYuan}>2.5</div><div className={styles.lbYuan}>50</div><div className={styles.rbYuan}>20.8</div></div><div className={styles.textName}>出口表</div></div></div></div><ButtononClick={handleClick}style={{ position: 'absolute', top: '10px', right: '10px' }}>下载关系图</Button></div>

成果如图:

完整代码包在这 :js纯canvas绘制关系图(横纵双向)节点层级个数不受限-Javascript文档类资源-CSDN下载

js 纯canvas实现横纵双向关系图相关推荐

  1. 使用pyechart生成节点关系图

    前言 前几天写过使用networkx生成节点关系图,但是,生成图的图中节点的分布,始终是不如人意,没找到合适的布局图参考这里.后来发现可以使用echart来实现类似的目的,就尝试了一下,不断调整之后得 ...

  2. python人像绘制_python 绘制三国人物关系图

    author:weizhendong data:2019.12.19 func:绘制三国演义人物关系图 """ import codecs import jieba.po ...

  3. [js] 纯函数和函数式编程有什么关系?

    [js] 纯函数和函数式编程有什么关系? 函数式编程是一种编程思想,纯函数是这种思想的基本要实现函数式编程,我们所封装的方法应该是抽象的,应该是和外部状态无关系的,也就需要是纯函数的,这样才能保证抽象 ...

  4. 画线标记html,markline.js——轻量级canvas绘制标记线的库

    这段时间要做的是一个数据可视化的小型项目.其中最基本要求是实现两点之间的迁徙关系(比如同一个用户不同时间上网的地点)用一条有向线段(markline)联系在一起.很自然的我一开始想的就是采用百度的ec ...

  5. 【图形基础篇】02 # 指令式绘图系统:如何用Canvas绘制层次关系图?

    说明 [跟月影学可视化]学习笔记. 如何用 Canvas 绘制几何图形? 1. Canvas 元素和 2D 上下文 Canvas 元素上的 width 和 height 属性不等同于 Canvas 元 ...

  6. skycons.js 基于canvas的天气动态js插件

    skycons.js 基于canvas的天气动态js插件 skycons.js是一个开源的javascript天气动画图标渲染器.相当于gif动图一样. skycons CDN地址:https://w ...

  7. java使用重绘实现拖动_原生JS使用Canvas实现拖拽式绘图功能

    一.实现的功能 1.基于oop思想构建,支持坐标点.线条(由坐标点组成,包含方向).多边形(由多个坐标点组成).圆形(包含圆心坐标点和半径)等实体 2.原生JavaScript实现,不依赖任何第三方j ...

  8. captcha.js一个生成验证码的插件,使用js和canvas生成

    一.captcha`captcha.js`是一个生成验证码的插件,使用js和canvas生成的,确保后端服务被暴力攻击,简单判断人机以及系统的安全性,体积小,功能多,支持配置. 验证码插件内容,包含1 ...

  9. 原生js实现canvas气泡冒泡效果

    说明: 本文章主要分为ES5和ES6两个版本 ES5版本是早期版本,后面用ES6重写优化的,建议使用ES6版本. 1, 原生js实现canvas气泡冒泡效果的插件,api丰富,使用简单 2, 只需引入 ...

最新文章

  1. Ansible中文手册
  2. linux的tmp目录不会清空,关于Linux系统中/tmp目录的清除问题
  3. python数字类型转换函数_python中的各种数据类型中的数据格式转换
  4. ASCII码、ISO8859-1、Unicode、GBK和UTF-8 的区别
  5. 一名游戏开发者的告白
  6. jQuery的事件1——on,one
  7. 去死吧!USB转串口!!!
  8. ubuntu-14.04.2-desktop使用方法
  9. java 蓝桥杯算法训练 快速排序
  10. oracle 循环select查询的结构集,执行insert到指定表保存
  11. 干货来袭!java核心技术卷一pdf
  12. vue入门:(class与style绑定)
  13. div中字符串自动换行
  14. 为-微软-重写-TechNet Library-中-Microsoft Lync Server 2010
  15. 公专网集群对讲系统在城市执法过程中的应用
  16. 百望系统网络配置服务器地址,各省百旺参数设置服务器地址
  17. 氨基酸在php的溶液中,氨基酸等电点的计算和应用.ppt
  18. phpmyadmin mysql_phpmyadmin
  19. netbeans卸载
  20. #10064 「一本通 3.1 例 1」黑暗城堡(spfa+乘法原理)

热门文章

  1. NotePad++ 添加Jass插件
  2. 在说下一个关于数据库的项目
  3. Python 依赖管理及打包三方库 Poetry
  4. 2017.1.13【初中部 GDKOI】模拟赛B组 我要的幸福 题解
  5. sql线上线下数据库同步方式
  6. 前缀表达式(波兰表达式)介绍及其代码实现(Java)
  7. 高光谱数据之多元散射校正
  8. 仿智联招聘实现简历导出功能
  9. 内网服务器外网连接SSH远程端口转发实战详解
  10. 硬件篇:智能家居DIY(esp8266+继电器+blinker+天猫精灵实现开关灯)