0%

ECharts源码解析之图例选取

本文简述ECharts如何实现点击图例联动数据筛选。

这里回答三个问题

  1. 实现图例联动数据筛选的核心逻辑。
  2. 如何隐藏/移除数据。
  3. 如何显示/新增数据。

结论

实现图例联动数据筛选的核心逻辑

  1. 点击图例后触发了legendAction同时触发整个图表的update
  2. 在图表真正执行update之前执行了restoreData方法以恢复最原始的默认数据。
  3. 图表的update任务中执行了legendFilter
  4. legendFilter中根据自身属性对series做了filter

如何隐藏/移除数据

在步骤4中对要渲染的series做了过滤仅渲染需要渲染的series

如何显示/新增数据

在步骤2中恢复了整个图表最原始的默认数据。

定位实现代码

核心逻辑

  1. 点击图例后触发了legendActionlegendAction执行了makeSelectedMap方法。action触发后,触发了整个图表的update

https://github.com/apache/echarts/blob/master/src/component/legend/legendAction.ts#L74-L92

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
function makeSelectedMap(legendModel: LegendModel, out?: Record<string, boolean>) {
const selectedMap: Record<string, boolean> = out || {};
each(legendModel.getData(), function (model) {
const name = model.get('name');
// Wrap element
if (name === '\n' || name === '') {
return;
}
const isItemSelected = legendModel.isSelected(name);
if (hasOwn(selectedMap, name)) {
// Unselected if any legend is unselected
selectedMap[name] = selectedMap[name] && isItemSelected;
}
else {
selectedMap[name] = isItemSelected;
}
});

// 这里维护了数据模型(dataModel)上的selectedMap大致结构如下。
/*
* { A: true,
* B: false
* }
*/
return selectedMap;
}
  1. 在图表真正执行update之前执行了restoreData方法以恢复最原始的默认数据
1
2
3
4
5
6
7
8
9
reCreateSeriesIndices = function (ecModel: GlobalModel): void {
// 这里重新维护了要渲染的 series 列表(ecModel._seriesIndices)
const seriesIndices: number[] = ecModel._seriesIndices = [];
each(ecModel._componentsMap.get('series'), function (series) {
// series may have been removed by `replaceMerge`.
series && seriesIndices.push(series.componentIndex);
});
ecModel._seriesIndicesMap = createHashMap(seriesIndices);
};
  1. 图表的update任务中执行了legendFilter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function legendFilter(ecModel: GlobalModel) {

const legendModels = ecModel.findComponents({
mainType: 'legend'
}) as LegendModel[];
if (legendModels && legendModels.length) {
// 这里根据 legendModel 对 Series 做了 filter
ecModel.filterSeries(function (series: SeriesModel) {
// If in any legend component the status is not selected.
// Because in legend series is assumed selected when it is not in the legend data.
for (let i = 0; i < legendModels.length; i++) {
if (!legendModels[i].isSelected(series.name)) {
return false;
}
}
return true;
});
}

}
  1. legendFilter中根据自身属性对series做了filter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
filterSeries<T>(
cb: (this: T, series: SeriesModel, rawSeriesIndex: number) => boolean,
context?: T
): void {
assertSeriesInitialized(this);

const newSeriesIndices: number[] = [];
// 这里对当前的 _seriesIndices 做了过滤
each(this._seriesIndices, function (seriesRawIdx) {
const series = this._componentsMap.get('series')[seriesRawIdx] as SeriesModel;
// callback 返回 true 时 将原series移入新的 _seriesIndices。
// callback 返回 false 时 未移入就算过滤掉了。
cb.call(context, series, seriesRawIdx) && newSeriesIndices.push(seriesRawIdx);
}, this);

this._seriesIndices = newSeriesIndices;
this._seriesIndicesMap = createHashMap(newSeriesIndices);
}