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

2 篇文章 0 订阅
1 篇文章 0 订阅

先看图:

 需求如上图所示,为了不占用太多的空间,展示没有固定的方向,前期去找了很多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显示 且 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下载

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值