记录使用echarts的graph类型绘制流程图全过程(一)-x,y位置的计算

时间:2024-01-25 14:53:38

先说下本次案例业务需求,输入2个节点,获取数据后绘制出2个节点间的路径,之前使用的是网状图,但是网状图的效果不佳,需要转换成流程图的模式:

那么如何在不修改数据的情况下,实现类似效果尼?

看了下echarts的graph类型,可以实现类似的,下面是官方的实例

从实例中可以看出,难点在于节点的显示位置x,y和曲线的设置。业务数据中:
1、节点的数量不定,关系的数量不定,
2、后台返回的数据只有单独的节点信息和关系信息

实现思路:

1、分析数据,获取前后节点关系,获得行数位置(节点的xIndex信息)

在节点数组中找到开始节点并设置xIndex 为1,然后从它开始找第二层的节点

// 获取节点的x轴顺序
    setNodeOrder () {
      this.entityList.forEach(item => {
        if (item.id === this.startNode) {
          item.xIndex = 1;
        }

        // 设置一个对象,记录节点信息,用于后面查询信息,比数组查询来的快
        this.nodesObj[item.id] = item;
      });

      this.findNodeOrder(2, this.startNode);
    }
    
    // 广度遍历--按顺序找到第index层的数据,并设置对应的参数,再层层递进
    findNodeOrder (xIndex, sId){

    }

想一下,如果是第二层的节点,那么应该是关系中,源是startNode或者目的节点是startNode,如此类推,层层递进,
这里需要使用广度遍历而不是深度遍历,设定2个数组,currentQueue记录当前层的节点,afterQueue记录下一层的节点,当currentQueue层遍历完后,将afterQueue变成currentQueue,继续遍历,直至结束,核心代码如下:

......

       let nextIds = [];
        this.relationList.forEach(item => {
          // 源
          if (item.source === sId) {
            if (!this.nodesObj[item.target].xIndex) {
              this.nodesObj[item.target].xIndex = xIndex;
              nextIds.push(item.target);
            }
          }
          // 目的
          if (item.target === sId) {
            if (!this.nodesObj[item.source].xIndex) {
              this.nodesObj[item.source].xIndex = xIndex;
              nextIds.push(item.source);
            }
          }
        });

        let nextIdsLen = nextIds.length;

        // 1、没有当前的队列,没有后续的队列,有nextIds
        if (
          !this.currentQueue.length &&
          !this.afterQueue.length &&
          nextIdsLen
        ) {
          this.currentQueue = nextIds;
          this.currentXindex = this.currentXindex + 1;
          this.setNextOrder();
        } else if (this.currentQueue.length && nextIdsLen) {
          // 2、有当前的队列在遍历,排队
          this.afterQueue = this.afterQueue.concat(nextIds);
        } else if (!this.currentQueue.length && this.afterQueue.length) {
          // 3、没有当前的队列了,有排队的队列,则排队的进去
          if (nextIdsLen) {
            this.afterQueue = this.afterQueue.concat(nextIds);
          }
          this.currentQueue = JSON.parse(JSON.stringify(this.afterQueue));
          this.afterQueue = [];
          this.currentXindex = this.currentXindex + 1;
        }

setNextOrder函数:

    setNextOrder () {
      while (this.currentQueue.length) {
        let id = this.currentQueue.shift();
        this.findNodeOrder(this.currentXindex, id);
      }
    }

2、根据行数信息设置列数的位置(节点的yIndex层的信息)

先排序第一层和第二层的顺序,因为第一层就一个节点,第二层和第一层的关系都是一样的,所以顺序无所谓,可以直接遇到就叠加

// 排完了x,再排列y
        // 先排第一和二行的元素的y,按顺序排
        let maxXindex = 1;
        let secondYIndexObj = {};

        for (let i in this.nodesObj) {
          let item = this.nodesObj[i];
          if (!item.yIndex) {
            maxXindex = item.xIndex > maxXindex ? item.xIndex : maxXindex;
            if (item.xIndex === 1) {
              item.yIndex = 1;
              this.yIndexObj[item.id] = {
                xIndex: 1,
                yIndex: 1
              };
            }
            if (item.xIndex === 2) {
              if (!secondYIndexObj[item.xIndex]) {
                item.yIndex = 1;
                // 记录当前的y轴上的节点个数
                secondYIndexObj[item.xIndex] = 1;
                this.yIndexObj[item.id] = {
                  xIndex: 2,
                  yIndex: 1
                };
              } else {
                item.yIndex = secondYIndexObj[item.xIndex] + 1;
                secondYIndexObj[item.xIndex] = item.yIndex;

                this.yIndexObj[item.id] = {
                  xIndex: 2,
                  yIndex: item.yIndex
                };
              }
            }
          }
        }

但是从第三层开始就要注意了,不能再按照第二层的递增顺序的方法来控制。因为第二层与第三层的关系和数量都不一定,如果随机排列会导致线很乱,最好的方法是根据第二层的顺序,获取第三层的顺序。即将第二层第N个节点有关的的节点设置在第N层。这样如果第二层的数量与第三层的数量相等,那么就完全在一条直线上了。如果数量不等,也不至于那么混乱但是如果第三层有多个节点与第二层的同一个节点有关系,这个时候我选择的是随机选择层级了


        // 从第三层开始
        if (maxXindex > 2) {
          // 后面列的排序根据前一列为准(尽量保证在一条直线上)
          for (let j = 3; j <= maxXindex; j++) {
            for (let i in this.nodesObj) {
              let item = this.nodesObj[i];
              while (item.xIndex === j && !item.yIndex) {
                // 先看跟它一层的节点有多少个
                if (!this.nodeOrders[j]) {
                  let totals = this.findYtotal(j);
                  this.nodeOrders[j] = [];
                  for (let i = 1; i <= totals; i++) {
                    this.nodeOrders[j].push(i);
                  }
                }

                // 找第二层中的对应的层级
                let findX = j - 1;

                // 找到所有的层级
                let findYs = this.findLinkNode(item.id, findX);

                // 找到还有的层级
                let sameArr = findYs.filter(x =>
                  this.nodeOrders[j].includes(x)
                );
                let findY;

                // 找不到按顺序抽取了
                if (!sameArr.length) {
                  // 只能随机选择一个了
                  let ran = Math.floor(
                    Math.random() * this.nodeOrders[j].length
                  );
                  findY = this.nodeOrders[j][ran];

                  this.nodeOrders[j].splice(ran, 1);
                } else {
                  findY = sameArr[0];
                  // 去除该顺序
                  let order;
                  this.nodeOrders[j].find((num, k) => {
                    if (num === findY) {
                      order = k;
                      return true;
                    }
                  });
                  this.nodeOrders[j].splice(order, 1);
                }

                this.yIndexObj[item.id] = {
                  xIndex: j,
                  yIndex: findY
                };
                item.yIndex = findY;
              }
            }
          }
        }

3、设置具体的位置信息

获取图表的中心位置centerY,将起点和终点的y位置放在中间,其他的根据上面获取的xIndex和yIndex的还有根据情况设置的间距(gapX,gapY)及节点的大小(size)计算出每个节点的x和y信息

// 设置节点的位置x,y
    setNodesPositon () {
      for (let i in this.nodesObj) {
        let item = this.nodesObj[i];
        if (item.id === this.startNode) {
          item.y = this.centerY;
          item.x = 1;
        }

        if (!item.x) {
          item.x = this.gapX * (item.xIndex - 1) + this.size / 2;
        }

        if (!item.y && !item.total) {
          item.total = this.findYtotal(item.xIndex);

          if (item.total === 1) {
            item.y = this.centerY;
          } else {
            let middleNum = item.total / 2;
            let ceilNum = Math.ceil(middleNum);

            let topGap;
            let bottomGap;

            let ty = (ceilNum - item.yIndex) * (this.gapY + this.size);
            let by = (item.yIndex - ceilNum) * (this.gapY + this.size);

            if (item.total % 2 === 1) {
              topGap = this.centerY - ty;
              bottomGap = this.centerY + by;
            } else {
              topGap = this.centerY - (ty + this.gapY + 0.5 * this.size);
              bottomGap = this.centerY + (by - 0.5 * this.size);
            }

            if (item.yIndex <= middleNum) {
              item.y = topGap;
            } else {
              item.y = bottomGap;
            }

            // 奇数
            if (item.total % 2 === 1) {
              if (item.yIndex === ceilNum) {
                item.y = this.centerY;
              }
            }
          }
        }
      }
    }