甘特图的基本概念
在项目管理中,甘特图是一种常用的工具,用于展示项目任务的时间安排和进度。
我们将甘特图拆分成以下几个部分:
- 左侧任务列表:显示项目的任务列表,通常在图的左侧。
- 顶部时间轴:显示项目的时间范围,通常在图的顶部或底部。
- 任务条:表示每个任务的开始和结束时间。
- 网格线:用于分隔时间轴和任务条,使图表更加清晰。
- 标记线:用于标记重要时间点。
- 分割线:用于分隔任务列表和时间轴,使图表更加清晰。
VTable-Gantt
VTable-Gantt 是一款基于 VTable表格组件及canvas渲染引擎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 容器。
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,其中可以配置:
- 左侧表格整体宽度:通过 tableWidth 配置项,可以设置任务列表表格的整体宽度。
- 列信息: 通过 columns,可以定义任务信息表格的列信息和各列宽度。
- 样式配置: 通过 theme.headerStyle 和theme.bodyStyle 配置项,可以设置表头和表体的样式。
- 宽度限制: 通过 minTableWidth 和 maxTableWidth 配置项,可以设置任务列表的最小和最大宽度。
完整代码见:https://visactor.io/vtable/demo/gantt/gantt-basic
具体配置可参考官网配置:https://visactor.io/vtable/option/Gantt#taskListTablehttps://on/Gantt#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:
'<svg t="1724675965803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4299" width="200" height="200"><path d="M53.085678 141.319468C23.790257 141.319468 0 165.035326 0 194.34775L0 918.084273C0 947.295126 23.796789 971.112572 53.085678 971.112572L970.914322 971.112572C1000.209743 971.112572 1024 947.396696 1024 918.084273L1024 194.34775C1024 165.136896 1000.203211 141.319468 970.914322 141.319468L776.827586 141.319468 812.137931 176.629813 812.137931 88.275862C812.137931 68.774506 796.328942 52.965517 776.827586 52.965517 757.32623 52.965517 741.517241 68.774506 741.517241 88.275862L741.517241 176.629813 741.517241 211.940158 776.827586 211.940158 970.914322 211.940158C961.186763 211.940158 953.37931 204.125926 953.37931 194.34775L953.37931 918.084273C953.37931 908.344373 961.25643 900.491882 970.914322 900.491882L53.085678 900.491882C62.813237 900.491882 70.62069 908.306097 70.62069 918.084273L70.62069 194.34775C70.62069 204.087649 62.74357 211.940158 53.085678 211.940158L247.172414 211.940158C266.67377 211.940158 282.482759 196.131169 282.482759 176.629813 282.482759 157.128439 266.67377 141.319468 247.172414 141.319468L53.085678 141.319468ZM211.862069 176.629813C211.862069 196.131169 227.671058 211.940158 247.172414 211.940158 266.67377 211.940158 282.482759 196.131169 282.482759 176.629813L282.482759 88.275862C282.482759 68.774506 266.67377 52.965517 247.172414 52.965517 227.671058 52.965517 211.862069 68.774506 211.862069 88.275862L211.862069 176.629813ZM1024 353.181537 1024 317.871192 988.689655 317.871192 35.310345 317.871192 0 317.871192 0 353.181537 0 441.457399C0 460.958755 15.808989 476.767744 35.310345 476.767744 54.811701 476.767744 70.62069 460.958755 70.62069 441.457399L70.62069 353.181537 35.310345 388.491882 988.689655 388.491882 953.37931 353.181537 953.37931 441.457399C953.37931 460.958755 969.188299 476.767744 988.689655 476.767744 1008.191011 476.767744 1024 460.958755 1024 441.457399L1024 353.181537ZM776.937913 582.62069C796.439287 582.62069 812.248258 566.811701 812.248258 547.310345 812.248258 527.808989 796.439287 512 776.937913 512L247.172414 512C227.671058 512 211.862069 527.808989 211.862069 547.310345 211.862069 566.811701 227.671058 582.62069 247.172414 582.62069L776.937913 582.62069ZM247.172414 688.551724C227.671058 688.551724 211.862069 704.360713 211.862069 723.862069 211.862069 743.363425 227.671058 759.172414 247.172414 759.172414L600.386189 759.172414C619.887563 759.172414 635.696534 743.363425 635.696534 723.862069 635.696534 704.360713 619.887563 688.551724 600.386189 688.551724L247.172414 688.551724ZM776.827586 211.940158 741.517241 176.629813 741.517241 247.328574C741.517241 266.829948 757.32623 282.638919 776.827586 282.638919 796.328942 282.638919 812.137931 266.829948 812.137931 247.328574L812.137931 176.629813 812.137931 141.319468 776.827586 141.319468 247.172414 141.319468C227.671058 141.319468 211.862069 157.128439 211.862069 176.629813 211.862069 196.131169 227.671058 211.940158 247.172414 211.940158L776.827586 211.940158ZM282.482759 176.629813C282.482759 157.128439 266.67377 141.319468 247.172414 141.319468 227.671058 141.319468 211.862069 157.128439 211.862069 176.629813L211.862069 247.328574C211.862069 266.829948 227.671058 282.638919 247.172414 282.638919 266.67377 282.638919 282.482759 266.829948 282.482759 247.328574L282.482759 176.629813Z" fill="#389BFF" p-id="4300"></path></svg>'
});
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支持五种时间粒度:'day' | 'week' | 'month' | 'quarter' | 'year'
。
通过 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
},
},
效果如下:
### 横纵分割线
同时支持表头和body的横向分割线,及左侧信息表和右侧任务列表的分割线。通过 frame.horizontalSplitLine
配置项,可以自定义水平分割线的样式。frame.verticalSplitLine
配置项,可以自定义垂直分割线的样式。
### 标记线
在gantt甘特图中通常需要标记一些重要的日期,我们通过配置项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
配置项,可以自定义右侧任务条背景网格线的样式。包括背景色、横纵方向的线宽、线型等。
例如:
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 文件。
<script src="https://unpkg.com/@visactor/vtable-editors@latest/dist/vtable-editors.min.js"></script>
<script>
const inputEditor = new VTable.editors.InputEditor();
</script>
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';
// 注册编辑器到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 配置中指定使用的编辑器:
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在监听有移位事件时将顺序同步到任务条区域显示。
例如:
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 的开源建设:
github:https://github.com/VisActor
官方网站:www.visactor.io/
Discord:discord.gg/3wPyxVyH6m
飞书群:
微信公众号:Twiter:twitter.com/xuanhun1