Antv 过去 5 年中,在很多可视化领域进行了探索,在统计图表、可视分析、关系图、地理可视化等可视化场景中都面临如何把数据转换成图形(可视化编码)的问题,数据一旦以图形的方式呈现给用户,用户则需要在上面进行交互,查看数据细节从不同的层面对数据进行探查,以一句可视化领域常说的话来总结:“Overview first, zoom and filter, then details-on-demand"。
《Visuallization Analysis&Design》是这个领域的一本非常经典的书籍,感兴趣的不能错过。
G2(统计图表) 、F2(移动端图表)、L7(地理可视化)和 G6(关系可视化) 都在 Overview first 上有了很大的收获,通过可视化编码我们已经探索了所有常见图表如何从数据转换成图形。从而在 G2、F2 上形成图形语法,在 L7 上使用地理符号学,在 G6上也支持数据映射到节点和边的状态,但是接下来的交互我们还没有形成统一的理论依据,各自为政。今天我们在 G2 4.0 上根据过去的经验和教训总结出一套交互语法,初步验证了完备性、易用性和实现交互的效率,我们将会推广到其他产品上。
以 G2 为例,我们在交互方面的探索主要有三个阶段:
图表的交互内置是一种直觉的实现方式,在通用图表的开发过程中,我们可以枚举每种图表支持的交互:
但是一旦图表库同产品进行结合事情就没那么简单,很多交互同图表相关,但是又与产品的设计相关:框选、过滤、排序和标注等交互需要在不同的产品中有不同的触发形式,而交互一旦内置,用户很难改变交互的触发方式和结果。
一旦用户面临交互的改造,我们的答疑成本会激增,作为图表的开发者也很难改变交互的形态只能在上面增加配置项,打上一个个的补丁。
出于交互可以扩展,用户可以自由定制交互的目的,我们在 G2 v3.4、F2 v3.3、G6 v2.1 版本上增加可以注册的交互,交互不再同渲染和图表的声明周期进行耦合,依然以 G2 为例:
股票指数走势探索 | brush结合dataset |
散点图缩放交互 | brush过滤图形 |
其核心思想是:
chart.interaction('brush'); 复制代码
这种方案对于扩展性和用户直接使用已经写好的交互有比较好的支持,但是来定制交互的用户需要深入理解各个产品的内部细节,例如:框选了画布,有哪些图形会被框选,图形对应的数据有哪些,如何进行过滤。
最终的结果是: 这套注册交互的机制,主要还是图表库(可视化工具)的开发者在使用,很多细节也实现的不够好
前面两种交互的实现方式有各种各样的问题,因此我们在寻找一种易于使用、易于扩展、易于理解的交互模式,由 G2 的图形语法我们想到了能否创造一套完备的交互语法。
如何在不影响现有图表声明周期(渲染、更新)的前提下,用户可以自由搭配交互,这看似是个无解的问题,我们回到 G2 的起点 《The Grammar of Graphics》 ,Leland Wilkinson 将数据转换为图形的过程拆解为:
回到可视化的交互方面,并没有一套成熟的理论,能够提供出足够的抽象,将现有的交互总结到一套框架中。我们在 唐纳德·A·诺曼 的 《设计心理学》中找到依据,他将交互过程划分为不同的阶段:
交互的目的决定: 用户从哪里开始交互,最终的交互结果是什么
对交互目的的详细说明,参看 交互概述
我们将交互聚焦于可视化的场景时,整理了常见的交互后,发现这些交互都有一致的过程,我们可以将一个交互分解成多个交互环节:
大多数的交互仅使用上面的部分过程,但是也有些交互需要所有的过程,我们以一个框选高亮为示例:
注意:这个交互当前的实现比较简单,没有考虑拖拽 mask、异常操作等,其最完整的形式我们会在后文中给出,证明交互语法的完备性。
从上面列举的框选过滤的示例中,我们可以看到一个交互过程中各个环节都需要触发对象、触发事件和反馈,我们简单的将一个环节分为:
我们可以看到交互的目的、概念模型贯穿整个过程,用户需要理解哪些对象可以进行交互,一个交互有哪些环节,每个环节是否必要,是否清晰,交互的最终结果是否符合用户的目标。
理论距离实现由很大的距离,在可视化的过程中实现交互语法,需要综合考虑交互目的、交互过程、触发和反馈。我们依然以 G2 为示例,讲解交互语法从设计到实现的整个过程。通过上面的讨论我们可以抽象出交互的概念模型:
通过上面的概念模型我们可以得出交互的结构图:
一个交互有多个交互环节,交互环节之间有顺序关系和并行关系:
一个交互环节可以有多个触发和反馈,每个触发分为:
我们对 G2 所有可以触发交互的元素,都进行梳理并且命名:
可以触发的事件有浏览器支持的所有事件:
触发对象和触发的事件可以组合使用,例如 'axis-lable:mouseenter'
每个交互都需要反馈,以表示交互正在进行,同时最终的反馈也就是用户操作的目的。我们将交互的反馈命名为 Action,反馈也分为:
由于反馈要表示交互的可以进行、已经进行、持续和结束,我们总结所有的交互反馈的对象,这些交互对象可以是:
反馈的行为同反馈对象相关,就是每个反馈对象可以支持的响应,例如:
反馈对象和行为也可以在交互,进行组合,但是由于一个反馈的对象可以有非常多的行为,而共同的行为有需要多个方法来实现一个反馈行为可以有多个方法,例如图形的高亮(highlight)包括:
我们将上面的 Action 命名为 element-highlight
,一个反馈使用反馈行为和方法进行组合 'element-active:highlight', 'element-active:reset',
由此我们就可以通过触发和反馈搭配出一个交互的多个环节:
环节一:
trigger: 'element:mouseenter' action: 'element-highlight:highlight' 复制代码
环节二:
trigger: 'element:mouseleave' action: 'element-highlight:reset' 复制代码
进行组合,就完成了图形 highlight 的交互:
{ start: { trigger: 'element:mouseenter' action: 'element-highlight:highlight' }, end: { trigger: 'element:mouseleave' action: 'element-highlight:reset' } } 复制代码
通过上面的简单的示例我们可以看到交互可以通过定义交互环节,并在交互环节中配置触发和反馈来实现,回到我们一开始的框选高亮为示例,来看一下如何组合出这个交互:
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'}, {trigger: 'rect-mask:change', action: 'element-highlight:highlight'} ] 复制代码
end: [ {trigger: 'plot:mouseup', action: 'rect-mask:end'} ] 复制代码
rollback: [ {trigger: 'dblclick', action: ['rect-mask:hide', 'element-highlight:clear']} ] 复制代码
这个交互就完成了,但是我们在操作中发现,框选后还要能够拖拽遮罩(rect-mask),拖拽到画布外面时需要异常处理等,我们可以继续完善各个环节,最终效果如下:
{ 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']}, { trigger: 'document:mouseup', isEnable(context) { return !context.isInPlot(); }, action: ['element-range-highlight:clear', 'rect-mask:end', 'rect-mask:hide'], }, ], rollback: [{ trigger: 'dblclick', action: ['element-range-highlight:clear', 'rect-mask:hide'] }], } 复制代码
我们已经将所有 G2的交互全部通过交互语法进行了组装,开发交互的效率和质量得到极大的提升,下面是一些示例:
目前 G2 4.0 已经完成上面所有环节的设计和开发,并已经实现 3.x 默认的所有交互,你也可以自由组合出更多的交互,我们还将在拖拽组件/视图重布局、拖拽图形重计算、拖拽合并以及图例排序等方面进行进行交互语法的组装。G2 4.0将于 2020 年 2月 27 日正式发布,敬请期待!
G2 官网: g2.antv.vision/zh/
github: github.com/antvis/G2