基于 ECharts 封装甘特图并实现自动滚屏

项目中须要用到甘特图组件,以前的图表一直基于 EChart 开发,但 EChart 自己没有甘特图组件,须要自行封装html

通过一番鏖战,终于完成了...api

我在工程中参考 v-chart 封装了一套图表组件,因此这里只介绍甘特图组件的实现,图表的初始化、数据更新、自适应等不在这里介绍数组

 

1、约定数据格式数据结构

EChart 自己没有甘特图,但能够经过 EChart 提供的“自定义”方法 type: 'custom' 开发echarts

const option = { series: [{ type: 'custom', renderItem: (params, api) => { // do sth  }, data, }] }

这里的 data 就是数据集,它是一个二维数组,主要须要两个参数:ide

name: 名称,能够在 legend 和 tooltip 中展现函数

value:参数集合,自定义的图表时须要的参数均可以放到这个数组里工具

若是须要其它的配置,也能够按照 ECharts 的 series 结构添加别的字段this

我自定义的数据结构是这样的:spa

{ name, itemStyle: { normal: { color: color || defaultColor, }, }, // value 为约定写法,依序为“类目对应的索引”、“状态类型”、“状态名称”、“开始时间”、“结束时间”  value: [ index, type, name, new Date(start).getTime(), new Date(end || Date.now()).getTime(), ], }

注意:series.data 中的元素须要根据状态划分,不能根据类目(Y轴)划分,这样才能保证图例 legend 的正常显示

最终的 data 结构如图:

自定义的核心是 renderItem 函数,这个函数的本质就是:将 data 中的参数 value 处理以后,映射到对应的坐标轴上,具体处理参数的逻辑彻底自定义

甘特图就须要计算出各个数据块的高度和宽度,而后映射到对应的类目轴(Y轴)和时间轴(X轴)上

因为甘特图会用到时间轴(X轴),因此定义的 value 中须要开始时间和结束时间的时间戳

为了区分该数据属于类目轴(Y轴)的哪一条类目,还须要对应类目的索引 index

若是还有其它的须要,好比自定义 tooltip,还能够在 value 中添加其它的参数

但必定要约定好参数的顺序,由于 renderItem 函数是根据 value 的索引去取对应的参数

 

2、处理数据 Series

// 处理数据 function getGantSeries(args) { const { innerRows, columns } = args const baseItem = { type: 'custom', renderItem: (params, api) => renderGanttItem(params, api), dimensions: columns, }; return innerRows.map(row => { return { ...baseItem, name: row[0].name, data: row, }; }); }

当 type 指定为 'custom' 的时候,series 的元素能够添加 dimensions 字段,用来定义每一个维度的信息

处理数据的核心是 renderItem 方法,该方法提供了 paramsapi 两个参数,最后须要返回对应的图形元素信息

const DIM_CATEGORY_INDEX = 0; // value 中类目标识的索引 const DIM_CATEGORY_NAME_INDEX = 1; // value 中对应元素类型的索引 const DIM_START_TIME_INDEX = 3; // value 中开始时间的索引 const DIM_END_TIME_INDEX = 4; // value 中结束时间的索引 const HEIGHT_RATIO = 0.6; // 甘特图矩形元素高度缩放比例 const CATEGORY_NAME_PADDING_WIDTH = 20; // 在甘特图矩形元素上展现文字时,左右 padding 的最小长度 /** * 计算元素位置及宽高 * 若是元素超出了当前坐标系的包围盒,则剪裁这个元素 * 若是元素彻底被剪掉,会返回 undefined */ function clipRectByRect(params, rect) { return echarts.graphic.clipRectByRect(rect, { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height, }); } // 渲染甘特图元素 function renderGanttItem(params, api, extra) { const { isShowText, barMaxHeight, barHeight } = extra; // 使用 api.value(index) 取出当前 dataItem 的维度 const categoryIndex = api.value(DIM_CATEGORY_INDEX); // 使用 api.coord(...) 将数值在当前坐标系中转换成为屏幕上的点的像素值 const startPoint = api.coord([api.value(DIM_START_TIME_INDEX), categoryIndex]); const endPoint = api.coord([api.value(DIM_END_TIME_INDEX), categoryIndex]); // 使用 api.size(...) 取得坐标系上一段数值范围对应的长度 const baseHeight = Math.min(api.size([0, 1])[1], barMaxHeight); const height = barHeight * HEIGHT_RATIO || baseHeight * HEIGHT_RATIO; const width = endPoint[0] - startPoint[0]; const x = startPoint[0]; const y = startPoint[1] - height / 2; // 处理类目名,用于在图形上展现 const categoryName = api.value(DIM_CATEGORY_NAME_INDEX) + ''; const categoryNameWidth = echarts.format.getTextRect(categoryName).width; const text = width > categoryNameWidth + CATEGORY_NAME_PADDING_WIDTH ? categoryName : ''; const rectNormal = clipRectByRect(params, { x, y, width, height }); const rectText = clipRectByRect(params, { x, y, width, height }); return { type: 'group', children: [ { // 图形元素形状: 'rect', circle', 'sector', 'polygon' type: 'rect', ignore: !rectNormal, // 是否忽略(忽略即不渲染)  shape: rectNormal, // 映射 option 中 itemStyle 样式  style: api.style(), }, { // 在图形上展现类目名 type: 'rect', ignore: !isShowText || !rectText, shape: rectText, style: api.style({ fill: 'transparent', stroke: 'transparent', text: text, textFill: '#fff', }), }, ], }; }

上面是我用的 renderItem 方法全貌,主要是使用 api 提供的工具函数计算出元素的视觉宽高

再使用 echarts 提供的 graphic.clipRectByRect 方法,结合参数 params 提供的坐标系信息,截取出元素的图形信息

 

3、自定义 tooltip

若是数据格式正确,到这里已经能渲染出甘特图了,但一个图表还须要其它的细节,好比 tooltip 的自定义

在 renderItem 中有一个字段 encode 能够用来自定义 tooltip,但只能定义展现的文字

具体的 tooltip 排版和图例颜色(特别是渐变色)没法经过 encode 实现自定义,最终仍是得经过 formatter 函数

formatter: params => { const { value = [], marker, name, color } = params; const axis = this.columns; // 类目轴(Y轴)数据 // 删除空标题 let str = ''; isArray(axis[value[0]]) && axis[value[0]].map(item => { item && (str += `${item}/`); }); str = str.substr(0, str.length - 1); // 颜色为对象时,为渐变颜色,须要手动拼接 let mark = marker; if (isObject(color)) { const { colorStops = [] } = color; const endColor = colorStops[0] && colorStops[0].color; const startColor = colorStops[1] && colorStops[1].color; const colorStr = `background-image: linear-gradient(90deg, ${startColor}, ${endColor});`; mark = ` <span style="  display:inline-block; margin-right:5px; border-radius:10px; width:10px; height:10px; ${colorStr} "></span>`;  } // 计算时长 const startTime = moment(value[3]); const endTime = moment(value[4]); let unit = '小时'; let duration = endTime.diff(startTime, 'hours'); return ` <div>${str}</div> <div>${mark}${name}: ${duration}${unit}</div> <div>开始时间:${startTime.format('YYYY-MM-DD HH:mm')}</div> <div>结束时间:${endTime.format('YYYY-MM-DD HH:mm')}</div> `; }, },

 

4、自动滚屏

若是甘特图的数据过多,堆在一屏展现就会显得很窄,这时候能够结合 dataZoom 实现滚屏

首先须要在组件中引入 dataZoom

import 'echarts/lib/component/dataZoom'; // 配置项 const option = { ..., dataZoom: { type: 'slider', id: 'insideY01', yAxisIndex: 0, zoomLock: true, bottom: -10, startValue: this.dataZoomStartVal, endValue: this.dataZoomEndVal, handleSize: 0, borderColor: 'transparent', backgroundColor: 'transparent', fillerColor: 'transparent', showDetail: false, }, { type: 'inside', id: 'insideY02', yAxisIndex: 0, startValue: this.dataZoomStartVal, endValue: this.dataZoomEndVal, zoomOnMouseWheel: false, moveOnMouseMove: true, moveOnMouseWheel: true, } }

而后须要设定甘特图每一行的高度 barHeight,同时获取甘特图组件的高度

经过这两个高度计算出每屏能够展现的甘特图数据的数量 pageSize

const GANT_ITEM_HEIGHT = 56; const height = this.$refs.chartGantRef.$el.clientHeight; this.pageSize = Math.floor(height / GANT_ITEM_HEIGHT); // 设置 dataZoom 的起点
this.dataZoomStartVal = 0; this.dataZoomEndVal = this.pageSize - 1;

而后经过定时器派发事件,修改 dataZoom 的 startValue 和 endValue,实现自动滚屏的效果

const Timer = null; dataZoomAutoScoll() { Timer = setInterval(() => { const max = this.total - 1; if ( this.dataZoomEndVal > max || this.dataZoomStartVal > max - this.pageSize ) { this.dataZoomStartVal = 0; this.dataZoomEndVal = this.pageSize - 1; } else { this.dataZoomStartVal += 1; this.dataZoomEndVal += 1; } echarts.dispatchAction({ type: 'dataZoom', dataZoomIndex: 0, startValue: this.dataZoomStartVal, endValue: this.dataZoomEndVal }); }, 2000); },