简介
为什么要增加 Element?
在 G2 2.x 和 3.x 中,用户主要面对的对象是 Chart,仅仅有限的用户会接触自定 shape 中图形元素的创建,但是当监听事件是图形元素就会突兀的显示出来。用户仅仅进行图表的渲染,使用默认的几个交互时还能满足需求,但是当需求一旦复杂,直接操作图形元素的弊端就会显现,也无法实现局部数据的更新。
我们增加 Element 主要解决几个问题:
- 让操作图形更加简单,用户不需要理解底层的绘图代码也可以实现图形的操作
 - 增加更多操作的可能性,例如通过调整图形更新数据等操作
 - 数据的增删改等操作
 
什么是 Element
目前 G2 上仅有 Chart、View 和 Geometry 三层容器,而一个 geometry 下回根据视觉通道 color, size,shape 对数据进行分组,然后进行绘制。一组数据在不同的 geometry 上的渲染方式不同,在点图上一条数据会对应一个点,线图上多条数据(一组数据)对应一条线,而这一个个图形元素就是 Element。所以 Element 可以分为单条数据对应的 Element 和 集合数据对应的 Element。
注意:这里指的图形元素是能够代表一条数据或者集合数据的图形,不一定仅仅对应 canvas 上的一个 shape ,有可能会有多个 shape。
单数据对应的 Element
G2 上单条数据对应的 Element 被下面几种 geometry 拥有:
- point
 - interval
 - polygon
 - schema
 - 
集合数据对应的 Element
下面几种 geometry 的 Element 包含多条数据:
 line
- path
 - area
 - heatmap 热力图算是一整张 image 对象
 
Element 的功能
要解决上面的问题,Element 必须具备下面功能:
- 查找 Element 的功能
 - 支持各种操作状态(active, selected, visible)
 - 支持更新图形
 - 支持更新单条数据
 - 
Element 的查找
可以在 chart 和 view 上增加两种查找方式:
 获取自己和子 view 上所有的 Element 对象
根据条件查找 Element,主要是数据特征,根据画布上的位置查找 Element 可以考虑支持
局部更新
Element 在用户在图表上做交互时需要进行更新,主要分为 3 种场景:
第一种是纯粹响应交互的状态变化,如 active,selected 等交互状态
- 第二种是交互过程中临时对图形的更新,颜色、大小、位置等
 - 第三种是更新 Element 对应的数据,如果度量超出原先的范围,则可能引起整个图的刷新
状态变化
常见的 active,selected, highlight 等交互不会影响原始数据,所以考虑在 Element 上增加接口,自定义 shape 时定义不同状态下的变化响应,不同的 shape 可以定义自己的状态变化。图形的更新
一些细粒度的交互无法通过状态来实现,只能操作 Element 对应的图形,交互结束时再更新 Element 对应的数据。例如一个拖拽柱状图的例子,可以将柱状图拖拽到其他分类上,对数据进行合并,拖拽过程触发次数太多,不适合直接更新数据源,而仅需要更新图形属性即可。数据的更新
数据更新时 Element 的显示会同步更新,同时有可能会引起整个图表的变化,导致全局的刷新,可以通过不同的接口来分辨是否进行全局刷新。 
Element 的实现
查找方法
- chart/view/geometry.getAllElements();
 chart/view/geometry.findElements(callback);
状态相关
setState(name, value) : 设置交互状态,常见的有 active, selected 等
- hasState()
 - getStates()
 - 
图形
 getContainer() // 获取图形的容器
- getBBox() 包围盒
 - toFront()
 - 
数据
 getModel()
- 
更新
 updateModel(model)
更新 Element 有多种方式:
- 可以根据 Element 中的数据在输入的集合中找到,在集合中进行 增删改,然后调用 chart.repaint() 方法,整体刷新
 - 也可以直接调用 Element 上的方法,局部更新单条数据
 - 还可以直接修改图形属性,而不更新数据,刷新后会丢失修改
问题
 
- 图形的交互都是基于画布的,需要将画布坐标同原始数据进行转换
- 首先画布坐标通过坐标系转换成 0-1 之内的值
 - 0-1 的值再通过 scale 转换成原始值
 
 - 在自定义 shape 中图形通过 points 生成,而 points 是介于 0-1 之间的数值,所以很难通过更新 points 修改图形,而且不是所有的图形都有 points。
 - 如果在交互过程中直接修改图形属性,而最终更新数据,两者会存在一定的 gap,交互流程性存在问题。
 
Element 的使用
以几个示例来显示 Element 的使用,同时看一下其中复杂和不顺利的地方
注意:示例中仅实现核心代码,一些函数调用并不具体实现
几个示例
用户的常用交互中,拖拽是个比较复杂的交互,所以下面几个以拖拽为例,来看一下使用 Element 中的问题
框选散点图,过滤
鼠标在散点图上进行框选,框选时出现矩形,矩形内部的点被高亮,鼠标释放后,过滤显示选中的数据。
let start;function getIntersectElements(startPoint, endPoint) {const elements = char.findElements(el => {const bbox = el.getBBox();return intesect(bbox, startPoint, endPoint);});return elements;}chart.on('dragstart', ev => {start = {x: ev.x, y: ev.y};});chart.on('dragmove', ev => {const currentPoint = {x: ev.x, y: ev.y};showMask(start, currentPoint); // 显示矩形const elements = getIntersectElements(start, currentPoint);elements.forEach(el => {el.setState('active', true);});});chart.on('dragend', ev => {const elements = char.findElements(el => {return el.hasState('active');});const data = elements.map(el => el.getModel());chart.changeData(data);});
拖拽柱状图,两个柱子合并
let start ;let interval;let hoverInterval;function moveElement(interval, startPoint, endPoint) {const distance = {x: endPoint.x - startPoint.x,y: endPoint.y - startPoint.y};// 移动图形,有多种方式// 第一种在 group 上设置 translate,但是这种方案,需要在最后面移除掉 group 上的矩阵// 另外存在的问题是:coord 可能已经发生变换,需要考虑,否则坐标系翻转、缩放时就会出现问题const group = interval.getContainer();group.transform([['t', distance.x, distance.y]]);// 第二种修改原始数据,你需要知道 x,y 对应的 scale,需要知道 coord。这种方案基本不可行,过于复杂/* const xScale = Element.getXScale(); // 还不知道怎么提供这几个 scaleconst yScale = Element.getYScale();const coord = Element.getCoord();const invertPoint = coord.invert(endPoint.x, endPoint.y);const xValue = xScale.invert(invertPoint.x);const yValue = yScale.invert(invertPoint.y);const cfg = {};cfg[xScale.field] = xValue;cfg[yScale.field] = yValue;element.updateModel(cfg);*/}chart.on('interal:dragstart', ev => {inteval = ev.element;srart = {x: ev.x, y: ev.y};});chart.on('interal:dragmove', ev => {const current = {x: ev.x, y: ev.y};moveElement(interval, start, current);if (hoverInterval) {hoverInterval.setState('active', false);}hoverInterval = getIntersectElement(interal);if (hoverInterval) {hoverInterval.setState('active', true);}});chart.on('interval:dragend', ev => {if (hoverInterval) {mergeElements(interval, hoverInterval);}// 无论是否拖拽到 interval 上,图表都会重绘chart.repaint();});
拖拽折线图上的点,折线图形状变化
// 假设有两个 geometry, point 和 linechart.line().position('x*y').color('z');chart.point().position('x*y').color('z');let start;// 在点图上添加事件chart.on('point:dragstart', ev => {srart = {x: ev.x, y: ev.y};});chart.on('point:dragmove', ev => {const current = {x: ev.x, y: ev.y};// 再次面临上面拖拽 interval 一样的问题,这里只能使用数据更新,因为折线也需要同时更新// 这里需要在 chart,geometry,view 上封装一个按照位置获取数据值的方法const newRecord = chart.getRecord(current);element.updateModel(newRecord);});chart.on('point.dragend', ev => {// 最后整个图更新chart.repaint(); // 或者 refresh 方法,需要区分,render, refresh ,repaint 的差异});
总结
Element 是用户实现细粒度交互的基础,用户可以通过 Element 设置状态,移动图形,局部更新数据,但是需要对整个数据的变更流程重新梳理,设计出更好用、易懂的接口。
注意:这个文档上的 Element 方案仅仅是当前的一个设计,会持续修改,直到满足需求!
