简介

G6 从 3.0-beta.1 发布到现在已经发布了 10 几个版本,从最初的仅支持关系图的展示,到支持 editor 底层机制,到支持了一些分析场景的业务,在这个过程中发现了一些问题,特别是交互方面,本章讨论交互状态问题,节点和边的状态量的设计当初想的过于简单,出现了下面的问题:

  • 节点和边的交互状态过多,单个节点或者边有5个以上的状态。
  • 多个状态之间存在互斥

问题分析

由于原先的对状态量的设计过于简单,仅考虑了状态的设置和取消 (true / false) ,而且 false 时仅取消时执行,后面节点刷新时不再调用,也带来了一些理解上的成本。用户认为的同一个节点状态量有多个值时,只能拆解成多个变量。
以节点的状态进行举例,在一个通用场景下节点有以下状态:

  • hover,鼠标移动到节点上的状态
  • selected,点击、框选选中
  • highlight, 高亮,点击某被节点时高亮所有相邻的节点(不同于 selected)
  • dark,变暗, 所有未被高亮的变暗
  • 源点,点击边时显示源点
  • 结束点,点击边时区分显示结束点
  • 关键路径点,点击选中两个节点,显示关键路径

我们会发现这些状态应用到节点上时需要考虑一些问题:

  • 样式的冲突问题,多个状态改变相同的样式
  • 状态的互斥问题,多个状态不可能同时存在

状态量互斥

假设我们有四个互斥的状态:

  • dark: 变暗
  • light: 变亮
  • light:source:作为源点变亮
  • light:target :作为结束点变亮

当我们设置 dark 时,需要取消其他状态,看一下在这种情况下代码中的写法:

  1. // 设置 dark 前需要把其他状态都取消掉
  2. graph.setItemState(node, 'light', false);
  3. graph.setItemState(node, 'light:source', false);
  4. graph.setItemState(node, 'light:target', false);
  5. graph.setItemState(node, 'dark', true);
  6. // 改成 light
  7. graph.setItemState(node, 'dark', false);
  8. graph.setItemState(node, 'light:source', false);
  9. graph.setItemState(node, 'light:target', false);
  10. graph.setItemState(node, 'light', true);
  11. // 取消所有
  12. graph.setItemState(node, 'dark', false);
  13. graph.setItemState(node, 'light:source', false);
  14. graph.setItemState(node, 'light:target', false);
  15. graph.setItemState(node, 'light', false);
  • 互斥的状态量必须在设置前,将其他状态量设置成 false,否则会出问题

有两种方案可以解决这个问题:

  • 增加一个清理状态量的接口
  • 支持单个状态量支持多个值

清理状态量

增加一个 clearItemStates(Item, states) 接口来处理这个问题,上面的写法可以写成:

  1. const states = ['light', 'light:source', 'light:target', 'dark'];
  2. // 设置 dark 前需要把其他状态都取消掉
  3. graph.clearItemStates(node, states);
  4. graph.setItemState(node, 'dark', true);
  5. // 改成 light
  6. graph.clearItemStates(node, states);
  7. graph.setItemState(node, 'light', true);
  8. // 取消所有
  9. graph.clearItemStates(node, states);
  • 逻辑同上面的实现完全一样,但是代码更加简单好组织,同时也易于理解
  • 需要缓存所有的状态量,算是一个最佳实践,G6 已经支持这种方式

状态量支持多值

可以将多个状态量合并成一个 nodeActive 包含 ‘light’, ‘light:source’, ‘light:target’, ‘dark’ 4 个值,可以实现成:

  1. // 设置 dark
  2. graph.setItemState(node, 'nodeActive','dark');
  3. // 设置 light
  4. graph.setItemState(node, 'nodeActive','light');
  5. // 清理
  6. graph.setItemState(node, 'nodeActive',null);
  • 这种写法更好理解,代码更简单
  • 但是存在概念一致的问题, null 作为取消状态量有些不好理解,特别是单值状态量 selected 和 active 时 false 作为取消状态量的值同这里也不一致。
  • 注意 : 这种方式还没有支持,原因在于状态对应的配置项和自定义 shape 中有大量需要做的功能

状态量的实现

自定义节点修改状态量

定义节点的状态量后,需要思考每个状态量对应的样式,针对用户的这个诉求 G2 在自定义 Shape 的时候提供了 setState 方法供用户进行复写:

  1. G2.registerNode('custom', {
  2. setState(name, value, item) {
  3. const keyShape = item.getKeyShape();
  4. if (name == 'selected') {
  5. if(value) {
  6. // do something
  7. } else {
  8. // do something
  9. }
  10. } else if(name == 'active') {
  11. // do something
  12. }
  13. }
  14. }, 'rect');

通过复写自定义 shape 上的接口,这是一种最灵活的方式,
优点:

  • 可以针对每种状态的设置、取消进行样式的调整。
  • 也可以通过添加元素、增加动画来实现某些状态

缺点:

  • 需要自己处理状态量取消时的回滚,需要考虑多个状态改变单个属性的场景。

    通过配置项设置状态量

    在 3.0.0-beta.16 之后的版本,G6 增加了在 graph 层支持状态属性的配置项:
  1. const graph = new Graph({
  2. nodeStateStyles: {
  3. 'selected': {
  4. fill: 'red'
  5. },
  6. 'active': {
  7. lineWidth: 2
  8. }
  9. }
  10. });
  11. graph.setItemState(item, 'selected', true);
  12. graph.setItemState(item, 'selected', false);
  • 设置状态和回滚状态,样式会进行自动的设置和回滚
  • 没有任何内置的状态名,selected, active 都是用户自己定义的状态名