Antv G2 修改 Brush 默认行为为返回时间戳范围

G2 brush 时间轴,而不是筛选数据点

一、背景

目前 G2 使用 brush-x 筛选后的是对应的点,而不是 X 轴的时间范围。在实际使用过程中,我们需要场景如下

  1. 鼠标筛选一个区域
  2. 获取这个区域的开始时间和结束时间
  3. 以第2步获取到的时间范围作为结果来重新获取数据

二、核心代码一览

2.1 注册 Action

  • src/index.ts,中枚举出了可以使用的 Action
1
2
3
registerAction('brush', DataRangeFilter);
registerAction('brush-x', DataRangeFilter, { dims: ['x'] });
registerAction('brush-y', DataRangeFilter, { dims: ['y'] });

2.2 filter 处理逻辑

  • 看下 filter 流程src/interaction/action/data/range-filter.ts
    • 获取到用户当前选择的视觉点
    • 转换视觉点,获取到实际选择的 min value 和 max value,并且生成 filter
    • 根据 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
26
27
public filter() {
let startPoint;
let currentPoint;
// ... codes
// 进行一些列处理,得到 startPoint 和 currentPoint 的值

const view = this.context.view;

// 获取到归一化坐标
const coord = view.getCoordinate();
const normalCurrent = coord.invert(currentPoint);
const normalStart = coord.invert(startPoint);

// 设置 x 方向的 filter
if (this.hasDim('x')) {
const xScale = view.getXScale();

// filter 其实就是一个函数 (value: any, datum: Datum, idx?: number) => boolean;
const filter = getFilter(xScale, 'x', normalCurrent, normalStart);

// 核心!根据 filter 进行筛选
this.filterView(view, xScale.field, filter);
}
// 设置 y 方向的 filter
// ... codes
this.reRender(view);
}
  • 看下如何获取到 min value 和 max value src/interaction/action/data/range-filter.ts
    • 获取到的 minValue 和 maxValue 取整后就是时间戳了
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
function getFilter(scale: Scale, dim: string, point1: Point, point2: Point): FilterCondition {
let min = Math.min(point1[dim], point2[dim]);
let max = Math.max(point1[dim], point2[dim]);
const [rangeMin, rangeMax] = scale.range;
// 约束值在 scale 的 range 之间
if (min < rangeMin) {
min = rangeMin;
}
if (max > rangeMax) {
max = rangeMax;
}
// 范围大于整个 view 的范围,则返回 null
if (min === rangeMax && max === rangeMax) {
return null;
}
const minValue = scale.invert(min);
const maxValue = scale.invert(max);
/**
* 这里获取到的 minValue 和 maxValue,就是对应的 x 轴选择的时间范围
* 类似 minValue 1611724258188.5186 Wed Jan 27 2021 13:10:58 GMT+0800
* maxValue 1611724586405.7407 Wed Jan 27 2021 13:16:26 GMT+0800
*/

if (scale.isCategory) {
const minIndex = scale.values.indexOf(minValue);
const maxIndex = scale.values.indexOf(maxValue);
const arr = scale.values.slice(minIndex, maxIndex + 1);
return (value) => {
return arr.includes(value);
};
} else {
return (value) => {
return value >= minValue && value <= maxValue;
};
}
}

我们可以在用户定义的 action 上下文里拿到对应的 rangefilter 实例

1
ctx.actions.find(v => v.name === 'brush-x')

但同时我们也看到,我们需要的 minValue 和 maxValue 都是作为临时计算的产物,并没有挂在对象实例上,所以我们有以下三条路

  1. 获取到 ctx 后,自己重新计算
  2. 修改源码,把这个临时状态挂在对象上。不过需要重新发包,或者把代码纳入版本库?侵入性强,不便于后期升级,还是算了吧
  3. 再去看看其他方案吧

不需要在选择上浪费太多时间,干就完了。我们先选择方案一,要是后面有更好的,再更换嘛。

2.3 根据获取到的 ctx 来计算 Min 和 Max

image-20210127174119210

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
/**
* 1、获取到可视区域 view 的宽度
* 2、获取到可视区域的时间戳分布
* 3、算出 brush 的起始点
*
* @param ctx
*/
export function getBrushedTimeRange(ctx) {
// action 本身实例
const self = ctx.actions.find(v => v.name === 'brush-x')
const view = self.context.view

// 获取到两个选择的点
const startPoint = self.startPoint
const currentPoint = ctx.getCurrentPoint()

// 画布两侧的 padding
const paddingLeft = view.padding[3]
// 获取主体的实际宽度
const totalWith = view.width - view.padding[1] - view.padding[3]
// 获取选择点的开始结束坐标
const startX = startPoint.x - paddingLeft
const endX = currentPoint.x - paddingLeft

// range 范围的才会落点
const timestampsCount = (view.getXScale().max - view.getXScale().min) / (view.getXScale().range[1] - view.getXScale().range[0])
// 获取到每一个时间占用的宽度
const perTimestampWidth = timestampsCount / totalWith

// 开始需要补偿的时间范围
const startXTimestamp = view.getXScale().min - view.getXScale().range[0] * totalWith * perTimestampWidth
const startTime = startXTimestamp + startX * perTimestampWidth
const endTime = startXTimestamp + endX * perTimestampWidth

return startTime < endTime ? [startTime, endTime] : [endTime, startTime]
}

在brush回调的地方,执行下面的动作即可,着重关注 callback 地方

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
registerInteraction('brushX', {
showEnable: [
{ trigger: 'plot:mouseenter', action: 'cursor:crosshair' },
{ trigger: 'plot:mouseleave', action: 'cursor:default' }
],
start: [
{
trigger: 'mousedown',
action: ['brush-x:start', 'x-rect-mask:start', 'x-rect-mask:show']
}
],
processing: [
{
trigger: 'mousemove',
action: ['x-rect-mask:resize']
}
],
end: [
{
trigger: 'mouseup',
action: ['brush-x:end', 'x-rect-mask:end', 'x-rect-mask:hide'],
callback: ctx => {
// 重点再这里
const [startTime, endTime] = getBrushedTimeRange(ctx)

this.$emit('on-brushed', [startTime, endTime])
this.resetBrushAction = ctx.actions.find(v => v.name === 'brush-x')
}
}
],
rollback: [
{ trigger: 'dblclick', action: ['brush-x:reset', 'reset-button:hide'] },
{ trigger: 'reset-button:click', action: ['brush-x:reset', 'reset-button:hide'] }
]
})

三、踩坑方案

3.1 直接使用 brush-filter 导致的 scalex 上下文传递不一致问题

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

/**
* hack 原来的 getFilter() 方法,直接返回我们需要的数据
*
* @param scale Scale
* @param dim
* @param point1 Point
* @param point2
* @returns {null|{minValue, maxValue}}
*/
function hackGetFilterReturnMinMax(scale, dim, point1, point2) {
let min = Math.min(point1[dim], point2[dim])
let max = Math.max(point1[dim], point2[dim])
const [rangeMin, rangeMax] = scale.range
// 约束值在 scale 的 range 之间
if (min < rangeMin) {
min = rangeMin
}
if (max > rangeMax) {
max = rangeMax
}
// 范围大于整个 view 的范围,则返回 null
if (min === rangeMax && max === rangeMax) {
return null
}

const minValue = scale.invert(min)
const maxValue = scale.invert(max)

// console.log('start', minValue, 'end', maxValue)

return { minValue, maxValue }
}

3.2 区分 view 的几个视角

image-20210127174110482

四、一些链接

4.1 tooltip 联动

https://antv-g2.gitee.io/zh/examples/interaction/others#views-tooltip