VTable-Gantt:强大、高性能的开源甘特图组件
甘特图的基本概念
在项目管理中,甘特图是常用的呈现项目任务时间安排和进展情况的工具。
我们将甘特图分为以下几个部分:
表格甘特图
VTable-Gantt是基于VTable表格组件和画布渲染引擎VRender构建的一款功能强大的甘特图绘制工具,可以帮助开发者轻松创建和管理甘特图。
核心能力如下:
使用 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);
演示结果:

甘特图的主要功能
表格左侧多列信息展示
甘特图整个结构的左侧是一个完整的表格容器,因此可以支持丰富的列信息显示和自定义渲染能力。
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的配置项,其中可以配置以下内容:

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

自定义渲染需要了解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' } } ] },
效果如下:

外边框
表格的边框可能与内部网格线的样式不同,可以通过frame.outerFrameStyle配置项自定义甘特图的外边框。
const option = { overscrollBehavior: 'none', records, taskListTable: { }, frame: { outerFrameStyle: { borderLineWidth: 20, borderColor: 'black', cornerRadius: 8 }, },
效果如下:

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

标记线
在甘特图中,经常需要标记一些重要的日期,我们通过配置项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] } } ],
效果如下:

容器网格线
通过网格配置项可以自定义右侧任务栏背景网格线的样式。包括背景颜色、水平和垂直方向的线宽、线类型等。
例如:
grid: { verticalLine: { lineWidth: 3, lineColor: 'black' }, horizontalLine: { lineWidth: 2, lineColor: 'red' } },
效果如下:

相互作用
任务栏
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)' }, },
效果如下:

调整左侧表格的宽度
通过将 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 } },
效果如下:

完整示例: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

更全面的编辑功能可以参考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], } },
效果如下:

概括
本文详细介绍了@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