0%

G2源码解析之图例选取

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

这里回答三个问题

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

结论

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

  1. 点击图例图元后触发了legend:filter事件。
  2. legend模块相应了legend:filter事件,根据图例的筛选状态执行了filter方法。
  3. filter中根据上下文实例context获取了图表的marks。忽略了legends``marks对其他marks修改了其transformscale

如何隐藏/移除/显示/新增数据

由于filter操作本质上是在修改marks修改了其transformscale, 并没有涉及数据的移除和添加,所以隐藏/显示逻辑其实并无区别。

定位实现代码

核心逻辑

  1. 点击图例图元后触发了legend:filter事件。
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
const click = async (event) => {
const value = datum(item);
const index = selectedValues.indexOf(value);
if (index === -1) selectedValues.push(value);
else selectedValues.splice(index, 1);
await filter(selectedValues);
updateLegendState();

const { nativeEvent = true } = event;
if (!nativeEvent) return;
if (selectedValues.length === items.length) {
emitter.emit('legend:reset', { nativeEvent });
} else {
// Emit events.
emitter.emit('legend:filter', {
...event,
nativeEvent,
data: {
channel,
values: selectedValues,
},
});
}
};

  1. legend模块相应了legend:filter事件,根据图例的筛选状态执行了filter方法。
1
2
3
4
5
6
7
8
9
10
const onFilter = async (event) => {
const { nativeEvent } = event;
if (nativeEvent) return;
const { data } = event;
const { channel: specifiedChannel, values } = data;
if (specifiedChannel !== channel) return;
selectedValues = values;
await filter(selectedValues);
updateLegendState();
};
  1. filter中根据上下文实例context获取了图表的marks。忽略了legends``marks对其他marks修改了其transformscale
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
async function filterView(
context, // View instance,
{
legend, // Legend instance.
channel, // Filter Channel.
value, // Filtered Values.
ordinal, // Data type of the legend.
channels, // Channels for this legend.
allChannels, // Channels for all legends.
facet = false, // For facet.
},
) {
const { view, update, setState } = context;
setState(legend, (viewOptions) => {
const { marks } = viewOptions;
// Add filter transform for every marks,
// which will skip for mark without color channel.
const newMarks = marks.map((mark) => {
if (mark.type === 'legends') return mark;

// Inset after aggregate transform, such as group, and bin.
const { transform = [], data = [] } = mark;
const index = transform.findIndex(
({ type }) => type.startsWith('group') || type.startsWith('bin'),
);
const newTransform = [...transform];
if (data.length) {
newTransform.splice(index + 1, 0, {
type: 'filter',
[channel]: { value, ordinal },
});
}

// Set domain of scale to preserve encoding.
const newScale = Object.fromEntries(
channels.map((channel) => [
channel,
{ domain: view.scale[channel].getOptions().domain },
]),
);
return deepMix({}, mark, {
transform: newTransform,
scale: newScale,
...(!ordinal && { animate: false }),
legend: facet
? false
: Object.fromEntries(allChannels.map((d) => [d, { preserve: true }])),
});
});
return { ...viewOptions, marks: newMarks };
});
await update();
}