先看图:
需求如上图所示,为了不占用太多的空间,展示没有固定的方向,前期去找了很多antv/G6,echarts等插件,一个graph只有一个方向,要么从上往下,要么从左到右,都不满足需求,于是直接用canvas手写了。我设计的参数如下:
思路如下:
- 如何渲染出节点(包含矩形框和⚪,看作一体),计算摆放位置;
- 由于设计的数据结构是tree结构的,有child字段一级包裹一级,先处理一份同级数据。(此部分代码见代码1,全部代码见最后)
- 根据当前节点及兄弟节点的个数处理居中摆放,根据direction判断是从左往右还是从上至下;(此部分代码见代码2,全部代码见最后)
- 如何渲染线段,把节点相连接;
- 根据第一步节点的坐标来画连接线(此部分代码见代码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显示 且 connectNode
if (
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}>
<div
ref={containerRef}
style={{
width: `100%`,
height: `100%`,
overflow: 'auto',
position: 'relative',
}}
>
<canvas
ref={graphRef}
width={mycanvas.width}
height={mycanvas.height}
style={{ display: 'block', position: 'absolute', top: 0, left: 0 }}
></canvas>
{DIVList &&
DIVList.length >= 1 &&
DIVList.map((ele) => (
<div
className={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>
<Button
onClick={handleClick}
style={{ position: 'absolute', top: '10px', right: '10px' }}
>
下载关系图
</Button>
</div>
成果如图:
完整代码包在这 :js纯canvas绘制关系图(横纵双向)节点层级个数不受限-Javascript文档类资源-CSDN下载