使用 Echarts 实现项目进度甘特图

版权声明:本文为博主原创文章,转载请注明出处,如有问题,欢迎指正,谢谢。 https://blog.csdn.net/qq_33404903/article/details/84341339

 Echarts 功能十分强大,可以实现多种图表效果,下面简单介绍下最近使用 Eharts 实现的一个项目进度甘特图。

下面是实现的效果:

目录

 

一.概览 Echarts 基本内容

1.官网文档

2.查看示例

二.需求分析

1.数据系列(series)

2.标线 (markLine)

3.数据对象结构

4.“超时”、“按时”划分

三.引入并配置 Echarts

1.坑

2.具体操作

四.说明


一.概览 Echarts 基本内容

1.官网文档

首先基本的前端搭建必须的,然后应该大致浏览一下 Echarts 官网 文档 专题下的 教程 和 配置项手册,如果不是要实现特别复杂的效果和功能,这些内容基本就足够了。

2.查看示例

只看文字内容比较抽象,因为要做甘特图,可以先去 实例 中找一个甘特图研究下,看看别人是怎么实现的。你会发现Echarts官网好像只能在 实例 下找到官方示例图,别急,操作一番就可以看其他无私的贡献者发布的图表了。

点击官网页面右上角 “EN”,切换为英文版,

在 Get Started 下 点击 Gallery,

这样就能看到其他开发者贡献的 Demo 了,

点击 展开筛选,筛选你要找的图表类型。

下面这个图表(点击这里查看该图表Demo)和我们要实现的效果还是有几分类似的:

当然细节还得自己研究修改一下。

二.需求分析

从文章开头的图表效果来看,横坐标刻度值为时间,纵坐标为 “方案”、“纲要”、“成果” 三个项目阶段类目,每个阶段类目中包含按时完成(蓝色柱状图)和超时完成(红色柱状图)两种系列的数据,另外还有垂直于 x 时间坐标轴的 “计划开始时间”和“有效期”两条时间标线。结合查阅官网文档和demo,应该有以下初步结论:

1.数据系列(series)

每个阶段类目中实际应该有三种系列(series)的数据的,即应该有 开始时间、按时完成时间和超时完成时间三种系列,且三种系列的柱状图是堆叠在一起的。

2.标线 (markLine)

“计划开始时间” markLine 可以放在在 开始时间series 中,“有效期”markLine可以在 按时完成 或 超时完成 series 中。

3.数据对象结构

这里比较容易理解的传值结构,应该是传入 三个时间段 和 两个刻度值,类似下面这种结构:

/**
 * 存储阶段进度的对象
 */
var stageProgress = {
	// 方案 实际时间段
	fangAnTimeBucket: ['2017-01-01', '2017-03-01'],
	// 纲要 实际时间段
	gangYaoTimeBucket: ['2017-02-26', '2017-08-29'],
	// 成果 实际时间段
	chengGuoTimeBucket: ['2017-08-29', '2017-12-06'],
	// 计划开始时间
	scheduledStartTime: '2016-12-22',
	// 有效截止日期
	validTime: '2017-05-10'
};

4.“超时”、“按时”划分

这里要求三个阶段实际时间段的首尾可以不连续,即三个阶段之间没有影响,所以,是否超时就取决于 有效期 与 阶段实际时间段 之间的关系了。比如,有效期在阶段实际时间段开始和结束时间之间,那么该阶段是超时的,但是该阶段的柱状图应该是蓝色红色共存的;有效期在阶段实际时间段结束时间之后,那么该阶段按时完成,应该都是蓝色的;有效期在阶段实际时间段开始时间之前,那么该阶段超时完成,应该都是红色的。

其实主要是第四点需要计算一下,其他 参考一下 配置项手册基本没有太大问题。

三.引入并配置 Echarts

1.坑

用新不用旧,但是经过我的测试,Echarets4.x 透明堆叠不起效果,即设置了 series 为 bar ,且 开始时间、按时完成时间、按时完成时间 三个 series 设置了 相同的 stack 后,开始时间 柱状图并不能起到遮盖其他系列柱状图的效果,于是尝试使用3.x 版本,是可以起到柱条堆叠辅助隐藏的效果的。

若果需要兼容 IE8 的话,那么你需要定制下载  Echarts(我在实际测试中发现,这个定制下载即使选中 兼容IE8 ,下载下来的js仍让无法兼容 IE8,真的是让人脑壳痛)。

2.具体操作

在你的页面中,放置一个 div,作为放置图表的容器,需要为它指定宽高。

<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="content"></div>

控制图表的 js 代码基本结构如下:

// 基于准备好的dom,初始化echarts实例
        var myChart = echarts.init(document.getElementById('main'));

        // 指定图表的配置项和数据
        var option = {
            title: {
                text: 'ECharts 入门示例'
            },
            tooltip: {},
            legend: {
                data:['销量']
            },
            xAxis: {
                data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
            },
            yAxis: {},
            series: [{
                name: '销量',
                type: 'bar',
                data: [5, 20, 36, 10, 10, 20]
            }]
        };

        // 使用刚指定的配置项和数据显示图表。
        myChart.setOption(option);

下面是实现本文开头图表效果的 js,方法说明和需要注意的点基本都写在注释里了:

/**
 * 存储阶段进度时间段的对象
 */
var stageProgress = {
// 			// 方案 实际时间段
// 			fangAnTimeBucket: ['2017-01-01', '2017-03-01'],
// 			// 纲要 实际时间段
// 			gangYaoTimeBucket: ['2017-02-26', '2017-08-29'],
// 			// 成果 实际时间段
// 			chengGuoTimeBucket: ['2017-08-29', '2017-12-06'],
// 			// 计划开始时间
// 			scheduledStartTime: '2016-12-22',
// 			// 有效截止日期
// 			validTime: '2017-05-10'

	// 		// 方案 实际时间段
	// 		fangAnTimeBucket: ['2017-01-01', '2017-01-20'],
	// 		// 纲要 实际时间段
	// 		gangYaoTimeBucket: ['2017-02-07', '2017-02-28'],
	// 		// 成果 实际时间段
	// 		chengGuoTimeBucket: ['2017-02-15', '2017-03-20'],
	// 		// 计划开始时间
	// 		scheduledStartTime: '2017-01-01',
	// 		// 有效截止日期
	// 		validTime: '2017-02-20'

	// 方案 实际时间段
	fangAnTimeBucket: ['2017-11-23', '2017-11-28'],
	// 纲要 实际时间段
	gangYaoTimeBucket: ['2017-11-25', '2017-11-30'],
	// 成果 实际时间段
	chengGuoTimeBucket: ['2017-12-06', '2017-12-12'],
	// 计划开始时间
	scheduledStartTime: '2017-11-23',
	// 有效截止日期
	validTime: '2017-12-02'
};

/**
 * 横坐标轴时间刻度可选值
 * 这里 month和year 没有考虑平闰年之分
 */
var timeInterval = {
	day: 3600 * 1000 * 24,
	month: 3600 * 1000 * 24 * 31,
	year: 3600 * 1000 * 24 * 31 * 12,
};

/**
 * 时间坐标轴标签单位应该精确到哪一位
 */
var xAxisLabelUnit = {
	year: false,
	month: false,
	day: false
}

/**
 * 获取合适的横坐标时间刻度间隔
 */
function getProperTimeAxisInterval() {
	xAxisLabelUnit.year = false;
	xAxisLabelUnit.month = false;
	xAxisLabelUnit.day = false;

	var timeDataArray = getXAxisData();
	var begin = getTimeMilliseconds(timeDataArray[timeDataArray.length - 1]);
	console.log("begin " + begin);
	var periodMillis = getTimeMilliseconds(timeDataArray[timeDataArray.length - 1]) - getTimeMilliseconds(timeDataArray[0]);
	console.log("periodMillis " + periodMillis);
	var years = periodMillis / timeInterval.year;
	console.log("years " + years);
	var months = periodMillis / timeInterval.month;
	console.log("months " + months);
	var days = periodMillis / timeInterval.day;
	console.log("days " + days);

	if (months <= 1) {
		xAxisLabelUnit.day = true;
		return timeInterval.day * 2;
	} else if (months <= 16) {
		xAxisLabelUnit.month = true;
		return timeInterval.month;
	} else if (months <= 24) {
		xAxisLabelUnit.month = true;
		return timeInterval.month * 2;
	} else if (years <= 16) {
		xAxisLabelUnit.year = true;
		return timeInterval.year;
	}
}

/**
 * 获取横轴坐标数据源,这里横坐标只显示年月
 * 最小值取传入数据最小的时间再减小一个月
 * 最大值取传入数据最小的时间再增加一个月
 */
function getXAxisData() {
	var arr = new Array();
	arr = arr.concat(stageProgress.scheduledStartTime)
		.concat(stageProgress.fangAnTimeBucket)
		.concat(stageProgress.gangYaoTimeBucket)
		.concat(stageProgress.chengGuoTimeBucket)
		.concat(stageProgress.validTime).filter(function(item) {
			return item != "-";
		}).sort();
	console.log(arr);
	return arr;
}

/**
 * 更改日期字符串为相应月份的第一天
 * @param {Object} dateStr 日期字符串
 */
function changeDateToMonthFirstDay(dateStr) {
	var inputDate = new Date(dateStr);
	inputDate.setDate(1);
	var result = inputDate.getFullYear() + "-" +
		(inputDate.getMonth() >= 9 ? inputDate.getMonth() + 1 : "0" +
			(inputDate.getMonth() + 1)) + "-" + ("0" + 1);
	return result;
}

/**
 * 获取格式化的日期 YYYY-MM-dd
 */
function formatDateToStr(date) {
	var inputMonth = date.getMonth();
	var inputDate = date.getDate();
	var result = date.getFullYear() +
		"-" + (inputMonth >= 9 ? inputMonth + 1 : "0" + (inputMonth + 1)) +
		"-" + (inputDate >= 9 ? inputDate : "0" + (inputDate));
	return result;
}

var faOnTimeCompletionTime = getOnTimeCompletionTime('方案', stageProgress.fangAnTimeBucket[0], stageProgress.fangAnTimeBucket[
	1]);
var gyOnTimeCompletionTime = getOnTimeCompletionTime('纲要', stageProgress.gangYaoTimeBucket[0], stageProgress.gangYaoTimeBucket[
	1]);
var cgOnTimeCompletionTime = getOnTimeCompletionTime('成果', stageProgress.chengGuoTimeBucket[0], stageProgress.chengGuoTimeBucket[
	1]);

var faOverTimeCompletionTime = getOverTimeCompletionTime('方案', stageProgress.fangAnTimeBucket[0], stageProgress.fangAnTimeBucket[
	1]);
var gyOverTimeCompletionTime = getOverTimeCompletionTime('纲要', stageProgress.gangYaoTimeBucket[0], stageProgress.gangYaoTimeBucket[
	1]);
var cgOverTimeCompletionTime = getOverTimeCompletionTime('成果', stageProgress.chengGuoTimeBucket[0], stageProgress.chengGuoTimeBucket[
	1]);

/**
 * 时间数组
 */
var timeArray = {
	// 开始时间
	beginTimeArr: [
		getTimeMilliseconds(stageProgress.fangAnTimeBucket[0]),
		getTimeMilliseconds(stageProgress.gangYaoTimeBucket[0]),
		getTimeMilliseconds(stageProgress.chengGuoTimeBucket[0]),
	],
	// 按时完成时间
	onTimeCompletionTimeArr: [
		getTimeMilliseconds(faOnTimeCompletionTime),
		getTimeMilliseconds(gyOnTimeCompletionTime),
		getTimeMilliseconds(cgOnTimeCompletionTime),
	],
	// 超时完成时间
	overTimeCompletionTimeArr: [
		getTimeMilliseconds(faOverTimeCompletionTime),
		getTimeMilliseconds(gyOverTimeCompletionTime),
		getTimeMilliseconds(cgOverTimeCompletionTime),
	],
};

// 初始化图表
var myChart = echarts.init(document.getElementById('content'));

// 构建图表配置项
option = {
	tooltip: {
		trigger: 'axis',
		axisPointer: {
			type: 'shadow',
		},

		/**
		 * 也可以使用 formatter: '{b0}:<br />{a0}: {c0}<br />{a1}: {c1}<br />{a2}: {c2}',
		 * 但是这样当鼠标指向纵坐标的三个阶段中的某一个时,即使该阶段 没有按时完成,或者 没有超时,
		 * 也会显示 按时 或 超时 的 tooltip
		 */
		formatter: function(params) {
// 			console.log("params[0]" + JSON.stringify(params[0]));
// 			console.log("params[1]" + JSON.stringify(params[1]));
			console.log("params[2]" + JSON.stringify(params[2]));
			var info = params[0].axisValue + ":<br />";
			info += params[0].seriesName + ":" + getSeriesDateStr(params[0].data) + "<br />";
			info += "结束时间:" + (params[2].data != "-" && params[2].data != undefined && params[2].data != null&&!isNaN(params[2].data) ?
				getSeriesDateStr(params[2].data) :
				(!isNaN(params[1].data)&&params[1].data != undefined && params[1].data != null ? getSeriesDateStr(params[1].data) : "-"));
			return info;
		},
	},

	/**
	 * 右上角工具栏
	 */
	toolbox: {
		right: '3%',
		show: false,
		feature: {
			/**
			 * 数据视图
			 */
			// 			dataView:{
			// 				show:true,
			// 			},
			saveAsImage: {
				show: true
			}
		}
	},

	/**
	 * 图例
	 */
	legend: {
		data: ['按时完成', '超时完成'],
		tooltip: {
			show: true,
		},
		itemWidth: 15,
		itemHeight: 15,
		textStyle: {
			fontSize: 16,
			fontFamily: 'Microsoft YaHei',
		},
		itemGap: 120,
		padding: 10,
	},

	/**
	 * 直角坐标系内绘图网格
	 */
	grid: {
		left: '3%',
		right: '3%',
		bottom: '2%',
		/**
		 * grid 区域是否包含坐标轴的刻度标签。
		 */
		containLabel: true,
	},

	/**
	 * 横坐标
	 */
	xAxis: {
		// 		/**
		// 		 * 坐标轴指示器
		// 		 */
		// 		axisPointer: {
		// 			show: true,
		// 		},

		/**
		 * 时间轴,适用于连续的时序数据,与数值轴相比时间轴带有时间的格式化,
		 * 在刻度计算上也有所不同,例如会根据跨度的范围来决定使用月,星期,日还是小时范围的刻度。
		 */
		type: 'time',

		/**
		 * value 是一个包含 min 和 max 的对象,分别表示数据的最大最小值,这个函数应该返回坐标轴的最大值。
		 *
		 * 坐标轴刻度最小值。
		 */
		min: function(value) {
			return value.min + (getTimeMilliseconds(getProperTimeAxisBeginAndEndTime()[0]) - value.min);
		},

		/**
		 * value 是一个包含 min 和 max 的对象,分别表示数据的最大最小值,这个函数应该返回坐标轴的最大值。
		 *
		 * 坐标轴刻度最大值。
		 */
		max: function(value) {
			return value.max + (getTimeMilliseconds(getProperTimeAxisBeginAndEndTime()[1]) - value.max);
		},
		// 
		/**
		 * 设置坐标轴分割间隔
		 */
		interval: getProperTimeAxisInterval(),
		axisLine: {
			lineStyle: {
				color: '#BDC8CD',
				width: 1,
			},
		},

		/**
		 * 坐标轴刻度标签的相关设置。
		 */
		axisLabel: {
			showMinLabel: false,
			showMaxLabel: false,
			rotate: 35,
			margin: 12,
			fontSize: 16,
			color: '#1c2431',
			formatter: function(value, index) {
				var date = new Date(value);
				// var time = date.getFullYear() + "." + (date.getMonth() + 1) + "." + date.getDate();
				var time = date.getFullYear();
				if (xAxisLabelUnit.month) {
					time += "." + (date.getMonth() + 1);
				}
				if (xAxisLabelUnit.day) {
					time += "." + (date.getMonth() + 1) + '.' + date.getDate();
				}
				return time;
			},
		},

		/**
		 * 坐标轴刻度分割线
		 */
		splitLine: {
			show: false,
		},
	},

	/**
	 * 纵坐标
	 */
	yAxis: {
		type: 'category',
		data: ['方\n案', '纲\n要', '成\n果'],
		axisTick: {
			show: false,
		},
		axisLine: {
			lineStyle: {
				color: '#e9e9ea',
				width: 1,
			},
		},
		axisLabel: {
			fontWeight: 'bold',
			fontSize: 16,
			color: '#1c2431',
			fontFamily: 'Microsoft YaHei',
		},
		splitLine: {
			show: true,
			lineStyle: {
				color: '#eaeae9',
				width: 1,
			},
		},
	},

	/**
	 * 系列
	 */
	series: [{
		name: '开始时间',
		type: 'bar',
		stack: '时间',
		itemStyle: {
			normal: {
				barBorderColor: 'rgba(0,0,0,0)',
				color: 'rgba(0,0,0,0)'
			},
			emphasis: {
				barBorderColor: 'rgba(0,0,0,0)',
				color: 'rgba(0,0,0,0)'
			}
		},
		label: {
			normal: {
				formatter: function(params) {
					return getSeriesDateStr(params.value);
				},
				show: true,
				position: 'insideRight',
				fontSize: 16,
				color: '#1c2431',
				fontFamily: 'Microsoft YaHei',
				offset: [40, -20],
			}
		},
		data: timeArray.beginTimeArr,

		/**
		 * 标注线
		 */
		markLine: {
			lineStyle: {
				normal: {
					color: '#0f77e9',
				},
			},
			label: {
				normal: {
					fontWeight: 'bold',
					padding: 2,
					fontSize: 14,
					fontFamily: 'Microsoft YaHei',
					formatter: function(params) {
						return '计划开始时间 ' + getSeriesDateStr(params.value);
					},
				},
			},
			data: [{
				name: '计划开始时间',
				xAxis: getTimeMilliseconds(stageProgress.scheduledStartTime),
			}, ]
		},
	}, {
		name: '按时完成',
		type: 'bar',
		stack: '时间',
		itemStyle: {
			normal: {
				color: '#0f77e9'
			}
		},
		label: {
			normal: {
				formatter: function(params) {
					return getSeriesDateStr(params.value);
				},
				show: true,
				fontSize: 16,
				color: '#1c2431',
				position: 'right',
				fontFamily: 'Microsoft YaHei',
				offset: [-45, -20],
			}
		},
		data: timeArray.onTimeCompletionTimeArr,
		markLine: {
			lineStyle: {
				normal: {
					color: '#ff4747',
				},

			},
			label: {
				normal: {
					fontWeight: 'bold',
					padding: 2,
					fontSize: 14,
					fontFamily: 'Microsoft YaHei',
					formatter: function(params) {
						return '计划完成时间 ' + getSeriesDateStr(params.value);
					},
				},
			},
			data: [{
				name: '有效期',
				xAxis: getTimeMilliseconds(stageProgress.validTime),
			}, ]
		},
	}, {
		name: '超时完成',
		type: 'bar',
		stack: '时间',
		itemStyle: {
			normal: {
				color: '#ff4747'
			}
		},
		label: {
			normal: {
				formatter: function(params) {
					return getSeriesDateStr(params.value);
				},
				show: true,
				fontSize: 16,
				color: '#1c2431',
				offset: [-45, -20],
				position: 'right',
				fontFamily: 'Microsoft YaHei',
			}
		},
		data: timeArray.overTimeCompletionTimeArr,
		/**
		 * 柱状图宽度
		 */
		barWidth: 20,
	}, ]
};

// 将构建好的配置项传入echarts
myChart.setOption(option);

/**
 * 时间对象转日期字符串 yyyy.MM.dd
 * @param {Object} timeObject 毫秒值或时间字符串
 */
function getSeriesDateStr(timeObject) {
	if (timeObject == "-") {
		return timeObject;
	}
	var date = new Date(timeObject);
	var dateStr = '';
	dateStr += date.getFullYear() + '.';
	dateStr += date.getMonth() + 1 + '.';
	dateStr += date.getDate();
	return dateStr;
};

/**
 * 获取阶段的计划内完成时间(蓝色柱状图值)
 * @param {Object} stage 阶段
 * @param {Object} stateBeginTime
 * @param {Object} stateCompletionTime
 */
function getOnTimeCompletionTime(stage, stageBeginTimeStr, stageCompletionTimeStr) {
	var validTimeMillis = getTimeMilliseconds(stageProgress.validTime);
	var stageBeginTimeMillis = getTimeMilliseconds(stageBeginTimeStr);
	var stageCompletionTimeMillis = getTimeMilliseconds(stageCompletionTimeStr);
	if (validTimeMillis <= stageBeginTimeMillis) {
		// 若阶段开始时间大于等于有效期,则项目超时完成,有效时间为'-'
		return '-';
	}
	if (validTimeMillis > stageBeginTimeMillis && validTimeMillis < stageCompletionTimeMillis) {
		// 若有效期介于阶段完成时间和阶段开始时间之间,则该阶段按时完后时间(实际该阶段是超时完成的)即蓝色柱状图的终值为有效期
		return stageProgress.validTime;
	}
	if (validTimeMillis >= stageCompletionTimeMillis) {
		// 若有效期大于等于阶段完成时间,则阶段按时完成
		return stageCompletionTimeStr;
	}
}

/**
 * 获取阶段内的超时完成时间(红色色柱状图值)
 * @param {Object} stage 阶段
 * @param {Object} stateBeginTime
 * @param {Object} stateCompletionTime
 */
function getOverTimeCompletionTime(stage, stageBeginTimeStr, stageCompletionTimeStr) {
	var validTimeMillis = getTimeMilliseconds(stageProgress.validTime);
	var stageBeginTimeMillis = getTimeMilliseconds(stageBeginTimeStr);
	var stageCompletionTimeMillis = getTimeMilliseconds(stageCompletionTimeStr);

	if (validTimeMillis < stageCompletionTimeMillis) {
		// 阶段完成时间大于有效期,则将阶段完成时间作为超时时间返回
		return stageCompletionTimeStr;
	}

	if (validTimeMillis >= stageCompletionTimeMillis) {
		// 阶段完成时间小于等于有效期,则阶段按时完成,超时时间应为'-'
		return '-';
	}
}

/**
 * 根据时间字符串获取对应的毫秒值
 * @param {Object} timeStr 时间字符串
 */
function getTimeMilliseconds(timeStr) {
	return (new Date(timeStr)).getTime();
}

/**
 *获取时间坐标轴的起始和结束值
 */
function getProperTimeAxisBeginAndEndTime() {
	var xAxis = getXAxisData();
	var begin = xAxis[0];
	var end = xAxis[xAxis.length - 1];
	var beginDate = new Date(begin);
	var endDate = new Date(end);

	if (xAxisLabelUnit.month) {
		beginDate.setDate(1);
		endDate.setMonth(endDate.getMonth() + 1);
		endDate.setDate(1);
	} else {
		var daysCount = getProperTimeAxisInterval() / timeInterval.day;
		console.log("daysCount " + daysCount);
		beginDate.setDate(beginDate.getDate() - daysCount);
		endDate.setDate(endDate.getDate() + daysCount);
	}
	var beArr = [formatDateToStr(beginDate), formatDateToStr(endDate)];
	console.log("beArr " + beArr);
	return beArr;
}

四.说明

横轴的 axisLabel 刻度标注,并不是准确的 x月1号,有可能是 x月12号等其他值。我尝试设置了坐标轴最小(min)和最大(max)刻度的值,并设置精确值(interval),但是都没有达成想要的效果,如果各位有什么好的解决办法请赐教。另外本人接触前端的时间较短,代码上有什么纰漏也请大家指出。

猜你喜欢

转载自blog.csdn.net/qq_33404903/article/details/84341339