从0到1 抄一个甘特图

4,899 阅读2分钟

前文概述

日常需求需要一个可以展示项目集合总时间流程,以及每个环节占的时间的总览。

image.png

选型

基于项目开发是vue2.x所以倾向找原本就是vue所写的甘特图插件。github 里vue gantt排名前三。

插件gantt-schedule-timeline-calendargantt-elasticvue-gantt-chart
优点1. 节点交互充分,能满足所有操作。 2. 样式优雅。 3. 表格slot1. 节点有事件吐出 2. 数据配置简单1. 虚拟滚动一定不会卡 2. 支持自定义描述和容器块
缺点要钱,不开源不维护,前面插件的前身。条类型固定样式固定无法调节整体样式怪异,左侧配置需要自己传component去渲染,没有父子关系的数据概念

综合而言,上述的gantt-elastic比较符合,只要把Chart row进行改造即可使用。

关键代码分析

插件关键步骤图 image.png

// 折叠操作 
makeTaskTree(task, tasks) {
  for (let i = 0, len = tasks.length; i < len; i++) {
    let current = tasks[i];
    if (current.parentId === task.id) {
      if (task.parents.length) {
        task.parents.forEach((parent) => current.parents.push(parent));
      }
      if (!Object.prototype.propertyIsEnumerable.call(task, '__root')) {
        current.parents.push(task.id);
        current.parent = task.id;
      } else {
        current.parents = [];
        current.parent = null;
      }
      current = this.makeTaskTree(current, tasks); // 递归找完一个点的下游的父节点
      task.allChildren.push(current.id);
      task.children.push(current.id);
      current.allChildren.forEach((childId) => task.allChildren.push(childId));
    }
  }
  return task;
}

因为形成树结构,后续折叠操作只需要修改tasks里的状态key,全局监听tasks即可完成filter 重新画。
// 滚动同步,chart监听wheel事件 通过refs获取每个模块修改scrollLeft,scrollTop属性。
    onWheelChart(ev) {
      // if (!ev.shiftKey && ev.deltaX === 0) {
      let top = this.state.options.scroll.top + ev.deltaY;
      const chartClientHeight = this.state.options.rowsHeight;
      const scrollHeight = this.state.refs.chartGraph.scrollHeight - chartClientHeight;
      if (top < 0) {
        top = 0;
      } else if (top > scrollHeight) {
        top = scrollHeight;
      }
      this.scrollTo(null, top);
      let left = this.state.options.scroll.left + ev.deltaX;
      const chartClientWidth = this.state.refs.chartScrollContainerHorizontal.clientWidth;
      const scrollWidth = this.state.refs.chartScrollContainerHorizontal.scrollWidth - chartClientWidth;
      if (left < 0) {
        left = 0;
      } else if (left > scrollWidth) {
        left = scrollWidth;
      }
      this.scrollTo(left);
    },
// row components group类型 子模块全集合一起渲染
    foundChildrens() {
      const childrens = this.task.allChildren.map((id) => {
        return this.root.getTask(id);
      });
      this.task.group = childrens;
    }
    
// 算row展示长度是否可以放下text情况。
   calText(task) {
      let marginLeft = this.root.style['chart-row-inline-text']['marginLeft'];
      let text = task.text;
      let textWidth = this.root.state.ctx.measureText(text).width + marginLeft;
      if (textWidth < task.width) {
        return text;
      } else {
        let ellipsisWidth = this.root.state.ctx.measureText('...').width;
        const textArr = text.split('');
        let currentWidth = ellipsisWidth;
        const res = [];
        for (let text of textArr) {
          const textWidth = this.root.state.ctx.measureText(text).width;
          if (textWidth + currentWidth > task.width) {
            break;
          } else {
            currentWidth += textWidth;
            res.push(text);
          }
        }
        return res.length ? res.join('') + '...' : '';
      }
    }
    

最终我们发现,图是否能成功画完的关键在于最小粒度的宽高(row,column)明确,所有的计算都基于此。gantt-elastic 通过provide自身实例在每一块都可以记录下对应的宽高共享使用。

后续改造

  • 修改为virtual scroll,避免渲染压力。只需要记录滚动距离。通过Math.floor(top/taskHeight)即可算出当前startIdx。 然后修改 visibleTasks 即可重画。

觉得有用的话点个赞吧,或者有更好的方案也可以在评论区留言,谢谢各位。

参考链接

  1. gantt-elastic 源码
  2. virtual scroll