AntV数据可视化
Background
最近做数据可视化的时候用到AntV的S2, G2和L7这三个框架,基于的是typescript+react,这里是一些使用过程中的情况记录。
虽然官方文档看起来比较简陋,但是讲的还是非常详细,用起来效果也是很好的。
L7
官网在这里,开源大规模地理空间数据可视分析引擎,所以出来的图应该都带着地图背景的。地图的引擎可以有多种选择,比如:
Summary
- 给出个
.html
示例,直接单文件打开就能运行
1 |
|
- 一个官方的快速上手教程:https://l7.antv.antgroup.com/tutorial/quickstart
- 官方图表演示:https://l7.antv.antgroup.com/examples
- github源码:https://github.com/antvis/L7
示例学习
事件监听
对PointLayer的实例监听click和unclick事件,设置中心点并添加一个Plane,这个Plane是通过getElementById获取到整个Scene实例,然后appendChild的,纯手写的html;每次事件触发时直接修改innerHTML,就会触发重新渲染
根据某个字段设置区间映射,像size这种数值型才可以区间映射,也可以传一个函数进去
1 | layer.size('mag', [ 1, 25 ]).color('mag', mag => { return mag > 4.5 ? '#5B8FF9' : '#5CCEA1'; }) |
字段映射
https://l7.antv.antgroup.com/zh/examples/point/bubble/#color
根据某个字段(name)不同设置不同点的形状or颜色,通过直接调用PointLayer实例的
.shape
和.color
方法1
2layer.shape('name', ['circle', 'triangle', 'square'])
layer.color('name', ['#5B8FF9', '#5CCEA1', '#5D7092', '#F6BD16', '#E86452'])
气泡图动画
https://l7.antv.antgroup.com/zh/examples/point/bubble/#scatter
- active为true的话就是鼠标移动上去会highlight,也可以设置颜色
1
2layer.active(true)
layer.active({ color: 'gray' });- 动画为true,也可以设置参数
1
2layer.animate(true)
layer.animate({ duration: 4, interval: 0.2, trailLength: 0.1, });
标注在点上
https://l7.antv.antgroup.com/zh/examples/point/cluster/#cluster2
用的就是
.shape
,见官方1
layer.shape('name', 'text');
时序变化(数据不断更新)
https://l7.antv.antgroup.com/tutorial/quickstart#%E6%97%B6%E5%BA%8F%E5%8F%98%E5%8C%96%E5%9B%BE
直接对着layer示例调用setData方法就行,参数和source的参数一致,api文档见这里
可以一开始设置数据为空:
layer.source([])
,然后把layer的setData方法暴露出去,设置完成后自动更新渲染注意如果数据流是
Array<Array<DataType>>
而非Array<Map<string, DataType>>
,需要来一步转换,似乎是不支持直接拿索引作为键,但是G2和S2倒是支持的,这里parser的x和y就对应long和lat,是死的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16geoLayer.setData(
// use index as key cannot work properly
data.map((dataArray) =>
header.reduce((dict, item, index) => {
dict[item.name] = dataArray[index];
return dict;
}, {})
),
{
parser: {
type: 'json',
x: header[longIndex].name,
y: header[latIndex].name,
},
}
);可以使用
React.useEffect()
监听图表选项或数据,如果有变化就更改,当然图表选项(如size, color等)不setData也可以更新渲染
复合图表
Popup信息窗
-
- 可以参照官方给的示例,直接new一个Popup,然后分别设置位置(setLnglat)和内容(setHTML),这个回调参数
e
包含当前所在点的数据
1
2
3
4
5
6
7
8
9layer.on('mousemove', e => {
const popup = new Popup({
offsets: [ 0, 0 ],
closeButton: false
})
.setLnglat(e.lngLat)
.setHTML(`<span>地区: ${e.feature.properties.name}</span><br><span>确诊数: ${e.feature.properties.case}</span>`);
scene.addPopup(popup);
});但是这个示例有点问题,只监听layer的mousemove,所以鼠标不聚焦在那些点上的时候就没反应(我们知道,点都在layer上,layer被加到scene中)
我们可以在外边搞一个
const popupRef = useRef(null)
,然后把new的Popup赋值给popupRef,监听scene的click事件将其从scene中去掉,或者监听layer的mouseout事件。- 图层事件:https://l7.antv.antgroup.com/api/base_layer/base#%E9%BC%A0%E6%A0%87%E4%BA%8B%E4%BB%B6
- 地图事件:https://l7.antv.antgroup.com/api/scene#%E5%9C%BA%E6%99%AF%E4%BA%8B%E4%BB%B6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31layer.on('mousemove', (e) => {
const feature = e.feature;
const popup = new Popup({
offsets: [0, 0],
closeButton: false,
autoClose: true,
}).setLnglat(e.lngLat).setHTML(`
<div style="height: 100%; width: 100%; overflow: auto; background-color: black">
<table>
<tbody>
${Object.entries(feature)
.map(([key, value]) => {
if (headerKeys.includes(key)) {
return `<tr><td>${key}</td><td>${value}</td></tr>`;
}
})
.join('')}
</tbody>
</table>
</div>
`);
scene.addPopup(popup);
popupRef.current = popup;
});
scene.on('click', (e) => {
if (popupRef.current) {
scene.removePopup(popupRef.current);
popupRef.current = null;
}
});
- 可以参照官方给的示例,直接new一个Popup,然后分别设置位置(setLnglat)和内容(setHTML),这个回调参数
图例
https://l7.antv.antgroup.com/tutorial/quickstart#%E6%B7%BB%E5%8A%A0%E5%9B%BE%E4%BE%8B
- L7提供默认Zoom、Scale等组件(https://github.com/antvis/L7/tree/master/packages/component/src/control),它们都是基于Control组件构建的,组件可以在任意时候被添加进入Scene实例,这里在React中有一种写法,在后续会介绍到
- 组件另一个关键点就是和地图的交互,比如Zoom、Center,当地图缩放或平移时就需要更改参数。我们可以对组件进行事件监听,如zoomend或moveend,当缩放或移动结束时,手动获取Scene实例的zoom和center,再重新渲染Control的内容,这里放一个我写的组件,这个React组件返回空,只用到useEffect。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24export const CenterDisplay = ({ onChange }) => {
const geoScene = useContext(SceneContext);
useEffect(() => {
const centerLabel = `<span style="display: inline;">Center: </span>`;
if (geoScene && !geoScene.getControlByName('center')) {
const centerControl = new Control({ position: 'bottomright', name: 'center' });
centerControl.onAdd = function () {
const centerDiv = document.createElement('div');
centerDiv.style.paddingInline = '8px';
const center = geoScene.getCenter();
centerDiv.innerHTML = `${centerLabel}(${center.lat.toFixed(4)}, ${center.lng.toFixed(4)})`;
return centerDiv;
};
geoScene.addControl(centerControl);
geoScene.on('moveend', () => {
const center = geoScene.getCenter();
onChange('center', [center.lng, center.lat]);
centerControl.getContainer().innerHTML = `${centerLabel}(${center.lat.toFixed(4)}, ${center.lng.toFixed(4)})`;
});
}
}, [geoScene, onChange]);
return <></>;
};
React结构
React非常基于组件,首先我们定义一个GeoCore,它可能有很多参数,它返回值的核心是一个div
,div
的ref是通过自定义useGeo获取到的L7实例
1 | const SceneContext = createContext<Scene>(null); |
在useGeo中需要传出来Scene实例、整个L7实例引用和popup的容器。具体地,先抽取出来一个createGeoScene函数,这个返回Scene实例,自定义center和zoom,并设置好ref。这个函数在测试的时候会被整体mock,因为测试环境的webgl问题无法运行L7。
1 | export const createGeoScene = (geoChartRef, defaultCenterRef, defaultZoomRef) => { |
然后定义useGeo:
- 这里定义的geoChartRef是直接给到Scene的id的,作为唯一标识(这里的ref具体值是多少应该无所谓,就是实例的唯一标识,要传给
div
的ref属性) - 设置pop,在mousemove时set,在unmousemove时变null,同时搞了个popRef给这个useState的pop,保证其实例唯一,并且传出去这个唯一实例。传出去的popContent包含当前鼠标位置气泡的信息,用于在外部显示。
1 | export const useGeo = (defaultCenter: [number, number], defaultZoom: number) => { |
关于图例,在GeoCore同级定义一个SceneContext,在外部传给GeoCore对应的图例组件,然后放在它的children中,在图例组件中再获取Scene实例即可,具体可见我上述CenterDisplay
。
单元测试
关于在测试环境构建L7实例,需要对webgl进行mock,按照我的理解,webgl是构建在canvas之上的一个3D渲染引擎。我们可以通过创建一个canvas,然后获取它的context来得到webgl对象,这是一个通过jest去mock canvas的包:https://github.com/hustcc/jest-canvas-mock
1 | const canvas = document.createElement('canvas'); |
通过查看L7官方的测试方案create-context.ts,我们发现他们通过gl这个包,构建了一个context并且将这个context传给了regl的构造函数,在把regl实例给了Scene的gl参数。这里regl是webgl的一种简化版。
这种方法是可行的,但是gl这个包的安装需要系统的一些环境,在我们的github action的workflow中安装失败,于是我尝试删掉gl这个包。
其实本质是找到一个webgl的context,我找到github上一份代码:https://github.com/googlemaps/js-three/blob/main/src/__tests__/__utils__/createWebGlContext.ts,它通过jest-webgl-canvas-mock这个基于jest-canvas-mock的包,并自定义mock gl的很多函数,从而实现成功运行。
由于测试环境的Scene和真实环境不同,因此需要mock上述的createGeoScene函数。
由于我们使用的是vitest,因此使用vitest-canvas-mock在全局将jest替换为vi,即在jest-webgl-canvas-mock中用的jest其实都是vitest。
纪念一下我对着regl.js
这个6k多行的文件研究它的报错捣鼓了一下午。
- geo.test.tsx
1 | import { createWebGlContext } from './createWebGlContext'; |
- createWebGlContext.tsx
1 | // @ts-ignore |
S2
在我使用S2做表格的时候,v2还没有推出来,但是现在v2已经进入内测阶段,详细见这里
目前S2用得比较浅,等待v2进入正式版后会再深入使用,到时候记录的内容也会更多一些
Summary
依然是给出一个直接能运行的.html
示例,
1 |
|
基础知识
透视表
摘录自官网:在统计学中,透视表是矩阵格式的一种表格,显示多变量频率分布。它们提供了两个变量(或者多个)之间的相互关系的基本画面,可以帮助发现它们之间的相互作用,帮助业务进行交叉探索分析,是目前商业 BI
分析领域中使用频率最高的图表之一。
总结来说,透视表的数据就类似某个值+它所属的一堆类别,类别不同导致值不同,因此维度可以很高。数据就是类似如下组成的列表:
1 | { |
但是如上的值需要满足一定约束。这个在fields中体现:
1 | "fields": { |
既然columns字段是先type再sub_type,那首先会把所有type显示全,然后在各个type下面继续显示其包含的sub_type。所以一般情况下需要遵守每个sub_type只能出现在一个type下,比如桌子不能既是家具又是办公用品,否则就会从左图变成右图:
明细表
明细表比较简单,就是普通的表格,在列头下把每行数据直接展示出来就行。这玩意儿基于canvas
渲染,可以替换基于 DOM
的表格组件,大幅提升性能。它的fields字段的columns都是并列的
基础配置
S2DataConfig
数据实际上就是个JSON的东西,各个字段有不同的作用。
1 | const s2DataConfig = { |
看官方文档吧,讲得非常清楚:https://s2-v1.antv.antgroup.com/api/general/s2-data-config#meta
这里如果data是Array of Array,而非Array of object的话,即数据都是[[]]
的形式而非[{}]
,这时候我们可以把columns字段设置['1', '2', ...]
,把meta字段设置成如下:
1 | meta: [ |
S2Options
各种图表选项,可以自定义样式、添加组件、交互设置、工具栏等等,这个可以实时进行更新,可以在useEffect中通过table.setOptions()
操作。
S2Theme
主要想说一下theme,S2的很多theme配置项默认都是打开的,需要你手动去关了,这里列出一个深色主题的配置,各个字段的意思在官网都有说明:https://s2-v1.antv.antgroup.com/api/general/s2-theme
比如splitLine就是分割线,我都设置成宽度为0;很多color都设置成transparent
比如colCell, dataCell就是列头和数据单元格的样式设置。
1 | export const s2Theme = { |
React和Vue组件
React和Vue都提供开箱即用的组件,不用再自己去挂载在div上。
React
- 提供
@antv/s2-react
,里面有<SheetComponent />
,见官网:https://s2-v1.antv.antgroup.com/api/components/sheet-component
1 | import React from 'react'; |
当然,如果你信不过这个组件,可以自己创建实例然后挂载。
透视表(PivotSheet
)和明细表(TableSheet
)都继承于基础类SpreadSheet
,它有很多方法,见官网API文档
比如我会用到过的:
创建完实例,挂载在
div
之前设置主题(setThemeCfg和setTheme)、设置数据(setDataCfg)、设置选项(setOptions)等等设置图表自适应,这个在官网也有提到:https://s2-v1.antv.antgroup.com/manual/advanced/adaptive
本质就是使用监听窗口
ResizeObserver
类,它的回调函数的参数entries
是包含所有被监视元素的变化信息的数组(所以如果没有监视多个的话,entries
只包含一个元素)。监视之后,在每次需要自适应时,添加一个节流函数,去更新窗口大小。
这里给出一个React中使用S2的完整例子:
1 | // 钩子函数 |
1 | // 组件 |
Vue
感觉S2对React支持比Vue多一些
https://s2-v1.antv.antgroup.com/manual/getting-started#vue3-%E7%89%88%E6%9C%AC
G2
有空再补吧