背景
S2 是 AntV 在多维交叉分析表格领域的解决方案,主要用于看数分析,S2 采用 canvas
来进行表格绘制 (基于 易用、高效、强大的 2D 可视化渲染引擎 G ) , 同时内置大量的 交互能力 来辅助用户看数,如 行列联动高亮
单选/多选高亮
刷选高亮
行高列宽动态调整
列头隐藏
等,同时还支持 自定义交互
, 本文主要介绍 S2 是如何实现这些交互的。
DOM 交互和 Canvas 交互的区别
以单元格点击为例,得益于强大的 CSS3
选择器,我们可以准确的监听任意 dom 元素的点击事件
1 | <ul class="cell"> |
1 | const cell = document.querySelector('.cell > li:first-child'); |
但是 canvas 就只有一个 <canvas/>
dom 元素
1 | <canvas /> |
如何准确的知道点击的是哪个单元格呢?答案是 事件委托
+ 鼠标坐标
1 | const canvas = document.querySelector('canvas'); |
在 dom 中,有一个很经典的事件冒泡应用场景,那就是 事件委托
, 还是以上面的例子,我们可以只监听父级的 ul
元素,根据当前的 event.target
来判断当前点击的是哪一个单元格
1 | const cell = document.querySelector('.cell'); |
所以在 canvas
中,我们也可以依葫芦画瓢,不同点是,单元格不再是一个个的 dom 节点,而是一个个 canvas 图形 对应的数据结构,类似于虚拟 dom
1 | const cell = new Shape({ type: 'rect' }); |
1 |
|
事件分类
通过事件委托,能够获取到具体触发事件的单元格 ( 具体实现 )
- 角头单元格点击:
S2Event.CORNER_CELL_CLICK
- 列头单元格点击:
S2Event.COL_CELL_CLICK
- 行头单元格点击:
S2Event.ROW_CELL_CLICK
- 数据单元格点击:
S2Event.DATA_CELL_CLICK
- 单元格双击
- 单元格右键
- …
在监听到对应事件后,通过内部的 event emitter
分发出去,从而触发对应的单元格事件
1 | private onCanvasMousedown = (event: CanvasEvent) => { |
1 | this.spreadsheet.on(S2event.DATA_CELL_MOUSE_DOWN, (event) => { |
交互分类
有了分好类的单元格事件,我们就可以将其排列组合。 比如刷选高亮,就对应 数值单元格的 mousedown
+ mousemove
+ mouseup
事件,再将获取到的单元格 meta 信息存储在状态机,最后根据交互状态进行 canvas 重绘
交互类型 | 名称 | 适用场景 |
---|---|---|
全选 | ALL_SELECTED | 复制 |
选中 | SELECTED | 单选/多选/行列批量选中 |
未选中 | UNSELECTED | 点击空白处,ESC 键重置,偶数次点击单元格 |
悬停 | HOVER | 行列联动高亮 |
长时间悬停 | HOVER_FOCUS | 显示 tooltip |
预选中 | PREPARE_SELECT | 刷选 |
单选高亮
在线体验
鼠标左键单击单元格后,会高亮当前单元格,聚焦当前的数据。
在实现上,其实并没有对当前选中单元格做高亮操作,而是置灰其他所有非选中状态的数值单元格,就像一种 聚光灯
效果。
通过 cell.getMeta()
拿到渲染时闭包保存的当前单元格信息,然后调用 interaction.changeState
改变当前交互状态,将状态改为 InteractionStateName.SELECTED
1 | this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event: CanvasEvent) => { |
最后的 state 为:
1 | const cell = { |
接下来就是获取到当前可视范围内所有的数值单元格,对它们进行更新
1 |
|
每一个单元格实例会有一个 update
方法,最终会根据当前的状态 改变单元格背景色透明度 fillOpacity
1 | // 简化代码 |
行列联动高亮
当鼠标 hover 在数值单元格上时,会同时高亮对应的行头和列头,也就是 十字高亮效果
, 便于用户清晰的知道对应关系,实现上首先和单选一样,先改变状态为 InteractionStateName.HOVER
然后绘制当前单元格的黑色边框
1 | this.spreadsheet.on(S2Event.DATA_CELL_HOVER, (event: CanvasEvent) => { |
先绘制数值单元格区域的十字高亮,比较当前单元格和 state 存储的 rowIndex
/ colIndex
是否一致,如果有一个相同就表示处于同一列/行,对其进行高亮
1 | const currentColIndex = this.meta.colIndex; |
1 | cell.attrs = { |
接下来是行头和列头,处理有些许不同,由于透视表行头和列头是多维嵌套的,有父子级关系,不能单纯的比较行/列索引,需要额外比较 单元格 id
如图,行头我们需要高亮 浙江省/舟山市
列头需要高亮 家具/沙发/数量
, 内部对应存储的 id 为
浙江省/舟山市
=>root[&] 浙江省 [&] 舟山市
家具/沙发/数量
=>root[&] 家具 [&] 沙发 [&]number
所以 浙江省/舟山市
和 家具/沙发/数量
对应的834
数值单元格的 id 为 => root[&] 浙江省 [&] 舟山市-root[&] 家具 [&] 沙发 [&]number
, 最后去看行/列头单元格 id 是否为包含关系,高亮即可
1 | const allRowHeaderCells = getActiveHoverRowColCells( |
刷选高亮
刷选用于对批量单元格数据汇总,本质是一种拖拽的动作,拖拽结束后,需要选中拖拽起始坐标点对角线矩形区域的所有单元格。
刷选过程中,还需要考虑鼠标已经超过表格区域,此时默认认为用户还想继续刷选可视范围外的单元格 (如有), 也就是滚动刷选,这个在 使用 AntV S2 打造大数据表格组件 已有相关介绍。这里就不再赘述。
刷选和其他交互不同,会有一个 预选中
状态,如图,会有一个蓝色的预选中蓝色蒙层,并且该区域单元格显示黑色边框,表示松开鼠标后,这些单元格会被选中,用于给用户一个提示
首先在点击单元格时记录一个刷选起始点,包含 x/y
坐标,rowIndex/colIndex
行/列索引等信息
1 | private getBrushPoint(event: CanvasEvent): BrushPoint { |
然后在刷选结束,鼠标松开后,得到一个完整的刷选信息,最后比较当前单元格是否在这个范围即可
1 | return { |
1 | private isInBrushRange(meta: ViewMeta) { |
将获取到单元格信息,存储在 state, 然后重绘
1 | this.spreadsheet.on(S2Event.GLOBAL_MOUSE_UP, (event) => { |
行高列高动态调整
S2 默认提供 列等宽布局
行列等宽布局
和 紧凑布局
三种布局方式 (预览), 也可以拖拽行/列头进行动态调整,要实现这种效果,首先需要绘制调整的热区,也就是如下图这个蓝色的小条,默认情况下是隐藏的,只有在鼠标放在单元格边缘才会显示出来 (还可以 自定义热区范围 )
细心的同学可能发现了,鼠标放在热区上面,会变成这样一个图标,这个比较有趣,在 CSS
中 我们可以给任意元素添加 cursor: col-resize
来实现,在 Canvas
中 由于只有 canvas
一个 dom 标签,我们则需要判断 hover
热区时,给 canvas
加上 cursor: col-resize
行内样式,实现同样的效果
如果把热区全部显示出来,展示的效果如下:
平铺模式:
树状模式:
明细表:
接下来需要绘制辅助线,和刷选类似,刷选需要显示预选中的遮罩,动态调整需要显示两条辅助线来让用户预览调整之后的单元格宽度
两条线,对应两条 path
, 虚线使用 lineDash
实现
1 | const attrs: ShapeAttrs = { |
在拖动过程中,需要实时更新参考线的位置,需要考虑水平和垂直两种情况,起始点为单元格的底部,结束点为表格区域的底部
1 | if (type === ResizeDirectionType.Horizontal) { |
这里大写的 M
和 L
熟悉 SVG
的同学应该清楚,大写表示绝对定位,小写表示相对定位,对应的含义如下:
1 | M = moveto 移动到 |
在拖拽完成后,将最新的单元格高度/宽度保存到 s2Options.style 中,重绘更新后,单元格按照最新的大小渲染即可
1 | private getResizeWidthDetail(): ResizeDetail { |
链接跳转
在线体验
可以给指定单元格的文字加上下划线,表示可以点击跳转
如果使用 DOM
实现,只需要给对应元素加上 a
超链接标签即可,使用 Canvas
实现,则需要自己绘制 下划线
, 监听点击事件。来模拟 a
标签的效果,核心实现如下
1 | // 获取当前文字的包围盒 |
列头隐藏
透视表和明细表都支持隐藏列头,首先点击列头,显示 tooltip, 然后点击 tooltip 的 隐藏
按钮,同时支持批量/分组隐藏
首先需要知道当前隐藏的列是否需要分组,如果给定的隐藏列不是连续的,比如原始列是 [1,2,3,4,5,6,7]
, 隐藏列是 [2,3,6]
, 那么其实在表格上需要显示两个展开按钮 [[2,3],[6]]
, 核心代码如下
1 | export const getHiddenColumnsThunkGroup = ( |
接下来是生成分组信息
1 | const detail = { |
有了这些数据,就能知道展开按钮绘制在哪一个单元格上,展开按钮默认显示在后一个兄弟节点,首尾单元格被隐藏的情况例外,需要反过来
除了手动点击进行隐藏,S2 还支持通过声明配置默认隐藏,用于去掉一些不重要数据的干扰,提升看数效率
1 | const s2DataConfig = { |
对于明细表,一个 field
就只对应一个列头,对于透视表,一个 field
对应一个或多个列头,只指定 field
的话并不知道需要隐藏哪个列头,需要指定对应列头的 id
1 | const s2Options = { |
列头隐藏后,对应的就是展开,展开相对来说就比较简单了,将当前隐藏列配置和展开的列头做一次 diff
, 移除相应配置即可
1 | private handleExpandIconClick(node: Node) { |
最后我们根据这些配置信息,重新构建布局,渲染隐藏/展开列头后的表格即可
自定义交互
在线体验
除了上面提到的丰富的内置交互以外,开发者还可以根据 S2 提供的 事件S2Event
, 自由排列组合,自定义表格交互,可通过 interaction.customInteractions
注册,比如自定义一个 行列头 hover 显示 tooltip
的交互
1 | import { PivotSheet, BaseEvent, S2Event } from '@antv/s2'; |
结语
以上就是对于 S2 部分交互实现的一些介绍,除此之外,S2 还支持 合并单元格, 自定义滚动速度 等丰富的交互,篇幅有限,就不一一列举了。
也欢迎社区的同学和我们一起共建 AntV/S2,打造最强的开源大数据表格引擎。如果看完这篇文章你有所收获,欢迎给我们的 仓库 Star⭐️ 鼓励。
S2 的相关链接:
- GitHub
- 官网
- 核心层:@antv/s2
- 组件层:@antv/s2-react