在项目管理中,甘特图是最常用的工具之一。传统的静态甘特图虽然能展示任务时间线,但当项目计划需要频繁调整时,静态图表就显得力不从心了。这就是为什么越来越多的团队开始转向动态交互式甘特图。
想象一下这样的场景:你的团队正在开发一个新产品,原定两周完成的需求调研突然需要延长三天,同时UI设计需要提前开始。如果使用静态图表,你需要手动修改每个相关任务的开始和结束时间,然后重新生成图表。而使用ECharts打造的动态甘特图,你可以直接拖拽任务条来调整时间,所有依赖任务会自动更新位置,团队成员立刻就能看到最新进度。
ECharts作为百度开源的优秀可视化库,特别适合构建这类交互式图表。它不仅支持基础的条形图展示,还提供了丰富的事件处理机制。这意味着你可以实现:
首先确保你的项目已经引入了ECharts。如果是新项目,可以通过CDN快速引入:
html复制<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
或者使用npm安装:
bash复制npm install echarts --save
让我们从最简单的甘特图开始。ECharts中甘特图本质上是特殊的条形图,关键配置在于x轴使用时间类型,y轴显示任务名称。
javascript复制var option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'category',
data: ['需求分析', 'UI设计', '开发', '测试', '上线']
},
series: [{
name: '任务时间',
type: 'bar',
data: [
[new Date('2023-01-01'), new Date('2023-01-07'), '需求分析'],
[new Date('2023-01-08'), new Date('2023-01-14'), 'UI设计'],
[new Date('2023-01-15'), new Date('2023-02-05'), '开发'],
[new Date('2023-02-06'), new Date('2023-02-10'), '测试'],
[new Date('2023-02-11'), new Date('2023-02-11'), '上线']
],
encode: {
x: [0, 1],
y: 2
}
}]
};
这段代码创建了一个最基本的甘特图,展示了典型软件开发项目的各个阶段。每个条形代表一个任务,x轴是时间线,y轴是任务名称。
让甘特图真正活起来的关键是添加拖拽功能。ECharts提供了丰富的图形事件支持,我们可以利用这些事件实现任务条的拖拽调整。
首先需要启用dataZoom组件,它不仅能用于缩放,还能配合拖拽:
javascript复制option.dataZoom = [{
type: 'slider',
xAxisIndex: 0,
filterMode: 'filter'
}];
然后添加事件监听实现拖拽:
javascript复制myChart.on('mousedown', function(params) {
if (params.componentType === 'series') {
// 记录初始位置
var startX = params.event.offsetX;
var startValue = params.value[0];
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
function moveHandler(e) {
// 计算移动距离对应的时间变化
var pixelExtent = myChart.getModel().getComponent('xAxis').axis.scale._extent;
var timePerPixel = (pixelExtent[1] - pixelExtent[0]) / myChart.getWidth();
var timeDelta = (e.offsetX - startX) * timePerPixel;
// 更新数据
var newStart = new Date(startValue.getTime() + timeDelta);
option.series[0].data[params.dataIndex][0] = newStart;
option.series[0].data[params.dataIndex][1] = new Date(newStart.getTime() + (params.value[1] - params.value[0]));
myChart.setOption(option);
}
function upHandler() {
document.removeEventListener('mousemove', moveHandler);
document.removeEventListener('mouseup', upHandler);
}
}
});
默认的tooltip可能无法显示足够详细的任务信息。我们可以自定义tooltip的formatter函数:
javascript复制option.tooltip = {
trigger: 'axis',
formatter: function(params) {
var task = params[0];
var start = task.value[0];
var end = task.value[1];
var duration = (end - start) / (1000 * 60 * 60 * 24);
return `
<div style="padding:5px">
<strong>${task.seriesName}</strong><br/>
任务: ${task.value[2]}<br/>
开始: ${start.toLocaleDateString()}<br/>
结束: ${end.toLocaleDateString()}<br/>
历时: ${duration}天<br/>
负责人: ${task.data.owner || '未分配'}
</div>
`;
}
};
实际项目中,任务通常是有层级的。我们可以通过yAxis的配置实现任务分组:
javascript复制option.yAxis = {
type: 'category',
data: [
{ value: '项目阶段1', textStyle: { fontWeight: 'bold' } },
' 任务1.1',
' 任务1.2',
{ value: '项目阶段2', textStyle: { fontWeight: 'bold' } },
' 任务2.1',
' 任务2.2'
]
};
配合series的itemStyle可以为不同层级设置不同样式:
javascript复制series: [{
// ...
itemStyle: {
normal: {
color: function(params) {
var name = option.yAxis.data[params.dataIndex];
return name.trim() === name ? '#5470c6' : '#91cc75';
}
}
}
}]
使用ECharts的markLine可以显示任务间的依赖关系。首先需要在数据中记录依赖:
javascript复制var data = [
{
name: '任务1',
start: new Date('2023-01-01'),
end: new Date('2023-01-07'),
depends: []
},
{
name: '任务2',
start: new Date('2023-01-08'),
end: new Date('2023-01-14'),
depends: ['任务1']
}
// ...
];
然后添加markLine配置:
javascript复制series: [{
// ...
markLine: {
data: [],
symbol: ['none', 'arrow'],
lineStyle: { color: '#999', type: 'dashed' },
silent: true
}
}]
最后在数据加载时生成依赖线:
javascript复制function generateDependencies() {
var dependencies = [];
data.forEach(function(task, index) {
task.depends.forEach(function(depName) {
var depIndex = data.findIndex(function(t) { return t.name === depName; });
if (depIndex >= 0) {
dependencies.push([{
coord: [data[depIndex].end, depIndex]
}, {
coord: [task.start, index]
}]);
}
});
});
option.series[0].markLine.data = dependencies;
myChart.setOption(option);
}
当任务数量超过100时,可能会遇到性能问题。以下是几个优化建议:
javascript复制series: [{
progressive: 200,
progressiveThreshold: 500
}]
javascript复制series: [{
itemStyle: { opacity: 0.8 },
emphasis: { itemStyle: { opacity: 1 } }
}]
javascript复制dataZoom: [{
type: 'inside',
start: 0,
end: 20
}]
实际项目中,甘特图数据通常来自后端API。这里提供一个与REST API集成的示例:
javascript复制async function loadGanttData() {
try {
const response = await fetch('/api/projects/current/tasks');
const data = await response.json();
// 转换API数据为ECharts格式
const tasks = data.map(task => ({
name: task.name,
start: new Date(task.start_date),
end: new Date(task.end_date),
owner: task.assignee,
depends: task.dependencies
}));
// 更新图表
updateChart(tasks);
} catch (error) {
console.error('加载数据失败:', error);
}
}
function updateChart(tasks) {
option.yAxis.data = tasks.map(t => t.name);
option.series[0].data = tasks.map(t => [t.start, t.end, t.name]);
myChart.setOption(option);
generateDependencies(tasks);
}
在实际使用中,可能会遇到以下问题:
时间显示不正确:
拖拽后位置跳变:
性能下降:
移动端支持: