蚂蚁金服 AntV G2 交互语法之框选高亮

3,478 阅读11分钟

G2 从 4.0 开始,将所有的交互行为使用全新的交互语法实现,并且不再默认内置,需要用户显式调用
chart.interaction() 接口。为了帮助大家更好地理解、使用交互语法,我们将会推出交互语法专题。

本文介绍的是最常见的交互:框选,包括框选的形状、框选过程中的图形变化以及框选后的各种操作。

框选中,高亮图形
b5.gif
框选后,拖拽 mask
b1.gif
框选后,过滤数据
b2.gif
框选后,过滤图形
b3.gif
框选的形状选择
b6-2.gif
多视图的框选联动
b4.gif

交互语法概览

G2 的交互语法,是将交互拆解成多个环节,每个环节由触发和反馈组成。只要你能将交互用自然语言的方式描述出来,就可以使用 G2 的交互语法进行组合搭建出交互行为。在这里我们再一起温习下 G2 交互语法中对于交互环节的定义,更详细的内容可以阅读可视化交互语法。

G2 将每一个交互环节拆解成以下步骤:

  • showEnable 示能:表示交互可以进行;
  • start 开始:交互开始;
  • processing 持续:交互持续;
  • end 结束:交互结束;
  • rollback 回滚:取消交互,恢复到原始状态;

下面我们就开始框选高亮图形的交互语法组装吧,为了帮助大家理解,每个交互行为我们都会以自然语言 + 交互语法的形式向大家阐述。我们以柱状图的高亮为例,实现交互的过程中我们会使用 G2 内置的 Action,Action 的定义和列表参考 G2 配置交互。

框选中,高亮图形

b6.gif
b5.gif

交互的语言描述

  1. 鼠标进入绘图区域时变成十字,鼠标离开绘图区域时恢复正常;
  2. 按下鼠标,拖动鼠标开始框选,出现框选的遮罩层(mask),框选过程中被框选的图形高亮;
  3. 松开鼠标按键,框选结束;
  4. 双击画布,隐藏遮罩层,图形的高亮效果。

G2 交互语法

registerInteraction('element-range-highlight', {
  showEnable: [
    { trigger: 'plot:mouseenter', action: 'cursor:crosshair' },
    { trigger: 'plot:mouseleave', action: 'cursor:default' },
  ],
  start: [
    {
      trigger: 'plot:mousedown',
      action: ['rect-mask:start', 'rect-mask:show'],
    }
  ],
  processing: [
    {
      trigger: 'plot:mousemove',
      action: ['rect-mask:resize', 'element-range-highlight:highlight'],
    },
  ],
  end: [
    { trigger: 'plot:mouseup',
      action: ['rect-mask:end']
    },
  ],
  rollback: [
    { 
      trigger: 'dblclick', 
      action: ['element-range-highlight:clear', 'rect-mask:hide'] 
   	}
  ],
});

我们使用了三个 Action 来实现这个区域高亮功能:

  • cursor控制鼠标样式的 Action,这个 Action 的方法支持所有的鼠标样式,例如 'pointer'、'crosshair'、'move' 等。
  • rect-mask 矩形的遮罩层,这个 Action 支持的方法有:
    • start:开始,表示遮罩层开始改变;
    • show:显示;
    • resize: 改变形状;
    • hide: 隐藏;
    • end: 结束。
  • element-range-highlight图表元素的区域高亮,支持的方法有:
    • highlight:高亮;
    • clear: 清除高亮。

**
交互语法解释:

  • 我们使用了多个环节来组装框选高亮的交互: showEnable 意味着交互是否可以进行; start 表示交互开始进行; processing 表示交互持续进行; end 表示交互结束; rollback 表示交互回滚。这些过程不完全是顺序的,例如:框选结束后,不需要回滚就可以继续开始新的框选; showEnable 在各个环节中都生效。
  • 你可以更改任何环节中的实现,例如你可以在 end 环节中的 action 增加 'rect-mask:hide' 这时候框选结束后遮罩层消失,但是框选的高亮效果还存在。

框选后,拖拽 mask

框选高亮后我们可以开始新的框选,但是如果能够拖拽遮罩层 (mask)使得被遮罩的图形高亮,体验更好。

b1.gif

交互的语言描述

这个交互的步骤如下:

  1. 当鼠标进入绘图区域时鼠标变成十字,当鼠标离开绘图区域时鼠标恢复正常;
  2. 当鼠标进入遮罩层时鼠标变成移动形状,当鼠标离开遮罩层时鼠标变成十字;
  3. 在不是遮罩层的地方按下鼠标并移动鼠标(拖拽),开始显示遮罩层,拖拽过程中遮罩层跟随鼠标变化,同时被遮挡的图形高亮;
  4. 松开鼠标框选结束;
  5. 当拖拽遮罩层时,遮罩层跟随鼠标移动,被遮挡的图形高亮;
  6. 松开鼠标拖动遮罩层结束;
  7. 双击画布,遮罩层隐藏,图形高亮效果取消。

注意这个交互同上面交互的差异,新增了 2、5 和 6 三个步骤,遮罩层的变化 3 需要增加触发条件。

G2 交互语法

registerInteraction('element-range-highlight', {
  showEnable: [
    { trigger: 'plot:mouseenter', action: 'cursor:crosshair' },
    { trigger: 'mask:mouseenter', action: 'cursor:move' },
    { trigger: 'plot:mouseleave', action: 'cursor:default' },
    { trigger: 'mask:mouseleave', action: 'cursor:crosshair' },
  ],
  start: [
    {
      trigger: 'plot:mousedown',
      isEnable(context) { // 不要点击在 mask 上重新开始
        return !context.isInShape('mask');
      },
      action: ['rect-mask:start', 'rect-mask:show'],
    },
    {
      trigger: 'mask:dragstart',
      action: ['rect-mask:moveStart']
    }
  ],
  processing: [
    {
      trigger: 'plot:mousemove',
      action: ['rect-mask:resize'],
    },
    {
      trigger: 'mask:drag',action: ['rect-mask:move']
    },
    {
      trigger: 'mask:change', action: ['element-range-highlight:highlight']
    }
  ],
  end: [
    { trigger: 'plot:mouseup',
      action: ['rect-mask:end']
    },
    { trigger: 'mask:dragend', action: ['rect-mask:moveEnd']},
  ],
  rollback: [{ trigger: 'dblclick', action: ['element-range-highlight:clear', 'rect-mask:hide'] }],
});

我们根据这个交互同前一个交互的差别,来逐条增加新的步骤(触发和反馈):

  • **2 **在 showEnable 上增加鼠标移入 mask 和移出 mask 的效果。
  • 3start 环节中触发遮罩层变化的步骤中增加是否在遮罩层上触发的判定。
  • 5start 环节中增加拖拽遮罩层,在 processing 中增加遮罩层移动、图形根据遮罩层变化而高亮的步骤。
  • 6 在 end 环节中结束遮罩层的移动。

通过这个示例,我们可以看到如何来扩展一个交互。框选本身不是交互的目的,框选后的操作才是框选交互的目的,框选后可以进行数据过滤、详情展示、显示隐藏等,下面我们通过几个交互来进行说明。

框选后,过滤数据

框选后对数据进行过滤时常见的操作,为了让用户意识到过滤已经发生,并且显示的告诉用户如何恢复,我们在框选发生过滤够显示了一个 reset 按钮。

b2.gif

交互的语言描述

  1. 鼠标进入绘图区域变成十字,鼠标离开绘图区域恢复正常;
  2. 鼠标移入 reset 按钮变成 pointer,鼠标离开变成十字;
  3. 按下鼠标拖拽出现遮罩层,拖拽过程中遮罩层变化;
  4. 松开鼠标,对图表的数据进行过滤,并显示 reset 按钮;
  5. 点击 reset 按钮,数据过滤取消。

G2 交互语法

registerInteraction('brush', {
  showEnable: [
    { trigger: 'plot:mouseenter', action: 'cursor:crosshair' },
    { trigger: 'plot:mouseleave', action: 'cursor:default' },
    { trigger: 'reset-button:mouseenter', action: 'cursor:pointer' },
    { trigger: 'reset-button:mouseleave', action: 'cursor:crosshair' },
  ],
  start: [
    {
      trigger: 'plot:mousedown',
      action: ['brush:start', 'rect-mask:start', 'rect-mask:show'],
    },
  ],
  processing: [
    {
      trigger: 'plot:mousemove',
      action: ['rect-mask:resize'],
    },
  ],
  end: [
    {
      trigger: 'plot:mouseup',
      action: ['brush:filter', 'brush:end', 'rect-mask:end', 
               'rect-mask:hide', 'reset-button:show'],
    },
  ],
  rollback: [
    { 
      trigger: 'reset-button:click', 
     	action: ['brush:reset', 'reset-button:hide', 'cursor:crosshair'] 
    }],
});

我们使用了四个 Action 来实现这个区域高亮功能, cursor 和 rect-mask 已经介绍过,这里介绍其他两个:

  • brush 通过指定范围来过滤数据,有下面几个方法:
    • start 开始过滤的位置
    • end 结束过滤的位置
    • filter 过滤
    • reset 恢复过滤
  • reset-button恢复按钮,仅有显示和隐藏两个方法:
    • show 显示
    • hide 隐藏

**
几点说明:

  • G2 内部提供了基础的 button Action,所有个性化的 button 都可以从其中扩展出来,这个示例中的 Action(reset-button) 就是修改文本和位置而生成的。
  • Action brush 通过开始位置和结束位置确定过滤的区域,另外还有两个类似的 Action:
    • brush-x 仅仅过滤 x 轴范围内的数据;
    • brush-y 仅仅过滤 y 轴范围内的数据。

对应 rect-mask 也有两个 Action: x-rect-mask,y-rect-mask。

b6-1.gif

框选后,过滤图形

框选后不进行数据过滤,而仅仅控制图形的显示隐藏也是常见的,交互,我们来看一下这个交互的实现:

b3.gif

交互的语言描述

  1. 鼠标进入绘图区域变成十字,鼠标离开绘图区域恢复正常;
  2. 鼠标按下,拖拽过程中遮罩层变化大小,被遮挡的图形高亮;
  3. 松开鼠标,被遮挡的图形保留,其他图形隐藏;
  4. 双击画布,取消图形过滤。

G2 交互语法

registerInteraction('brush-visible', {
  showEnable: [
    { trigger: 'plot:mouseenter', action: 'cursor:crosshair' },
    { trigger: 'plot:mouseleave', action: 'cursor:default' },
  ],
  start: [
    {
      trigger: 'plot:mousedown',
      action: ['rect-mask:start', 'rect-mask:show', 'element-range-highlight:start'],
    },
  ],
  processing: [
    {
      trigger: 'plot:mousemove',
      action: ['rect-mask:resize','element-range-highlight:highlight'],
    },
    {trigger: 'mask:end',action: ['element-filter:filter']}
  ],
  end: [
    {
      trigger: 'plot:mouseup',
      action: ['rect-mask:end', 'rect-mask:hide', 
               'element-range-highlight:end', 'element-range-highlight:clear'],
    },
  ],
  rollback: [
    {
      trigger: 'dblclick',
      action: ['element-filter:clear']
    }
  ]
});

这个交互中我们使用到了,其中 cursorrect-maskelement-range-highlight 三个 Action 前面已经介绍到,这里对新的 Action element-filter 进行说明:

  • element-filter[8]:过滤图表元素,有两个方法:
    • filter:过滤
    • clear:清理过滤

从上面的几个交互我们可以看到,多个交互之间会共享大量的 Action,这就解决了交互代码复用的问题,为提升开发交互的效率和提升质量提供了保障。

扩展

框选的形状选择

b6-2.gif
b6-3.gif

交互的语言描述

这个交互的步骤同前面的几个交互类似,最大的差别在于:鼠标在画布上拖拽时,根据鼠标移动的轨迹改变遮罩层的形状

G2 交互语法

registerInteraction('element-range-highlight', {
  showEnable: [
    { trigger: 'plot:mouseenter', action: 'cursor:crosshair' },
    { trigger: 'mask:mouseenter', action: 'cursor:move' },
    { trigger: 'plot:mouseleave', action: 'cursor:default' },
    { trigger: 'mask:mouseleave', action: 'cursor:crosshair' },
  ],
  start: [
    {
      trigger: 'plot:mousedown',
      isEnable(context) { // 不要点击在 mask 上重新开始
        return !context.isInShape('mask');
      },
      action: ['path-mask:start', 'path-mask:show'],
    },
    {
      trigger: 'mask:dragstart',
      action: ['path-mask:moveStart']
    }
  ],
  processing: [
    {
      trigger: 'plot:mousemove',
      action: ['path-mask:resize'],
    },
    {
      trigger: 'mask:drag',action: ['path-mask:move']
    },
    {
      trigger: 'mask:change', action: ['element-range-highlight:highlight']
    },
    {trigger: 'mask:end',action: ['element-filter:filter']}
  ],
  end: [
    { trigger: 'plot:mouseup',
      action: ['path-mask:end']
    },
    { trigger: 'mask:dragend', action: ['path-mask:moveEnd']},
  ],
  rollback: [
    { 
      trigger: 'dblclick', 
     	action: ['element-range-highlight:clear', 
               'path-mask:hide', 'element-filter:clear'] 
    }],
});

这个交互同上面 “框选后的操作-拖拽” 完全一致,除了显示遮罩层的 Action 从 rect-mask 替换成 path-mask 之外,这个 Action 同 rect-mask 的方法完全一致:

  • path-mask 矩形的遮罩层,这个 Action 支持的方法有:
    • start:开始,表示遮罩层开始改变
    • show:显示
    • resize: 改变形状
    • hide: 隐藏
    • end: 结束

除了 rect-maskpath-mask 之外 G2 还内置了其他两种 circle-masksmooth-path-mask,除了形状不同外提供的方法完全相同。

多视图的框选联动

b4.gif

交互的语言描述

  1. 鼠标进入一个 View 的绘图区域时变成十字,离开 View 的绘图区域时恢复正常;
  2. 鼠标按下进行框选,在当前 View 上显示遮罩层,同时被遮挡的图形高亮;其他 View 上同当前高亮图形的数据对应的图形同时高亮;
  3. 松开鼠标,遮罩层结束改变;
  4. 在遮罩层上拖拽,被遮挡的图形高亮;其他 View 上同当前高亮图形的数据对应的图形同时高亮;
  5. 松开鼠标,遮罩层位置改变结束;
  6. 双击画布,高亮效果取消。

G2 交互语法

registerInteraction('highlight-view', {
  showEnable: [
    { trigger: 'plot:mouseenter', action: 'cursor:crosshair' },
    { trigger: 'mask:mouseenter', action: 'cursor:move' },
    { trigger: 'plot:mouseleave', action: 'cursor:default' },
    { trigger: 'mask:mouseleave', action: 'cursor:crosshair' },
  ],
  start: [
    { trigger: 'plot:mousedown',isEnable(context) {
      return !context.isInShape('mask');
    }, action: ['rect-mask:start', 'rect-mask:show'] },
    {trigger: 'mask:dragstart', action: 'rect-mask:moveStart'}
  ],
  processing: [
    { trigger: 'plot:mousemove', action: 'rect-mask:resize' },
    { trigger: 'mask:drag', isEnable(context) {
      return context.isInPlot();
    }, action: 'rect-mask:move'},
    { trigger: 'mask:change', 
     action: ['element-sibling-highlight:highlight', 
              'element-range-highlight:highlight'] }
  ],
  end: [
    { trigger: 'plot:mouseup', action: 'rect-mask:end' },
    { trigger: 'mask:dragend', action: 'rect-mask:moveEnd' },
    {
      trigger: 'document:mousedown',
      isEnable(context) {
        return !context.isInPlot();
      },
      action: ['element-sibling-highlight:clear',
               'element-range-highlight:clear', 
               'rect-mask:end', 'rect-mask:hide'],
      once: true,
    },
    {
      trigger: 'document:mouseup',
      isEnable(context) {
        return !context.isInPlot();
      },
      action: ['rect-mask:end'],
      once: true,
    }
  ],
  rollback: [
    { 
     trigger: 'dblclick', 
     action: ['rect-mask:hide', 'element-sibling-highlight:clear', 
              'element-range-highlight:clear']}
  ]
});

这个交互同前面提到 “框选后的操作-拖拽” 几乎一模一样,除了在调用 Action element-range-highlight 的同时也调用了 element-sibling-highlight 的方法,其含义是“高亮图形” 的同时 “高亮所有同级 views 的图形”。

  • element-sibling-highlight[10]:高亮当前 view 同一级的 views 的对应图形,这个 Action 的方法有:
    • highlight:高亮
    • clear: 清除高亮

**
更多的解释:
细心的读者可能关注到这个交互中出现了几个前面交互中没有出现的步骤(触发和反馈),这些都是一些异常处理的步骤,这关系到一个交互的质量:

  • document:mousedown 绘图区域之外按下起鼠标(面上其他位置 ),结束高亮并且隐藏遮罩层。
  • document:mouseup 绘图区域之外抬起鼠标(面上其他位置 ),则结束 rect-mask 的变化。
  • mask:drag 时添加约束条件,如果不再当前 View 的绘图区域,则不再移动。

b7.gif

除了多个 View 之间联动高亮,还可以进行联动过滤、图形隐藏、tooltip 联动等操作,我们会在后面的章节中给大家介绍。

总结

框选高亮作为图表中经常使用的交互,在不同的场景下框选的过程和结果都有可能变化,传统的固定死的交互方式并不能满足用户的需求。当我们有了交互语法,只要你能列出交互的步骤,就可以将这些步骤自然地转换成交互语法,高效而且高质,快去尝试一下吧!

网址

G2 官网: g2.antv.vision/zh/

github: github.com/antvis/G2

参考资料

  1. 可视化交互语法
  2. G2 内置交互反馈
  3. cursor
  4. rect-mask
  5. element-highlight
  6. brush
  7. reset-button
  8. element-filter
  9. path-mask
  10. element-sibling-highlight