Background

最近做数据可视化的时候用到AntV的S2, G2和L7这三个框架,基于的是typescript+react,这里是一些使用过程中的情况记录。
虽然官方文档看起来比较简陋,但是讲的还是非常详细,用起来效果也是很好的。

L7

官网在这里开源大规模地理空间数据可视分析引擎,所以出来的图应该都带着地图背景的。地图的引擎可以有多种选择,比如:

  1. 高德地图(得注册开发者账号),详细见这里
  2. MapBox(需要MapBox Access Tokens),这里有个官方示例
  3. 其他暂时没研究

Summary

  • 给出个.html示例,直接单文件打开就能运行
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>L7</title>
<style> ::-webkit-scrollbar{display:none;}html,body{overflow:hidden;margin:0;}
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<div id="map"></div>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.css' rel='stylesheet' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.js'></script>
<script src="https://unpkg.com/@antv/l7"></script>
<script>
const scene = new L7.Scene({
id: 'map',
map: new L7.Mapbox({
pitch: 20,
style: 'light',
center: [120, 20],
zoom: 3
})
});
scene.on('loaded', () => {
fetch('https://gw.alipayobjects.com/os/basement_prod/d3564b06-670f-46ea-8edb-842f7010a7c6.json')
.then(res => res.json())
.then(data => {
const pointLayer = new L7.PointLayer({})
.source(data)
.shape('simple')
.size(15)
.color('mag', mag => {
return mag > 4.5 ? '#5B8FF9' : '#5CCEA1';
})
.active(true)
.style({
opacity: 0.6,
strokeWidth: 3
});
scene.addLayer(pointLayer);
});
});
</script>
</body>
</html>

示例学习

事件监听

  1. 对PointLayer的实例监听click和unclick事件,设置中心点并添加一个Plane,这个Plane是通过getElementById获取到整个Scene实例,然后appendChild的,纯手写的html;每次事件触发时直接修改innerHTML,就会触发重新渲染

  2. 根据某个字段设置区间映射,像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
    2
    layer.shape('name', ['circle', 'triangle', 'square'])
    layer.color('name', ['#5B8FF9', '#5CCEA1', '#5D7092', '#F6BD16', '#E86452'])

气泡图动画

  • https://l7.antv.antgroup.com/zh/examples/point/bubble/#scatter

    1. active为true的话就是鼠标移动上去会highlight,也可以设置颜色
    1
    2
    layer.active(true)
    layer.active({ color: 'gray' });
    1. 动画为true,也可以设置参数
    1
    2
    layer.animate(true)
    layer.animate({ duration: 4, interval: 0.2, trailLength: 0.1, });

标注在点上

时序变化(数据不断更新)

  • https://l7.antv.antgroup.com/tutorial/quickstart#%E6%97%B6%E5%BA%8F%E5%8F%98%E5%8C%96%E5%9B%BE

    1. 直接对着layer示例调用setData方法就行,参数和source的参数一致,api文档见这里

    2. 可以一开始设置数据为空:layer.source([]),然后把layer的setData方法暴露出去,设置完成后自动更新渲染

    3. 注意如果数据流是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
      16
      geoLayer.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,
      },
      }
      );
    4. 可以使用React.useEffect()监听图表选项或数据,如果有变化就更改,当然图表选项(如size, color等)不setData也可以更新渲染

复合图表

Popup信息窗

  • https://l7.antv.antgroup.com/tutorial/quickstart#%E6%B7%BB%E5%8A%A0%E4%BF%A1%E6%81%AF-popup-%E4%BF%A1%E6%81%AF%E7%AA%97

    1. 可以参照官方给的示例,直接new一个Popup,然后分别设置位置(setLnglat)和内容(setHTML),这个回调参数e包含当前所在点的数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    layer.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);
    });
    1. 但是这个示例有点问题,只监听layer的mousemove,所以鼠标不聚焦在那些点上的时候就没反应(我们知道,点都在layer上,layer被加到scene中)

    2. 我们可以在外边搞一个const popupRef = useRef(null),然后把new的Popup赋值给popupRef,监听scene的click事件将其从scene中去掉,或者监听layer的mouseout事件。

      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
      31
      layer.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;
      }
      });

图例

  • https://l7.antv.antgroup.com/tutorial/quickstart#%E6%B7%BB%E5%8A%A0%E5%9B%BE%E4%BE%8B

    1. L7提供默认Zoom、Scale等组件(https://github.com/antvis/L7/tree/master/packages/component/src/control),它们都是基于Control组件构建的,组件可以在任意时候被添加进入Scene实例,这里在React中有一种写法,在后续会介绍到
    2. 组件另一个关键点就是和地图的交互,比如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
    24
    export 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,它可能有很多参数,它返回值的核心是一个divdiv的ref是通过自定义useGeo获取到的L7实例

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
31
32
33
34
35
36
37
38
39
40
41
const SceneContext = createContext<Scene>(null);
export function GeoCore(props: GeoProps & { children: JSX.Element }) {
...
const { geoScene, ref, popContent } = useGeo(center, zoom);
// 用一些useEffect,当Props和Data更改时,及时更改样式和setData
return (
<>
<SceneContext.Provider value={geoScene}>
<div ref={ref} />
{children} // 这是可能出现的图例,会利用geoScene
<Grid
pointerEvents='none'
visibility={popContent ? 'visible' : 'hidden'}
position='absolute'
top={`2rem`}
left={`4rem`}
w='30%'
gridTemplateColumns='auto 1fr'
p='2'
gap='2'
overflow='hidden'
borderColor='tp.gray.500'
border='1px solid'
borderRadius='md'
color='tp.gray.100'
fontSize='xs'
>
{popContent.map(([key, v]) => (
// 这是自定义的一个popup
<Fragment key={key}>
<Box>{key}</Box>
<Box overflow='hidden' w='full' whiteSpace='nowrap'>
{v}
</Box>
</Fragment>
))}
</Grid>
</SceneContext.Provider>
</>
);
}

在useGeo中需要传出来Scene实例、整个L7实例引用和popup的容器。具体地,先抽取出来一个createGeoScene函数,这个返回Scene实例,自定义center和zoom,并设置好ref。这个函数在测试的时候会被整体mock,因为测试环境的webgl问题无法运行L7。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const createGeoScene = (geoChartRef, defaultCenterRef, defaultZoomRef) => {
return new Scene({
id: geoChartRef.current,
map: new Mapbox({
style: 'dark',
rotateEnable: false,
token: 'pk.eyJ1Ijoic2tvcm5vdXMiLCJhIjoiY2s4dDBkNjY1MG13ZTNzcWEyZDYycGkzMyJ9.tjfwvJ8G_VDmXoClOyxufg',
center: defaultCenterRef.current,
zoom: defaultZoomRef.current,
minZoom: 1,
}),
logoVisible: false,
});
};

然后定义useGeo:

  1. 这里定义的geoChartRef是直接给到Scene的id的,作为唯一标识(这里的ref具体值是多少应该无所谓,就是实例的唯一标识,要传给div的ref属性)
  2. 设置pop,在mousemove时set,在unmousemove时变null,同时搞了个popRef给这个useState的pop,保证其实例唯一,并且传出去这个唯一实例。传出去的popContent包含当前鼠标位置气泡的信息,用于在外部显示。
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
31
32
33
34
35
36
37
38
39
40
export const useGeo = (defaultCenter: [number, number], defaultZoom: number) => {
const [geoScene, setGeoScene] = React.useState<Scene | undefined>();
const geoChartRef = React.useRef(null);
const [pop, setPop] = React.useState(null);
const popRef = React.useRef(pop);

const defaultCenterRef = React.useRef(defaultCenter);
const defaultZoomRef = React.useRef(defaultZoom);

React.useEffect(() => {
const scene = createGeoScene(geoChartRef, defaultCenterRef, defaultZoomRef);
scene.on('loaded', () => {
const layer = new PointLayer({
name: 'geoLayer',
})
.source([])
.shape('circle')
.active({ color: 'gray' }); // highlight

layer.on('mousemove', (e) => {
setPop(e);
popRef.current = e;
});
layer.on('unmousemove', () => {
if (popRef.current !== null) {
setPop(null);
popRef.current = null;
}
});
scene.addLayer(layer);
setGeoScene(scene);
});

return () => {
setGeoScene(undefined);
scene.destroy();
};
}, [setPop]);
return { geoScene, ref: geoChartRef, popContent: pop };
};

关于图例,在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
2
3
4
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('webgl');
expect(() => ctx.arc('10', '10', '20', '0', '6.14')).not.toThrow();
expect(() => ctx.arc(1, 2, 3, 4)).toThrow(TypeError);

通过查看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
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
31
32
33
34
35
36
37
38
39
40
41
42
import { createWebGlContext } from './createWebGlContext';

// @ts-ignore
HTMLCanvasElement.prototype.getContext = vi.fn();

vi.mock('../xxx/utils', async () => {
const axios = await vi.importActual('../xxx/utils');
return {
...axios,
createGeoScene: (geoChartRef, defaultCenterRef, defaultZoomRef) => {
// mock this function for test
const el = document.createElement('div');
el.id = 'test-div-id';
const body = document.querySelector('body') as HTMLBodyElement;
body.appendChild(el);
const scene = new Scene({
id: el,
gl: regl({
gl: createWebGlContext(),
attributes: {
alpha: true,
antialias: true,
premultipliedAlpha: true,
preserveDrawingBuffer: false,
stencil: true,
},
extensions: ['OES_element_index_uint'],
}),
map: new Map({
style: 'dark',
rotateEnable: false,
token: 'xxx',
center: defaultCenterRef.current,
zoom: defaultZoomRef.current,
minZoom: 1,
}),
logoVisible: false,
});
return scene;
},
};
});
  • createWebGlContext.tsx
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// @ts-ignore
import registerWebglMock from 'jest-webgl-canvas-mock/lib/window.js';
import { vi } from 'vitest';

// Creates a mocked WebGL 1.0 context based on the one provided by the jest-webgl-canvas-mock package.
export function createWebGlContext() {
registerWebglMock(window);

const gl = new WebGLRenderingContext();
const glParameters: Record<number, unknown> = {
[gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS]: 8,
[gl.VERSION]: 'WebGL 1.0 (OpenGL ES 2.0 Chromium)',
[gl.SCISSOR_BOX]: [0, 0, 100, 100],
[gl.VIEWPORT]: [0, 0, 100, 100],
[gl.COMPRESSED_TEXTURE_FORMATS]: new Uint32Array(0),
[gl.MAX_TEXTURE_SIZE]: 16384,
[gl.MAX_RENDERBUFFER_SIZE]: 16384,
[gl.ALIASED_LINE_WIDTH_RANGE]: new Float32Array([1, 8]),
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const glExtensions: Record<string, any> = {
EXT_blend_minmax: {},
oes_element_index_uint: {},
};

vi.spyOn(gl, 'getContextAttributes').mockReturnValue({});
vi.spyOn(gl, 'getParameter').mockImplementation((key) => {
return glParameters[key];
});

vi.spyOn(gl, 'getError').mockImplementation(() => 0);

const getExtensionOrig = gl.getExtension;
vi.spyOn(gl, 'getExtension').mockImplementation((id) => {
return glExtensions[id] || getExtensionOrig(id);
});

vi.spyOn(gl, 'isContextLost').mockImplementation(() => false);
vi.spyOn(gl, 'checkFramebufferStatus').mockImplementation(() => gl.FRAMEBUFFER_COMPLETE);

const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 100;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(gl as any).canvas = canvas;

return gl;
}

S2

在我使用S2做表格的时候,v2还没有推出来,但是现在v2已经进入内测阶段,详细见这里

目前S2用得比较浅,等待v2进入正式版后会再深入使用,到时候记录的内容也会更多一些

Summary

依然是给出一个直接能运行的.html示例,

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>S2</title>
<style> ::-webkit-scrollbar{display:none;}html,body{overflow:hidden;margin:0;}
#container { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<div id="container"></div>
<script src="https://unpkg.com/@antv/s2@latest/dist/index.min.js"></script>
<script>
fetch('https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json')
.then((res) => res.json())
.then((dataCfg) => {
const container = document.getElementById('container');

const s2Options = {
width: 600,
height: 480,
};
const s2 = new S2.PivotSheet(container, dataCfg, s2Options);
s2.render();
});
</script>
</body>
</html>

基础知识

表格形态有两种,分别是透视表明细表

透视表

摘录自官网:在统计学中,透视表是矩阵格式的一种表格,显示多变量频率分布。它们提供了两个变量(或者多个)之间的相互关系的基本画面,可以帮助发现它们之间的相互作用,帮助业务进行交叉探索分析,是目前商业 BI 分析领域中使用频率最高的图表之一。

总结来说,透视表的数据就类似某个值+它所属的一堆类别,类别不同导致值不同,因此维度可以很高。数据就是类似如下组成的列表:

1
2
3
4
5
6
7
{
"number": 7789,
"province": "浙江省",
"city": "杭州市",
"type": "家具",
"sub_type": "桌子"
},

但是如上的值需要满足一定约束。这个在fields中体现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"fields": {
"rows": [
"province",
"city"
],
"columns": [
"type",
"sub_type"
],
"values": [
"number"
],
"valueInCols": true
},

既然columns字段是先type再sub_type,那首先会把所有type显示全,然后在各个type下面继续显示其包含的sub_type。所以一般情况下需要遵守每个sub_type只能出现在一个type下,比如桌子不能既是家具又是办公用品,否则就会从左图变成右图:

image-20240205205654405

明细表

明细表比较简单,就是普通的表格,在列头下把每行数据直接展示出来就行。这玩意儿基于canvas渲染,可以替换基于 DOM 的表格组件,大幅提升性能。它的fields字段的columns都是并列的

基础配置

S2DataConfig

数据实际上就是个JSON的东西,各个字段有不同的作用。

1
2
3
4
5
6
7
8
9
10
const s2DataConfig = {
data: [],
meta: [],
sortParams: [],
fields: {
rows: [],
columns: [],
values: []
}
}

看官方文档吧,讲得非常清楚:https://s2-v1.antv.antgroup.com/api/general/s2-data-config#meta

这里如果data是Array of Array,而非Array of object的话,即数据都是[[]]的形式而非[{}],这时候我们可以把columns字段设置['1', '2', ...],把meta字段设置成如下:

1
2
3
4
5
6
meta: [
{
field: '1',
name: 'realname'
}
]

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
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
export const s2Theme = {
background: {
color: colors.tp.gray[700],
},
splitLine: {
horizontalBorderWidth: 0,
verticalBorderWidth: 0,
},
colCell: {
cell: {
backgroundColor: colors.tp.gray[600],
horizontalBorderColor: 'transparent',
verticalBorderColor: 'transparent',
interactionState: {
hover: {
backgroundColor: 'transparent',
},
selected: {
backgroundColor: 'transparent',
},
},
},
text: {
fill: colors.tp.gray[200],
textAlign: 'left' as TextAlign,
},
bolderText: {
fill: colors.tp.gray[200],
textAlign: 'left' as TextAlign,
},
},
dataCell: {
cell: {
interactionState: {
hover: {
backgroundColor: 'transparent',
},
hoverFocus: {
borderColor: 'transparent',
backgroundColor: 'transparent',
},
selected: {
backgroundColor: 'transparent',
},
unselected: {
backgroundOpacity: 1,
opacity: 1,
},
prepareSelect: {
borderColor: 'transparent',
opacity: 0,
},
},
horizontalBorderColor: colors.tp.gray[600],
verticalBorderColor: colors.tp.gray[600],
backgroundColor: colors.tp.gray[700],
crossBackgroundColor: colors.tp.gray[700],
},
text: {
fill: colors.tp.gray[100],
textAlign: 'left' as TextAlign,
},
},
};

React和Vue组件

React和Vue都提供开箱即用的组件,不用再自己去挂载在div上。

React

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import ReactDOM from 'react-dom';
import { SheetComponent } from '@antv/s2-react';
import '@antv/s2-react/dist/style.min.css';

ReactDOM.render(
<SheetComponent
dataCfg={s2DataConfig}
options={s2Options}
/>,
document.getElementById('container'),
);

当然,如果你信不过这个组件,可以自己创建实例然后挂载。

透视表(PivotSheet)和明细表(TableSheet)都继承于基础类SpreadSheet,它有很多方法,见官网API文档

比如我会用到过的:

  1. 创建完实例,挂载在div之前设置主题(setThemeCfg和setTheme)、设置数据(setDataCfg)、设置选项(setOptions)等等

  2. 设置图表自适应,这个在官网也有提到:https://s2-v1.antv.antgroup.com/manual/advanced/adaptive

    本质就是使用监听窗口ResizeObserver类,它的回调函数的参数entries是包含所有被监视元素的变化信息的数组(所以如果没有监视多个的话,entries只包含一个元素)。

    监视之后,在每次需要自适应时,添加一个节流函数,去更新窗口大小。

这里给出一个React中使用S2的完整例子:

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
// 钩子函数
export const useTable = () => {
const [table, setTable] = React.useState<TableSheet | undefined>();
const tableRef = React.useRef(null);
React.useEffect(() => {
const _table = new TableSheet(tableRef.current!, { data: [], fields: {} }, s2Options);
_table.setTheme(s2Theme);
const debounceRender = debounce((width, height) => {
if (width === 0 || height === 0) return;
_table.changeSheetSize(width, height);
_table.render(false);
}, 200);
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { width, height } = entry.contentRect;
debounceRender(width, height);
});
});
resizeObserver.observe(tableRef.current!);
setTable(_table);
return () => {
setTable(undefined);
resizeObserver.disconnect();
_table.destroy();
};
}, []);
return { table, tableRef };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 组件
export const S2TableCore: React.FC<{
indexes: number[];
header: { name: string; type: string }[];
data: unknown[][];
}> = ({ header, indexes, data }) => {
const { table, tableRef } = useTable();
React.useEffect(() => {
if (table) {
table.setDataCfg({
fields: { columns: header.map((_, i) => `${i}`) },
meta: header.map(({ name }, i) => ({
field: `${i}`, name: name,
})), data,
});
table.render();
}
}, [table, header, data, indexes]);
return <Box w='full' h='full' ref={tableRef}></Box>;
};

Vue

G2

有空再补吧