简介

为什么要增加 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
  • edge

    集合数据对应的 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()
  • show(),hide() 不实现成 state

    图形

  • getContainer() // 获取图形的容器

  • getBBox() 包围盒
  • toFront()
  • toBack()

    数据

  • getModel()

  • getMapping()

    更新

  • updateModel(model)

更新 Element 有多种方式:

  • 可以根据 Element 中的数据在输入的集合中找到,在集合中进行 增删改,然后调用 chart.repaint() 方法,整体刷新
  • 也可以直接调用 Element 上的方法,局部更新单条数据
  • 还可以直接修改图形属性,而不更新数据,刷新后会丢失修改

    问题

  1. 图形的交互都是基于画布的,需要将画布坐标同原始数据进行转换
    • 首先画布坐标通过坐标系转换成 0-1 之内的值
    • 0-1 的值再通过 scale 转换成原始值
  2. 在自定义 shape 中图形通过 points 生成,而 points 是介于 0-1 之间的数值,所以很难通过更新 points 修改图形,而且不是所有的图形都有 points。
  3. 如果在交互过程中直接修改图形属性,而最终更新数据,两者会存在一定的 gap,交互流程性存在问题。

Element 的使用

以几个示例来显示 Element 的使用,同时看一下其中复杂和不顺利的地方
注意:示例中仅实现核心代码,一些函数调用并不具体实现

几个示例

用户的常用交互中,拖拽是个比较复杂的交互,所以下面几个以拖拽为例,来看一下使用 Element 中的问题

框选散点图,过滤

鼠标在散点图上进行框选,框选时出现矩形,矩形内部的点被高亮,鼠标释放后,过滤显示选中的数据。

  1. let start;
  2. function getIntersectElements(startPoint, endPoint) {
  3. const elements = char.findElements(el => {
  4. const bbox = el.getBBox();
  5. return intesect(bbox, startPoint, endPoint);
  6. });
  7. return elements;
  8. }
  9. chart.on('dragstart', ev => {
  10. start = {x: ev.x, y: ev.y};
  11. });
  12. chart.on('dragmove', ev => {
  13. const currentPoint = {x: ev.x, y: ev.y};
  14. showMask(start, currentPoint); // 显示矩形
  15. const elements = getIntersectElements(start, currentPoint);
  16. elements.forEach(el => {
  17. el.setState('active' true);
  18. });
  19. });
  20. chart.on('dragend', ev => {
  21. const elements = char.findElements(el => {
  22. return el.hasState('active');
  23. });
  24. const data = elements.map(el => el.getModel());
  25. chart.changeData(data);
  26. });

拖拽柱状图,两个柱子合并

  1. let start ;
  2. let interval;
  3. let hoverInterval;
  4. function moveElement(interval, startPoint, endPoint) {
  5. const distance = {
  6. x: endPoint.x - startPoint.x,
  7. y: endPoint.y - startPoint.y
  8. };
  9. // 移动图形,有多种方式
  10. // 第一种在 group 上设置 translate,但是这种方案,需要在最后面移除掉 group 上的矩阵
  11. // 另外存在的问题是:coord 可能已经发生变换,需要考虑,否则坐标系翻转、缩放时就会出现问题
  12. const group = interval.getContainer();
  13. group.transform([['t', distance.x, distance.y]]);
  14. // 第二种修改原始数据,你需要知道 x,y 对应的 scale,需要知道 coord。这种方案基本不可行,过于复杂
  15. /* const xScale = Element.getXScale(); // 还不知道怎么提供这几个 scale
  16. const yScale = Element.getYScale();
  17. const coord = Element.getCoord();
  18. const invertPoint = coord.invert(endPoint.x, endPoint.y);
  19. const xValue = xScale.invert(invertPoint.x);
  20. const yValue = yScale.invert(invertPoint.y);
  21. const cfg = {};
  22. cfg[xScale.field] = xValue;
  23. cfg[yScale.field] = yValue;
  24. element.updateModel(cfg);
  25. */
  26. }
  27. chart.on('interal:dragstart', ev => {
  28. inteval = ev.element;
  29. srart = {x: ev.x, y: ev.y};
  30. });
  31. chart.on('interal:dragmove', ev => {
  32. const current = {x: ev.x, y: ev.y};
  33. moveElement(interval, start, current);
  34. if (hoverInterval) {
  35. hoverInterval.setState('active', false);
  36. }
  37. hoverInterval = getIntersectElement(interal);
  38. if (hoverInterval) {
  39. hoverInterval.setState('active', true);
  40. }
  41. });
  42. chart.on('interval:dragend', ev => {
  43. if (hoverInterval) {
  44. mergeElements(interval, hoverInterval);
  45. }
  46. // 无论是否拖拽到 interval 上,图表都会重绘
  47. chart.repaint();
  48. });

拖拽折线图上的点,折线图形状变化

  1. // 假设有两个 geometry, point 和 line
  2. chart.line().position('x*y').color('z');
  3. chart.point().position('x*y').color('z');
  4. let start;
  5. // 在点图上添加事件
  6. chart.on('point:dragstart', ev => {
  7. srart = {x: ev.x, y: ev.y};
  8. });
  9. chart.on('point:dragmove', ev => {
  10. const current = {x: ev.x, y: ev.y};
  11. // 再次面临上面拖拽 interval 一样的问题,这里只能使用数据更新,因为折线也需要同时更新
  12. // 这里需要在 chart,geometry,view 上封装一个按照位置获取数据值的方法
  13. const newRecord = chart.getRecord(current);
  14. element.updateModel(newRecord);
  15. });
  16. chart.on('point.dragend', ev => {
  17. // 最后整个图更新
  18. chart.repaint(); // 或者 refresh 方法,需要区分,render, refresh ,repaint 的差异
  19. });

总结

Element 是用户实现细粒度交互的基础,用户可以通过 Element 设置状态,移动图形,局部更新数据,但是需要对整个数据的变更流程重新梳理,设计出更好用、易懂的接口。

注意:这个文档上的 Element 方案仅仅是当前的一个设计,会持续修改,直到满足需求!