VTable-Gantt:强大、高性能的开源甘特图组件

甘特图的基本概念

在项目管理中,甘特图是常用的呈现项目任务时间安排和进展情况的工具。

我们将甘特图分为以下几个部分:

  • 左侧任务列表:显示项目的任务列表,通常在图表的左侧。
  • 顶部时间轴:展示项目的时间范围,通常位于图表的顶部或底部。
  • 任务条:表示每个任务的开始和结束时间。
  • 网格线:用于分隔时间轴和任务条,使图表更加清晰。
  • 标记线:用于标记重要的时间点。
  • 分隔线:用于分隔任务列表与时间轴,使图表更加清晰。
  • 表格甘特图

    VTable-Gantt是基于VTable表格组件和画布渲染引擎VRender构建的一款功能强大的甘特图绘制工具,可以帮助开发者轻松创建和管理甘特图。

    核心能力如下:

  • 高性能:支持大规模项目数据的快速计算和渲染,保证流畅的用户体验。
  • 灵活布局:支持自定义时间轴、任务栏样式、布局,满足不同的项目管理需求。
  • 强大的交互:提供任务的拖拽、缩放、编辑功能,简化项目管理操作。
  • 丰富的可视化能力:支持信息单元格、任务栏的自定义绘制,提供树形结构展示,增强数据展示的多样性和直观性。 ## 获取@visactor/vtable-gantt 需要注意的是@visactor/vtable-gantt是基于@visactor/vtable 构建的,因此需要先安装@visactor/vtable 才能使用@visactor/vtable-gantt。 使用NPM 包 首先需要在项目根目录下使用以下命令安装:
  • 使用 npm 安装
    npm install @visactor/vtable
    npm install @visactor/vtable-gantt
    
    使用 yarn 安装
    yarn add @visactor/vtable
    yarn add @visactor/vtable-gantt

    VTableGantt 简介

    通过NPM包导入

    在 JavaScript 文件顶部使用 import 来导入 vtable-gantt:

    import {Gantt} from '@visactor/vtable-gantt';
    const ganttInstance = new Gantt(domContainer, option);

    绘制简单甘特图

    在绘制之前,我们需要为VTableGantt准备一个具有宽度和高度的DOM容器。

    
      

    接下来我们创建一个Gantt实例,并传入甘特图配置项:

    import {Gantt} from '@visactor/vtable-gantt';
    const records = [
      {
        id: 1,
        title: 'Task 1',
        developer: 'liufangfang.jane@bytedance.com',
        start: '2024-07-24',
        end: '2024-07-26',
        progress: 31,
        priority: 'P0',
      },
      {
        id: 2,
        title: 'Task 2',
        developer: 'liufangfang.jane@bytedance.com',
        start: '07/24/2024',
        end: '08/04/2024',
        progress: 60,
        priority: 'P0'
      },
      ...
    ];
    
    const columns = [
      {
        field: 'title',
        title: 'title',
        width: 'auto',
        sort: true,
        tree: true,
        editor: 'input'
      },
      {
        field: 'start',
        title: 'start',
        width: 'auto',
        sort: true,
        editor: 'date-input'
      },
      {
        field: 'end',
        title: 'end',
        width: 'auto',
        sort: true,
        editor: 'date-input'
      }
    ];
    const option = {
      overscrollBehavior: 'none',
      records,
      taskListTable: {
        columns,
      },
      taskBar: {
        startDateField: 'start',
        endDateField: 'end',
        progressField: 'progress'
      },
      timelineHeader: {
        colWidth: 100,
        backgroundColor: '#EEF1F5',
        horizontalLine: {
          lineWidth: 1,
          lineColor: '#e1e4e8'
        },
        verticalLine: {
          lineWidth: 1,
          lineColor: '#e1e4e8'
        },
        scales: [
          {
            unit: 'day',
            step: 1,
            format(date) {
              return date.dateIndex.toString();
            },
            style: {
              fontSize: 20,
              fontWeight: 'bold',
              color: 'white',
              strokeColor: 'black',
              textAlign: 'right',
              textBaseline: 'bottom',
              backgroundColor: '#EEF1F5'
            }
          }
        ]
      },
    };
    const ganttInstance = new Gantt(document.getElementById("tableContainer"), option);

    演示结果:

    Image description

    甘特图的主要功能

    表格左侧多列信息展示

    甘特图整个结构的左侧是一个完整的表格容器,因此可以支持丰富的列信息显示和自定义渲染能力。

    import * as VTableGantt from '@visactor/vtable-gantt';
    let ganttInstance;
    const records = [
     ...
    ];
    
    const columns = [
      ....
    ];
    const option = {
      overscrollBehavior: 'none',
      records,
      taskListTable: {
        columns,
        tableWidth: 250,
        minTableWidth: 100,
        maxTableWidth: 600,
        theme: {
          headerStyle: {
            borderColor: '#e1e4e8',
            borderLineWidth: 1,
            fontSize: 18,
            fontWeight: 'bold',
            color: 'red',
            bgColor: '#EEF1F5'
          },
          bodyStyle: {
            borderColor: '#e1e4e8',
            borderLineWidth: [1, 0, 1, 0],
            fontSize: 16,
            color: '#4D4D4D',
            bgColor: '#FFF'
          }
        }
      },
      .....
    };
    ganttInstance = new VTableGantt.Gantt(document.getElementById(CONTAINER_ID), option);

    清单表taskListTable的配置项,其中可以配置以下内容:

  • 左侧表格整体宽度:通过tableWidth配置项可以设置任务列表表格整体的宽度。
  • 列信息:通过列可以定义任务信息表的列信息以及每列的宽度。
  • 样式配置:通过theme.headerStyle和theme.bodyStyle配置项可以设置表格表头和表体的样式。
  • 宽度限制:通过minTableWidth、maxTableWidth配置项可以设置任务列表的最小和最大宽度,完整代码可以参见:https://visactor.io/vtable/demo/gantt/gantt-basic 具体配置可以参考官网配置:https://visactor.io/vtable/option/Gantt#taskListTable 效果如下:
  • Image description

    自定义渲染

    对应官网demo:https://visactor.io/vtable/demo/gantt/gantt-customLayout,该组件提供了丰富的自定义渲染能力。

    Image description

    自定义渲染需要了解VRender的图元属性,具体细节可以参考自定义渲染教程:https://visactor.io/vtable/guide/gantt/gantt_customLayout

    任务栏的自定义渲染

    通过taskBar.customLayout配置项可以自定义任务栏的渲染方式。例如:

    taskBar: {
          startDateField: 'start',
          endDateField: 'end',
          progressField: 'progress',
          customLayout: args => {
            const colorLength = barColors.length;
            const { width, height, index, startDate, endDate, taskDays, progress, taskRecord, ganttInstance } = args;
            const container = new VRender.Group({
              width,
              height,
              cornerRadius: 30,
              fill: {
                gradient: 'linear',
                x0: 0,
                y0: 0,
                x1: 1,
                y1: 0,
                stops: [
                  {
                    offset: 0,
                    color: barColors0[index % colorLength]
                  },
                  {
                    offset: 0.5,
                    color: barColors[index % colorLength]
                  },
                  {
                    offset: 1,
                    color: barColors0[index % colorLength]
                  }
                ]
              },
              display: 'flex',
              flexDirection: 'row',
              flexWrap: 'nowrap'
            });
            const containerLeft = new VRender.Group({
              height,
              width: 60,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'space-around'
              // fill: 'red'
            });
            container.add(containerLeft);
    
            const avatar = new VRender.Image({
              width: 50,
              height: 50,
              image: taskRecord.avatar,
              cornerRadius: 25
            });
            containerLeft.add(avatar);
            const containerCenter = new VRender.Group({
              height,
              width: width - 120,
              display: 'flex',
              flexDirection: 'column'
              // alignItems: 'left'
            });
            container.add(containerCenter);
    
            const developer = new VRender.Text({
              text: taskRecord.developer,
              fontSize: 16,
              fontFamily: 'sans-serif',
              fill: 'white',
              fontWeight: 'bold',
              maxLineWidth: width - 120,
              boundsPadding: [10, 0, 0, 0]
            });
            containerCenter.add(developer);
    
            const days = new VRender.Text({
              text: `${taskDays}天`,
              fontSize: 13,
              fontFamily: 'sans-serif',
              fill: 'white',
              boundsPadding: [10, 0, 0, 0]
            });
            containerCenter.add(days);
    
            if (width >= 120) {
              const containerRight = new VRender.Group({
                cornerRadius: 20,
                fill: 'white',
                height: 40,
                width: 40,
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                justifyContent: 'center', // 垂直方向居中对齐
                boundsPadding: [10, 0, 0, 0]
              });
              container.add(containerRight);
    
              const progressText = new VRender.Text({
                text: `${progress}%`,
                fontSize: 12,
                fontFamily: 'sans-serif',
                fill: 'black',
                alignSelf: 'center',
                fontWeight: 'bold',
                maxLineWidth: (width - 60) / 2,
                boundsPadding: [0, 0, 0, 0]
              });
              containerRight.add(progressText);
            }
            return {
              rootContainer: container
              // renderDefaultBar: true
              // renderDefaultText: true
            };
          },
          hoverBarStyle: {
            cornerRadius: 30
          }
        },

    日期标题的自定义渲染

    通过timelineHeader.scales.customLayout配置项可以自定义日期表头的渲染方式。例如:

    timelineHeader: {
          backgroundColor: '#f0f0fb',
          colWidth: 80,
          scales: [
            {
              unit: 'day',
              step: 1,
              format(date) {
                return date.dateIndex.toString();
              },
              customLayout: args => {
                const colorLength = barColors.length;
                const { width, height, index, startDate, endDate, days, dateIndex, title, ganttInstance } = args;
                const container = new VRender.Group({
                  width,
                  height,
                  fill: '#f0f0fb',
                  display: 'flex',
                  flexDirection: 'row',
                  flexWrap: 'nowrap'
                });
                const containerLeft = new VRender.Group({
                  height,
                  width: 30,
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'center',
                  justifyContent: 'space-around'
                  // fill: 'red'
                });
                container.add(containerLeft);
    
                const avatar = new VRender.Image({
                  width: 20,
                  height: 30,
                  image:
                    ''
                });
                containerLeft.add(avatar);
    
                const containerCenter = new VRender.Group({
                  height,
                  width: width - 30,
                  display: 'flex',
                  flexDirection: 'column'
                  // alignItems: 'left'
                });
                container.add(containerCenter);
                const dayNumber = new VRender.Text({
                  text: String(dateIndex).padStart(2, '0'),
                  fontSize: 20,
                  fontWeight: 'bold',
                  fontFamily: 'sans-serif',
                  fill: 'black',
                  textAlign: 'right',
                  maxLineWidth: width - 30,
                  boundsPadding: [15, 0, 0, 0]
                });
                containerCenter.add(dayNumber);
    
                const weekDay = new VRender.Text({
                  text: VTableGantt.tools.getWeekday(startDate, 'short').toLocaleUpperCase(),
                  fontSize: 12,
                  fontFamily: 'sans-serif',
                  fill: 'black',
                  boundsPadding: [0, 0, 0, 0]
                });
                containerCenter.add(weekDay);
                return {
                  rootContainer: container
                };
              }
            }
          ]
        },

    左侧任务信息表自定义渲染

    可以通过taskListTable.columns.customLayout定义每一列单元格的自定义渲染,也可以通过taskListTable.customLayout全局定义每一列单元格的自定义渲染。

    支持不同的日期尺度粒度

    在常见的业务场景中,可能需要涉及多层时间尺度的展示。VTable-Gantt支持'天' | '周' | '月' | '季度' | '年'五种时间粒度。

    通过timelineHeader.scales.unit配置项可以设置日期刻度的行高、时间单位(如日、周、月等)。

    同时,可以针对不同的时间粒度配置不同的表头样式:

    通过timelineHeader.scales.style配置项可以自定义日期表头的样式。

    通过timelineHeader.scales.rowHeight配置项可以设置日期刻度的行高。

    timelineHeader: {
        colWidth: 100,
        backgroundColor: '#EEF1F5',
       .....
        scales: [
          {
            unit: 'week',
            step: 1,
            startOfWeek: 'sunday',
            format(date) {
              return `Week ${date.dateIndex}`;
            },
            style: {
              fontSize: 20,
              fontWeight: 'bold',
              color: 'white',
              strokeColor: 'black',
              textAlign: 'right',
              textBaseline: 'bottom',
              backgroundColor: '#EEF1F5',
              textStick: true
              // padding: [0, 30, 0, 20]
            }
          },
          {
            unit: 'day',
            step: 1,
            format(date) {
              return date.dateIndex.toString();
            },
            style: {
              fontSize: 20,
              fontWeight: 'bold',
              color: 'white',
              strokeColor: 'black',
              textAlign: 'right',
              textBaseline: 'bottom',
              backgroundColor: '#EEF1F5'
            }
          }
        ]
      },

    效果如下:

    Image description

    外边框

    表格的边框可能与内部网格线的样式不同,可以通过frame.outerFrameStyle配置项自定义甘特图的外边框。

    const option = {
      overscrollBehavior: 'none',
      records,
      taskListTable: {
      },
      frame: {
        outerFrameStyle: {
          borderLineWidth: 20,
          borderColor: 'black',
          cornerRadius: 8
        },
      },

    效果如下:

    Image description

    水平和垂直分割线

    它既支持表格头和表格主体的水平分割线,也支持左侧信息表和右侧任务列表之间的分割线。通过frame.horizo​​ntalSplitLine配置项可以自定义水平分割线的样式。通过frame.verticalSplitLine配置项可以自定义垂直分割线的样式。

    Image description

    标记线

    在甘特图中,经常需要标记一些重要的日期,我们通过配置项markLine来配置这个效果,关键日期通过markLine.date指定,标记线的样式可以通过markLine.style配置项自定义,如果初始化时需要显示这个日期,可以设置markLine.scrollToMarkLine为true。

    例如:

    markLine: [
        {
          date: '2024-07-28',
          style: {
            lineWidth: 1,
            lineColor: 'blue',
            lineDash: [8, 4]
          }
        },
        {
          date: '2024-08-17',
          style: {
            lineWidth: 2,
            lineColor: 'red',
            lineDash: [8, 4]
          }
        }
      ],

    效果如下:

    Image description

    容器网格线

    通过网格配置项可以自定义右侧任务栏背景网格线的样式。包括背景颜色、水平和垂直方向的线宽、线类型等。

    例如:

    grid: {
        verticalLine: {
          lineWidth: 3,
          lineColor: 'black'
        },
        horizontalLine: {
          lineWidth: 2,
          lineColor: 'red'
        }
      },

    效果如下:

    Image description

    相互作用

    任务栏

    taskBar.moveable配置项可以用来设置任务栏是否可拖动。

    taskBar.resizable配置项可用于设置任务栏是否可调整大小。

    对应效果的官网示例:https://visactor.io/vtable/demo/gantt/gantt-interaction-drag-taskBar

    例如:

    taskBar: {
        startDateField: 'start',
        endDateField: 'end',
        progressField: 'progress',
        // resizable: false,
        moveable: true,
        hoverBarStyle: {
          barOverlayColor: 'rgba(99, 144, 0, 0.4)'
        },
      },

    效果如下:

    Image description

    调整左侧表格的宽度

    通过将 frame.verticalSplitLineMoveable 设置为 true,可以使分割线可拖动。

    例如:

    frame: {
        outerFrameStyle: {
          borderLineWidth: 1,
          borderColor: '#e1e4e8',
          cornerRadius: 0
        },
        verticalSplitLine: {
          lineColor: '#e1e4e8',
          lineWidth: 1
        },
        horizontalSplitLine: {
          lineColor: '#e1e4e8',
          lineWidth: 1
        },
        verticalSplitLineMoveable: true,
        verticalSplitLineHighlight: {
          lineColor: 'green',
          lineWidth: 1
        }
      },

    效果如下:

    Image description

    完整示例:https://visactor.io/vtable/demo/gantt/gantt-interaction-drag-table-width

    编辑任务信息

    通过ListTable的编辑功能,可以同步更新数据到任务栏。

    首先确保 VTable 库 @visactor/vtable 和相关编辑器包 @visactor/vtable-editors 已经正确安装,可以使用以下命令进行安装:

    npm install @visactor/vtable-editors
    yarn add @visactor/vtable-editors

    在代码中导入所需类型的编辑器模块:

    import { DateInputEditor, InputEditor, ListEditor, TextAreaEditor } from '@visactor/vtable-editors';

    您也可以通过CDN获取构建好的VTable-Editor文件。

    
    

    目前VTable-ediotrs库提供了四种类型的编辑器,包括文本输入框、多行文本输入框、日期选择器、下拉列表等,大家可以根据自己的需求选择合适的编辑器。(下拉列表编辑器的效果还在优化中,目前比较丑陋。)

    以下是创建编辑器的示例代码:

    const inputEditor = new InputEditor();
    const textAreaEditor = new TextAreaEditor();
    const dateInputEditor = new DateInputEditor();
    const listEditor = new ListEditor({ values: ['女', '男'] });

    在上面的例子中,我们创建了一个文本输入框编辑器(InputEditor)、一个多行文本框编辑器(TextAreaEditor)、一个日期选择器编辑器(DateInputEditor)和一个下拉列表编辑器(ListEditor)。您可以根据实际需求选择合适的编辑器类型。

    在使用编辑器之前,需要将编辑器实例注册到VTable中:

    // import * as VTable from '@visactor/vtable';
    // Register the editor to VTable
    VTable.register.editor('name-editor', inputEditor);
    VTable.register.editor('name-editor2', inputEditor2);
    VTable.register.editor('textArea-editor', textAreaEditor);
    VTable.register.editor('number-editor', numberEditor);
    VTable.register.editor('date-editor', dateInputEditor);
    VTable.register.editor('list-editor', listEditor);

    接下来,需要在列配置中指定要使用的编辑器:

    columns: [
      { title: 'name', field: 'name', editor(args)=>{
        if(args.row%2 === 0)
          return 'name-editor';
        else
          return 'name-editor2';
      } },
      { title: 'age', field: 'age', editor: 'number-editor' },
      { title: 'gender', field: 'gender', editor: 'list-editor' },
      { title: 'address', field: 'address', editor: 'textArea-editor' },
      { title: 'birthday', field: 'birthDate', editor: 'date-editor' },
    ]

    在左侧任务列表的表格中,用户可以通过双击(或其他交互方式)单元格来开始编辑。

    官网上对应效果的示例:https://visactor.io/vtable/demo/gantt/gantt-edit

    Image description

    更全面的编辑功能可以参考VTable的编辑教程:https://visactor.io/vtable/guide/edit/edit_cell

    调整数据顺序

    要启用拖拽重排序功能,ListTable的配置需要在配置中添加rowSeriesNumber,有了行序号,就可以配置该列的样式,要启用重排序,将dragOrder设置为true,当VTable-Gantt监听到shift事件时,会将顺序同步到任务栏区域显示。

    例如:

    rowSeriesNumber: {
        title: '行号',
        dragOrder: true,
        headerStyle: {
          fontWeight: 'bold',
          color: '#134e35',
          bgColor: '#a7c2ff'
        },
        style: {
          borderColor: '#e1e4e8',
          borderColor: '#9fb9c3',
          borderLineWidth: [1, 0, 1, 0],
        }
      },

    效果如下:

    Image description

    概括

    本文详细介绍了@visactor/vtable-gantt组件现有的功能,其基础能力和扩展能力已经可以满足当前大部分场景下甘特图的需求,组件还在不断完善中,如有任何建议或使用问题,欢迎交流。

    欢迎交流

    最后,我们真诚欢迎各位对数据可视化感兴趣的朋友参与到VisActor的开源建设中来:

    VChart:VChart 官方网站、VChart Github(感谢 Star)

    VTable:VTable 官网,VTable Github(感谢 Star)

    VMind:VMind 官方网站、VMind Github(感谢 Star)

    官方网站:www.visactor.io/

    Discord:discord.gg/3wPyxVyH6m

    飞书群(外网):打开链接扫码

    推特:twitter.com/xuanhun1

    github:github.com/VisActor