简介
为什么要增加 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(); // 还不知道怎么提供这几个 scale
const 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 和 line
chart.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 方案仅仅是当前的一个设计,会持续修改,直到满足需求!