背景

S2 是 AntV 在多维交叉分析表格领域的解决方案,主要用于看数分析, S2 采用 canvas
来进行表格绘制 (基于 易用、高效、强大的 2D 可视化渲染引擎 G ) , 同时内置大量的交互能力来辅助用户看数, 如 行列联动高亮
单选/多选高亮
刷选高亮
行高列宽动态调整
列头隐藏
等, 同时还支持 自定义交互
, 本文主要介绍 S2 是如何实现这些交互的。
DOM 交互和 Canvas 交互的区别
以单元格点击为例, 得益于强大的 CSS3
选择器, 我们可以准确的监听任意 dom 元素的点击事件
<ul class="cell"> <li id="cell1">我是第一个单元格</li> <li id="cell2">我是第二个单元格</li> </ul>
const cell = document.querySelector('.cell > li:first-child'); cell.addEventListener('click', () => { console.log('第一个单元格: 别点我!'); })
但是 canvas 就只有一个 <canvas/>
dom 元素
<canvas />
如何准确的知道点击的是哪个单元格呢? 答案是 事件委托
+ 鼠标坐标
const canvas = document.querySelector('canvas'); canvas.addEventListener('click', () => { console.log('我点的是哪个单元格?'); })
在 dom 中, 有一个很经典的事件冒泡应用场景, 那就是 事件委托
, 还是以上面的例子, 我们可以只监听父级的 ul
元素, 根据当前的 event.target
来判断当前点击的是哪一个单元格
const cell = document.querySelector('.cell'); cell.addEventListener('click', (event) => { const CELL_ID = 'cell1' if (event.target?.id === CELL_ID) { console.log('我是第一个单元格'); } });
所以在 canvas
中, 我们也可以依葫芦画瓢, 不同点是, 单元格不再是一个个的 dom 节点, 而是一个个 canvas 图形 对应的数据结构, 类似于虚拟dom

const cell = new Shape({ type: 'rect' })
public getCell<T extends S2CellType = S2CellType>(event): T { let parent = event.target; // 判断当前 target 属于哪一个实例 while (parent && !(parent instanceof Canvas)) { if (parent instanceof BaseCell) { // 在单元格中,返回true return parent as T; } parent = parent.get?.('parent'); } return null; } // antv/g 提供的 Canvas 构造器 const canvas = new Canvas() canvas.on('click', (event) => { const cell = this.getCell(event) })
事件分类
通过事件委托, 能够获取到具体触发事件的单元格 ( 具体实现 )
- 角头单元格点击:
S2Event.CORNER_CELL_CLICK
- 列头单元格点击:
S2Event.COL_CELL_CLICK
- 行头单元格点击:
S2Event.ROW_CELL_CLICK
- 数据单元格点击:
S2Event.DATA_CELL_CLICK
- 单元格双击
- 单元格右键
- ...

在监听到对应事件后, 通过内部的 event emitter
分发出去, 从而触发对应的单元格事件
private onCanvasMousedown = (event: CanvasEvent) => { const cellType = this.spreadsheet.getCellType(event.target); switch (cellType) { case CellTypes.DATA_CELL: this.spreadsheet.emit(S2Event.DATA_CELL_MOUSE_DOWN, event); break; case CellTypes.ROW_CELL: this.spreadsheet.emit(S2Event.ROW_CELL_MOUSE_DOWN, event); break; case CellTypes.COL_CELL: this.spreadsheet.emit(S2Event.COL_CELL_MOUSE_DOWN, event); break; case CellTypes.CORNER_CELL: this.spreadsheet.emit(S2Event.CORNER_CELL_MOUSE_DOWN, event); break; case CellTypes.MERGED_CELL: this.spreadsheet.emit(S2Event.MERGED_CELLS_MOUSE_DOWN, event); break; default: break; } };
this.spreadsheet.on(S2event.DATA_CELL_MOUSE_DOWN, (event) => { console.log('数值单元格点击') })
交互分类
有了分好类的单元格事件, 我们就可以将其排列组合。 比如刷选高亮, 就对应 数值单元格的 mousedown
+ mousemove
+ mouseup
事件, 再将获取到的单元格 meta 信息存储在状态机, 最后根据交互状态进行 canvas 重绘
交互类型 |
名称 |
适用场景 |
全选 |
ALL_SELECTED |
复制 |
选中 |
SELECTED |
单选/多选/行列批量选中 |
未选中 |
UNSELECTED |