简介
在前面的章节里面大概介绍了新设计的思路,在这里展开进行讨论两种交互写法:
- 触发和反馈写到一起
- 触发和反馈分开写
实现
都写到一起
active
active 的场景非常简单,所以写到一起更加合适
G2.registerInteraction('active', {
init(chart) {
this.chart = chart;
this.initEvents(chart)
},
initEvents(chart) {
chart.on('element:mouseenter', Util.bind(this, 'onEnter'));
chart.on('element:mouseenter', Util.bind(this, 'onEnter'));
},
onEnter(ev) {
const element = ev.element;
element.setState('active', true);
},
onOut(ev) {
const element = ev.element;
element.setState('active', false);
},
destroy() {
this.chart.off('element:mouseenter', Util.bind(this, 'onEnter'));
this.chart.off('element:mouseenter', Util.bind(this, 'onEnter'));
}
});
- 写到一起时,不需要全局的 stateManager 管理状态量,直接调用 element 的方法即可
- 触发形式只有一种的情况下,这种方案非常合适也很好理解
highlight
highlight 比较复杂一些,需要高亮对应的节点,也需要将其他节点变暗,实现代码如下:G2.registerInteraction('highlight', {
init(chart) {
this.chart = chart;
this.initEvents(chart)
},
initEvents(chart) {
chart.on('element:mouseenter', Util.bind(this, 'onEnter'));
chart.on('element:mouseleave', Util.bind(this, 'onOut'));
},
onEnter(ev) {
const element = ev.element;
element.setState('active', true);
const allElements = this.chart.getAllElements();
allElements.forEach(el => {
if (el !== element) {
el.setState('dark', true);
}
});
},
onOut(ev) {
const element = ev.element;
element.setState('active', false);
const allElements = this.chart.getAllElements();
allElements.forEach(el => {
if (el !== element) {
el.setState('dark', false);
}
});
},
destroy() {
this.chart.off('element:mouseenter', Util.bind(this, 'onEnter'));
this.chart.off('element:mouseleave', Util.bind(this, 'onOut'));
}
});
selected
selected 同 active 类似,但是需要考虑单选、多选、取消选中的场景:
G2.registerInteraction('selected', {
init(chart) {
this.chart = chart;
this.initEvents(chart)
},
initEvents(chart) {
chart.on('element:click', Util.bind(this, 'onClick'));
},
onClick(ev) {
const element = ev.element;
// 清理状态
if (element.hasState('selected')) {
element.setState('selected', false);
} else {
// 清理原先选中的元素
const elements = chart.getAllElements();
elements.forEach(el => {
if (el.hasState('selected')) {
el.setState('selected', false);
}
});
element.setState('selected', true);
}
},
destroy() {
this.chart.off('element:click', Util.bind(this, 'onClick'));
}
});
tooltip
G2 中的 tooltip 主要有两中方式:
- 根据 x 轴的位置,批量显示数据
- 鼠标移动到的元素上显示数据
首先看一下批量显示数据
G2.registerInteraction('shared-tooltip', {
init(chart) {
this.chart = chart;
this.initEvents(chart)
},
initEvents(chart) {
chart.on('mousemove', Util.bind(this, 'onMove'));
},
onMove(ev) {
const elements = chart.getSnapElements({x: ev.x, y: ev.y}); // 虚拟的方法,获取逼近的元素
const items = [];
elements.forEach(el => {
const model = el.getModel(); // 获取 element 的数据
const originData = model.origin;
items.push({
title: originData.x,
name: 'xxx',
value: 'xxx'
});
// element.setState('active', true);
// 或者 element.getTooltipItems();
// 也可以作为 interaction 的回调函数 this.itemFormatter(record,index);
});
// 这个接口怎么设计
// tooltip 是否要
chart.showTooltip({
x: ev.x,
y: ev.y,
items: items
});
},
destroy() {
this.chart.off('mousemove', Util.bind(this, 'onMove'));
}
});
// 外面使用 tooltip 时:
chart.interaction('shared-tooltip', {
itemFormatter(record, index) {
return { name: 'xxx', value: 'xxx' }; // 这里面需要思考的是如何拿到这一项的 color
}
});
直接显示对应图形的 tooltip 也类似,这里面有一些问题需要解决:
- tooltip 信息在不同的图表上有不同的显示,可能要针对不同的图表分别写 interaction,line-tooltip,pie-tooltip 等,是否需要一个通用的 tooltip
- tooltip 信息如何让用户自定义,之前是通过注册到 geometry 上的,而现在则是用户在图表的外层实现
- tooltip 显示时同时需要高亮 element,而 shared-tooltip 的场景下默认的 active 无法高亮默认的多个 element。可以禁用 active 的 interaction,而在 tooltip 的 interaction 中进行 active 操作,但是这就导致一个交互做了多件事,同一个反馈在多个交互里面都需要实现,难免会出现冲突。
legend
我们这里考虑三个 legend 上的交互,
一个是在legend 上过滤数据,
- 第二个是在 legend hover 时高亮对应的图形
- 第三个是图形 active 时高亮对应的 legend 选项
- 数据过滤:
G2.registerInteraction('legend-category-filter', {
init(chart) {
this.chart = chart;
this.initEvents(chart)
},
initEvents(chart) {
chart.on('legend:chenge', Util.bind(this, 'onClick'));
},
onClick(ev) {
const legendItem = ev.item;
const legend = ev.legend;
const field = legendItem.field;
const value = legendItem.value;
const checked = legendItem.checked;
const elements = chart.getAllElements();
elements.forEach(el => {
const record = el.getModel().origin; // 还没设计好如何定义
if (record[field] === value) {
if (checked) {
element.show();
} else {
element.hide();
}
}
});
},
destroy() {
this.chart.off('legend:chenge', Util.bind(this, 'onClick'));
}
});
- 图例需要自己实现勾选功能,然后通过 change 事件同 interaction 中交互,并没有解开 legend 和 chart 的耦合,后面会提供解开耦合的版本,但是实现上更加复杂。
- 如果图例要实现单选功能,也可以通过设置element 的 show,hide
- 这种方式存在的很大一个问题:无法同其他的过滤方式整合,例如默认情况下已经有几个类别被过滤,过滤数据不仅仅是图形隐藏,还会牵扯到数据 scale 范围的变化。
- legend 的 hover 时高亮
G2.registerInteraction('legend-category-active', {
init(chart) {
this.chart = chart;
this.initEvents(chart)
},
initEvents(chart) {
chart.on('legend-item:mouseenter', Util.bind(this, 'onEnter'));
chart.on('legend-item:mouseout', Util.bind(this, 'onOut'));
},
onEnter(ev) {
const legendItem = ev.item;
this.setActive(legendItem, true);
},
onOut(ev) {
const legendItem = ev.item;
this.setActive(legendItem, false);
},
setActive(legendItem, active) {
const chart = this.chart;
const field = legendItem.field;
const value = legendItem.value;
const checked = legendItem.checked;
if (checked) { // 只有 checked 的才会 active
const elements = chart.getAllElements();
elements.forEach(el => {
const record = el.getModel().origin; // 还没设计好如何定义
if (record[field] === value) {
element.setState('active', active);
}
});
}
},
destroy() {
this.chart.off('legend-item:mouseenter', Util.bind(this, 'onEnter'));
this.chart.off('legend-item:mouseout', Util.bind(this, 'onOut'));
}
});
- legend item 上的 hover 效果需要 legend 组件自己维护,也需要自己监听 mouseenter,mouseleave 时间,同时在 chart 上 emit 出来。
- element 的 active 再次出现,多个 interaction 都来操作 element 的 active 时肯定会发生冲突
- 图形 active 时高亮对应的 legend 选项
G2.registerInteraction('active-legend', {
init(chart) {
this.chart = chart;
this.initEvents(chart)
},
initEvents(chart) {
chart.on('element:mouseenter', Util.bind(this, 'onEnter'));
chart.on('element:mouseleave', Util.bind(this, 'onOut'));
},
onEnter(ev) {
const element = ev.element;
element.setState('active', true);
this.setLegendActive(element, true);
},
onOut(ev) {
const element = ev.element;
element.setState('active', false);
this.setLegendActive(element, false);
},
setLegendActive(element, active) {
const chart = this.chart;
const record = element.getModel().origin;
const legends = chart.getLegends(); // 有多个图例
legends.forEach(legend => {
// 也应该判定 legend 的类型是否是分类,如果是连续,需要另一种实现
const field = legend.get('field'); // 这里怎么存储 field 可以再设计一下
const items = legend.getItems();
items.forEach(item => {
const value = item.value;
if (record[field] === value) {
item.active = true;
legend.updateItem(item); // 更新图例项
}
});
});
},
destroy() {
this.chart.off('element:mouseenter', Util.bind(this, 'onEnter'));
this.chart.off('element:mouseleave', Util.bind(this, 'onOut'));
}
});
- element 的 active 设置再次出现,而且都是 mouseeneter 和 mouseleave 导致的
legend 的事件最好在 chart 上使用委托的机制绑定,图表的生命周期内会创建和销毁图例
小结
把事件的触发和图表的响应都写到一起的方案,通过上面的几个示例可以看到其优点:
实现简单,仅需要处理绑定事件、响应事件即可
- 易于理解
但是同时我们也发现了这种方案的弊端:
- active、selected 等常见的反馈到处需要设置,逻辑重复,可能会出现冲突的的情况
- 不同 interaction 之间不知道彼此做的事情,很容易出现同时操作单个图形、组件的情况,例如:给折线图写了一个 tooltip 的交互,也给柱状图写了一个,同时触发交互时,仅有一个 tooltip 这时候后出现者会覆盖前面的交互。
这个方案问题的本质其实是复用和耦合的冲突,要解决这个问题需要:
- 相同的行为只有一个入口
- 可以监听其他交互的行为
触发和反馈分开
这种方案的思路是一些反馈不仅仅在一个场景用得到,例如:
- 图表元素的 active 有多种情况会发生,鼠标移动到一个图形上,显示 tooltip 时,hover 到 legend 项上时。
- 图表元素的 selected 可能会是用户点击导致,也可能通过程序设置。
- 过滤在多种情况下发生,而触发过滤和响应过滤本质上时一体的,例如点击图例项会导致图表发生过滤,而其他情况引起过滤后,图例项也需要做出响应。
将这些通用的交互单独实现,其他交互中可以调用的方案有多种,但是一种解耦最好的方案是使用一个通用的状态管理控制器: StateManager
class StateManager extends EventEmiter {
setState(name, value) { // 设置状态,同时触发 'name' + change 的事件
const originValue = this.states[name];
this.states[name] = value;
// 这个地方其实可以做 orginValue 和 value 是否相等的判定
// 可能两者是复杂的对象,判定比较复杂
this.emit(name + 'change', { name: name, value: value, originValue: originValue));
},
getState(name) {
return this.states[name];
}
}
通用的交互 active, selected 等不再自己触发,而是监听状态量的变化,进行相应。
active
G2.registerInteraction('element-active', {
init(chart) {
this.stateManager = chart.getStateManager();
this.initEvents()
},
initEvents(chart) {
const stateManager = this.stateManager;
stateManager.on('activeElementschange', Util.bind(this, 'onChange'));
},
onChange(ev) {
const value = ev.value;
const activedElements = originValue;
// 如果存在之前激活的元素
activedElements && activedElements.forEach(el => {
el.setState('active', false);
});
const elements = value.elements;
// 如果 elements = null 这时候也会清理掉原先 active 的元素,会实现取消所有 active 的效果
// 这是一种互斥的方案,在很多编程中会变得更加简单
elements && elements.forEach(el => {
el.setState('active', true);
});
}
destroy() {
this.stateManager.off('activeElements', Util.bind(this, 'onChange'));
}
});
// 外面需要调用
stateManager.setState('activeElements', [element1, element2]);
// 清理所有的 active
stateManager.setState('activeElements', null);
如果这时候我们想实现 hover 的 active 效果,则需要定义一个新的 interaction
G2.registerInteraction('active', {
init(chart) {
this.chart = chart;
this.stateManager = chart.getStateManager();
},
initEvents(chart) {
chart.on('element:mouseenter', Util.bind(this, 'onEnter'));
chart.on('element:mouseenter', Util.bind(this, 'onEnter'));
},
onEnter(ev) {
const element = ev.element;
this.stateManager.setState('activeElments', [element]);
},
onOut(ev) {
this.stateManager.setState('activeElements', null);
},
destroy() {
this.chart.off('element:mouseenter', Util.bind(this, 'onEnter'));
this.chart.off('element:mouseenter', Util.bind(this, 'onEnter'));
}
});
- 跟原先的代码相比变化不大,但是增加了一个 stateManager 和 状态量的概念,易理解带来困难
- 也可以在业务代码中监听 activeElementschange ,显示被激活元素的信息
highlight
highlight 由于还牵扯到另一种元素的状态 dark ,可以先定义 dark 的交互,其他交互也可以复用
G2.registerInteraction('element-dark', {
init(chart) {
this.stateManager = chart.getStateManager();
this.initEvents()
},
initEvents(chart) {
const stateManager = this.stateManager;
stateManager.on('darkElementschange', Util.bind(this, 'onChange'));
},
onChange(ev) {
const value = ev.value;
const activedElements = originValue;
// 如果存在之前变暗的元素
activedElements && activedElements.forEach(el => {
el.setState('dark', false);
});
const elements = value.elements;
// 如果 elements = null 这时候也会清理掉原先 dark 的元素,会实现取消所有 dark 的效果
// 这是一种互斥的方案,在很多编程中会变得更加简单
elements && elements.forEach(el => {
el.setState('dark', true);
});
}
destroy() {
this.stateManager.off('darkElementschange', Util.bind(this, 'onChange'));
}
});
// 外面需要调用
stateManager.setState('darkElements', [element1, element2]);
// 清理所有的 active
stateManager.setState('darkElements', null);
- 我们会发现 active, dark 的状态完全一样,可以通过一套代码来实现
```javascript
G2.createElementInteraction = function(stateName) {
G2.G2.registerInteraction(‘element-‘ + stateName, {
}); }....
G2.createElementInteraction(‘active’); G2.createElementInteraction(‘dark’);
// and so on
```javascript
G2.registerInteraction('highlight', {
init(chart) {
this.chart = chart;
this.stateManager = chart.getStateManager();
this.initEvents(chart)
},
initEvents(chart) {
chart.on('element:mouseenter', Util.bind(this, 'onEnter'));
chart.on('element:mouseleave', Util.bind(this, 'onOut'));
},
onEnter(ev) {
const element = ev.element;
this.stateManager.setState('activeElements', element);
const allElements = this.chart.getAllElements();
const darkElments = [];
allElements.forEach(el => {
if (el !== element) {
darkElments.push(el);
}
});
this.stateManager.setState('darkElements', darkElments);
},
onOut(ev) {
this.stateManager.setState('darkElements', null);
this.stateManager.setState('activeElements', null);
},
destroy() {
this.chart.off('element:mouseenter', Util.bind(this, 'onEnter'));
this.chart.off('element:mouseleave', Util.bind(this, 'onOut'));
}
});
selected
在前面我们已经定义了 active, dark 的元素状态,这里仅需要定义 selected 状态即可
G2.createElementInteraction('selected'); // 定义新的元素状态量
监听 click 时间,选中元素
G2.registerInteraction('click-selected', {
init(chart) {
this.chart = chart;
this.stateManager = chart.getStateManager();
this.initEvents(chart)
},
initEvents(chart) {
chart.on('element:click', Util.bind(this, 'onClick'));
},
onClick(ev) {
const element = ev.element;
stateManager = this.stateManager;
// 仅考虑单选,如果已经选中,则清理状态
// 如果支持多选,这里复杂一些
if (element.hasState('selected')) {
stateManager.setState('selectedElements', null);
} else {
// 清理原先选中的元素
stateManager.setState('selectedElements', [element]);
}
},
destroy() {
this.chart.off('element:click', Util.bind(this, 'onClick'));
}
});
由于清理逻辑已经在之前的 interaction 中支持,这里的代码会精简很多
tooltip
我们可以将显示 tooltip 这个行为也实现成一个交互,批量调用个单个调用都可以复用这个交互
G2.registerInteraction('show-tooltip', {
init(chart) {
this.stateManager = chart.getStateManager();
this.initEvents()
},
initEvents(chart) {
const stateManager = this.stateManager;
stateManager.on('tooltipElementschange', Util.bind(this, 'onChange'));
},
onChange(ev) {
const elements = ev.value;
const items = [];
elements.forEach(el => {
const model = el.getModel(); // 获取 element 的数据
const originData = model.origin;
items.push({
title: originData.x,
name: 'xxx',
value: 'xxx'
});
});
// 这个接口怎么设计
// tooltip 是否要
this.chart.showTooltip({
x: ev.x,
y: ev.y,
items: items
});
}
destroy() {
this.stateManager.off('tooltipElementschange', Util.bind(this, 'onChange'));
}
});
这里仅仅显示 tooltip 的信息,而不要进行 active 设置
同时显示多个元素的信息
G2.registerInteraction('shared-tooltip', {
init(chart) {
this.chart = chart;
this.stateManager = chart.getStateManager();
this.initEvents(chart)
},
initEvents(chart) {
chart.on('mousemove', Util.bind(this, 'onMove'));
// 移出画布是 tooltip 消失的逻辑也要加
},
onMove(ev) {
const elements = chart.getSnapElements({x: ev.x, y: ev.y}); // 虚拟的方法,获取逼近的元素
this.stateManager.setState('tooltipElements', elements);
this.stateManager.setState('activeElements', elements); // 同时 active 元素
},
destroy() {
this.chart.off('mousemove', Util.bind(this, 'onMove'));
}
});
移动到单个图形上显示 tooltip
G2.registerInteraction('-tooltip', {
init(chart) {
this.chart = chart;
this.stateManager = chart.getStateManager();
this.initEvents(chart)
},
initEvents(chart) {
chart.on('mouseenter', Util.bind(this, 'onEnter'));
chart.on('mouseleave', Util.bind(this, 'onOut'));
},
onEnter(ev) {
const element = ev.element;
this.stateManager.setState('tooltipElements', [element]);
this.stateManager.setState('activeElements', [element]); // 同时 active 元素
},
onOut(ev) {
this.stateManager.setState('tooltipElements', null);
this.stateManager.setState('activeElements', null);
},
destroy() {
this.chart.off('mousemove', Util.bind(this, 'onMove'));
this.chart.off('mouseleave', Util.bind(this, 'onOut'));
}
});
legend
我们在这里同样实现常见的三种交互:
- 一个是在legend 上过滤数据,
- 第二个是在 legend hover 时高亮对应的图形
- 第三个是图形 active 时高亮对应的 legend 选项
- 数据过滤:
首先我们可以定义 filter-element 的交互和定义 filterElements 的状态量
G2.createElementInteraction('filtered');
通过 legend 过滤图形
G2.registerInteraction('legend-category-filter', {
init(chart) {
this.chart = chart;
this.stateManager = chart.getStateManager();
this.initEvents(chart)
},
initEvents(chart) {
chart.on('legend:chenge', Util.bind(this, 'onClick'));
},
onClick(ev) {
const legend = ev.legend;
const field = legend.field;
const items = legend.getItems();
const elements = chart.getAllElements();
const filetedElements = [];
elements.forEach(el => {
const record = el.getModel().origin; // 还没设计好如何定义
items.forEach(item => {
if (!item.checked && element[field] == item.value){
filetedElements.push(element);
}
});
});
this.stateManager.setState('filetedElements', filetedElements);
},
destroy() {
this.chart.off('legend:chenge', Util.bind(this, 'onClick'));
}
});
- 这种写法性能会变差,因为需要遍历所有 Legend 的选项
- legend 的 hover 时高亮
G2.registerInteraction('legend-category-active', {
init(chart) {
this.chart = chart;
this.stateManager = chart.getStateManager();
this.initEvents(chart)
},
initEvents(chart) {
chart.on('legend-item:mouseenter', Util.bind(this, 'onEnter'));
chart.on('legend-item:mouseout', Util.bind(this, 'onOut'));
},
onEnter(ev) {
const legendItem = ev.item;
this.setActive(legendItem, true);
},
onOut(ev) {
const legendItem = ev.item;
this.setActive(legendItem, false);
},
setActive(legendItem, active) {
const chart = this.chart;
const field = legendItem.field;
const value = legendItem.value;
const checked = legendItem.checked;
const activeElements = [];
if (checked) { // 只有 checked 的才会 active
const elements = chart.getAllElements();
elements.forEach(el => {
const record = el.getModel().origin; // 还没设计好如何定义
if (record[field] === value) {
activeElements.push(el);
}
});
}
this.stateManager.setState('activeElements', activeElements);
},
destroy() {
this.chart.off('legend-item:mouseenter', Util.bind(this, 'onEnter'));
this.chart.off('legend-item:mouseout', Util.bind(this, 'onOut'));
}
});
- 这个实现看上去同第一种写法没什么差别,仅仅是屏蔽了如何进行 active
- hover 图表的图形,legend 高亮
小结
将触发和反馈分离可以得到一些好处:
- 常用的反馈可以集中编写,提升代码复用率,反馈的实现方式对上层屏蔽,便于后面修改
- 可以在业务代码中监听反馈,而不管是如何触发的
- 也可以同一个反馈使用不同的交互实现,仅需要按需加载即可
同时也带了一些问题:
- 用户需要更多的知识,需要理解每个状态量的定义(可以是复杂对象)
- 有些交互编写起来更复杂一些
总结
本章把 G2 现在支持的一些交互,按照两种方式分别实现出来,进行了一些对比,但这两种设计仅仅将图表绘制和交互的耦合解开,并不能称得上是图形语法,真正的图形语法会在后面的章节中详细的讲解,让用户定制交互更加轻松简单。