基於 ECharts 封裝甘特圖並實現自動滾屏


項目中需要用到甘特圖組件,之前的圖表一直基於 EChart 開發,但 EChart 本身沒有甘特圖組件,需要自行封裝

經過一番鏖戰,終於完成了...

我在工程中參考 v-chart 封裝了一套圖表組件,所以這里只介紹甘特圖組件的實現,圖表的初始化、數據更新、自適應等不在這里介紹

 

一、約定數據格式

EChart 本身沒有甘特圖,但可以通過 EChart 提供的“自定義”方法 type: 'custom' 開發

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

這里的 data 就是數據集,它是一個二維數組,主要需要兩個參數:

name: 名稱,可以在 legend 和 tooltip 中展示

value:參數集合,自定義的圖表時需要的參數都可以放到這個數組里

如果需要其它的配置,也可以按照 ECharts 的 series 結構添加別的字段

我自定義的數據結構是這樣的:

{ 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 的索引去取對應的參數

 

二、處理數據 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 提供的坐標系信息,截取出元素的圖形信息

 

三、自定義 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> `; }, },

 

四、自動滾屏

如果甘特圖的數據過多,堆在一屏展示就會顯得很窄,這時候可以結合 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); },

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM