在数据大屏(数据看板)场景中,常遇到以折线图方式展现数据的同比情况的需求,即在同一张折线图中显示当前周期和上一周期的数据对比情况,如下图所示:

ECharts同比折线图
ECharts同比折线图

具体的需求包含以下几个方面:

  1. 在同一张图表中展示2条折线;
  2. Tooltip分别提示不同周期的同一时间点的数据详情;
  3. 弱化显示上一周期的数据,突出显示当前周期的数据;
  4. 支持不同时间维度(时间粒度)之间的切换,比如日、周、月、年等之间的切换,横坐标(x轴)和Tooltip随之动态切换显示不同的时间标签(label)。

下面针对上述需求分别作一介绍。

在同一张图表中展示2条折线

这个需求比较简单,可以有多种实现方式,直接对照ECharts的文档即可,本文采用datasetseries方式。

Tooltip实现

单一的时间维度比较简单,同样参照ECharts文档即可实现,代码如下:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
tooltip: { trigger: 'axis', formatter: (params: DefaultLabelFormatterCallbackParams[]) => { const param1 = params[0]; const param2 = params[1]; const itemData1 = <StatisticsEntity>param1.data; const itemData2 = <StatisticsEntity>param2?.data; const seriesName = `<div>${param1.seriesName}</div>`; const unit = ''; const value1 = itemData1.count; const value2 = itemData2?.count; const title1 = getChartDateTitle(itemData1.rawDate); const nameStyle = 'style="margin-left: 4px;"'; const valueStyle = 'style="margin-left: 20px; font-weight:bold;"'; const rowStyle = 'style="display: flex; justify-content: space-between;"'; const itemName1 = `${param1.marker}<span ${nameStyle}>${title1}</span>`; const itemValue1 = `<span ${valueStyle}>${value1}${unit}</span>`; let tooltip2 = ''; if (param2) { const title2 = getChartDateTitle(itemData2.rawDate); const itemName2 = `${param2.marker}<span ${nameStyle}>${title2}</span>`; const itemValue2 = `<span ${valueStyle}>${value2}${unit}</span>`; tooltip2 = `<div ${rowStyle}><div>${itemName2}</div>${itemValue2}</div>`; } return `${seriesName}<div ${rowStyle}><div>${itemName1}</div>${itemValue1}</div>${tooltip2}`; } }
tooltip: { trigger: 'axis', formatter: (params: DefaultLabelFormatterCallbackParams[]) => { const param1 = params[0]; const param2 = params[1]; const itemData1 = param1.data; const itemData2 = param2?.data; const seriesName = `
${param1.seriesName}
`; const unit = ''; const value1 = itemData1.count; const value2 = itemData2?.count; const title1 = getChartDateTitle(itemData1.rawDate); const nameStyle = 'style="margin-left: 4px;"'; const valueStyle = 'style="margin-left: 20px; font-weight:bold;"'; const rowStyle = 'style="display: flex; justify-content: space-between;"'; const itemName1 = `${param1.marker}${title1}`; const itemValue1 = `${value1}${unit}`; let tooltip2 = ''; if (param2) { const title2 = getChartDateTitle(itemData2.rawDate); const itemName2 = `${param2.marker}${title2}`; const itemValue2 = `${value2}${unit}`; tooltip2 = `
${itemName2}
${itemValue2}
`; } return `${seriesName}
${itemName1}
${itemValue1}
${tooltip2}`; } }

以上代码的难点在于对ECharts API的熟悉程度。而更为复杂的需求4的动态切换功能,需要对getChartDateTitle方法进行改造,增加时间维度(DateDimension)参数,并根据不同的时间维度返回不同的标签值,本文不再赘述。

突出显示当前周期

ECharts默认根据series定义顺序显示折线图的层级,后定义的在最上面。而文档中有一个配置项可以改变显示层级,即seriesz值:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
{ type: 'line', name: '浏览量(PV)', encode: { x: 'date', y: 'count' }, datasetId: 'dataset_current', z: 2, itemStyle: { color: '#1890ff' } }
{ type: 'line', name: '浏览量(PV)', encode: { x: 'date', y: 'count' }, datasetId: 'dataset_current', z: 2, itemStyle: { color: '#1890ff' } }

上述代码中,另一个配置项itemStyle定义折线图的颜色,因此,若需要弱化显示上一周期的数据,将其设为一个较浅的颜色即可。需要注意的是,在CSS中比较常用的透明度方法也同样适用于ECharts,因此,可以直接将color值定义为rgba形式的值。

不同时间维度(粒度)之间的切换

原理也不难:获取当前的时间维度(粒度),根据时间维度(粒度)重新计算起止时间后重新发起请求获取对应时间周期的数据,再调用一下setOption方法即可。需要注意的是,此时需要同时更新X轴和Tooltip的显示值,并且,后端接口返回的数据中往往缺失没有数据的点,因此还需要手动填补缺失的数据。具体方法本文不再赘述。

总结

至此,折线图的同比展示需求全部实现,以下是完整代码:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
const option = { grid: { left: 50, right: 8, top: 12, bottom: 20, width: 600, height: 240 }, xAxis: { type: 'category' }, yAxis: { type: 'value' }, dataset: [ { id: 'dataset_current', source: pvData }, { id: 'dataset_last', source: lastPvData } ], series: [ { type: 'line', name: '浏览量(PV)', encode: { x: 'date', y: 'count' }, datasetId: 'dataset_current', z: 2, itemStyle: { color: '#1890ff' } }, { type: 'line', name: '浏览量(PV)', encode: { x: 'date', y: 'count' }, datasetId: 'dataset_last', z: 1, itemStyle: { color: '#93c6ff' } } ], tooltip: { trigger: 'axis', formatter: (params: DefaultLabelFormatterCallbackParams[]) => { const param1 = params[0]; const param2 = params[1]; const itemData1 = <StatisticsEntity>param1.data; const itemData2 = <StatisticsEntity>param2?.data; const seriesName = `<div>${param1.seriesName}</div>`; const unit = ''; const value1 = itemData1.count; const value2 = itemData2?.count; const title1 = getChartDateTitle(itemData1.rawDate); const nameStyle = 'style="margin-left: 4px;"'; const valueStyle = 'style="margin-left: 20px; font-weight:bold;"'; const rowStyle = 'style="display: flex; justify-content: space-between;"'; const itemName1 = `${param1.marker}<span ${nameStyle}>${title1}</span>`; const itemValue1 = `<span ${valueStyle}>${value1}${unit}</span>`; let tooltip2 = ''; if (param2) { const title2 = getChartDateTitle(itemData2.rawDate); const itemName2 = `${param2.marker}<span ${nameStyle}>${title2}</span>`; const itemValue2 = `<span ${valueStyle}>${value2}${unit}</span>`; tooltip2 = `<div ${rowStyle}><div>${itemName2}</div>${itemValue2}</div>`; } return `${seriesName}<div ${rowStyle}><div>${itemName1}</div>${itemValue1}</div>${tooltip2}`; } } };
const option = { grid: { left: 50, right: 8, top: 12, bottom: 20, width: 600, height: 240 }, xAxis: { type: 'category' }, yAxis: { type: 'value' }, dataset: [ { id: 'dataset_current', source: pvData }, { id: 'dataset_last', source: lastPvData } ], series: [ { type: 'line', name: '浏览量(PV)', encode: { x: 'date', y: 'count' }, datasetId: 'dataset_current', z: 2, itemStyle: { color: '#1890ff' } }, { type: 'line', name: '浏览量(PV)', encode: { x: 'date', y: 'count' }, datasetId: 'dataset_last', z: 1, itemStyle: { color: '#93c6ff' } } ], tooltip: { trigger: 'axis', formatter: (params: DefaultLabelFormatterCallbackParams[]) => { const param1 = params[0]; const param2 = params[1]; const itemData1 = param1.data; const itemData2 = param2?.data; const seriesName = `
${param1.seriesName}
`; const unit = ''; const value1 = itemData1.count; const value2 = itemData2?.count; const title1 = getChartDateTitle(itemData1.rawDate); const nameStyle = 'style="margin-left: 4px;"'; const valueStyle = 'style="margin-left: 20px; font-weight:bold;"'; const rowStyle = 'style="display: flex; justify-content: space-between;"'; const itemName1 = `${param1.marker}${title1}`; const itemValue1 = `${value1}${unit}`; let tooltip2 = ''; if (param2) { const title2 = getChartDateTitle(itemData2.rawDate); const itemName2 = `${param2.marker}${title2}`; const itemValue2 = `${value2}${unit}`; tooltip2 = `
${itemName2}
${itemValue2}
`; } return `${seriesName}
${itemName1}
${itemValue1}
${tooltip2}`; } } };

其中,dataset的定义如下:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
export interface StatisticsEntity { rawDate: Date; date: string; count: number; }
export interface StatisticsEntity { rawDate: Date; date: string; count: number; }

完美。[]~( ̄▽ ̄)~*