React App - Map Chart

背景

之前系统中有关地理信息的图表是直接使用国家/省市名称, 通过柱状图来展示地区数据的区别. 在重构的时候想优化一下这块的展示, 直接使用中国地图/世界地图来展示数据的分布, 我们使用的图表组件是highcharts, 看了一下虽然自带map组件, 但是有个比较严重的问题, 中国地图不标准, 所以我们需要自己生成底图数据, 然后自己渲染.

又研究了一下其他的组件, 发现antv也支持地图绘制, 而且对于自定义地图支持得比较完善, 功能和性能也都比highcharts好. 在支付宝看疫情分布时候发现了和这里demo一模一样效果的地图(感觉就是直接使用这个组件生成的), 支持的地图数据是geojson也比较常见, 所以最终使用antv实现地图渲染.

地图渲染

首先按官方文档, 实现地图组件渲染, 然后获取数据后将数据按图层加载到组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (!antvL7Scene.current) {
antvL7Scene.current = new Scene(
currentShowItem.type === 'china-map'
? {
id: chartRef.current,
map: new Mapbox({
pitch: 0,
style: 'blank',
center: [107.042225, 37.66565],
zoom: 2.5,
}),
}
: {
id: chartRef.current,
map: new Mapbox({
pitch: 0,
style: 'blank',
center: [0, 40],
zoom: 0.5,
}),
},
);
}

生成图层数据, 颜色是根据数据量大小由浅入深的, 这里注意下需要对Sea就是海的部分的事件不要处理

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
const genAntvL7Options = (
result: ESResponse,
indicators: DashboardItemChart['indicators'],
dimensions: DashboardItemChart['dimensions'],
type: typeof CONSTANT.DASHBOARD_CHART_TYPE_CONFIG[number]['type'],
antvL7MapData: any,
colorOptions: string[],
): { baseLayer: ILayer; lineLayer: ILayer; tipsPopup: IPopup; dataSource: Record<string, number | string>[] } => {
const firstDimension = dimensions[0];

const indicator = indicators[0];

const aggsMapData: Record<string, number> = {};
const dataSource: Record<string, number | string>[] = [];

// 数据处理部分省略

let maxNum = 0;
let minNum = 0;
_.map(antvL7MapData.features, (feature) => {
const areaValue = aggsMapData[feature.properties.name];
if (areaValue) {
_.set(feature.properties, 'value', areaValue);
maxNum = maxNum > areaValue ? maxNum : areaValue;
minNum = minNum < areaValue ? minNum : areaValue;
} else if (feature.properties.name) {
_.set(feature.properties, 'value', 0);
}
});
maxNum = maxNum + 1;
const stepNum = (maxNum - minNum) / (colorOptions.length - 1);

const tipsPopup = new Popup({
offsets: [0, 0],
closeButton: false,
closeOnClick: false,
});
const baseLayer = new PolygonLayer({})
.scale('value', {
type: 'quantile',
})
.source(antvL7MapData)
.color('value', (value) => {
if (value === 0) {
return colorOptions[0];
}
for (let i = 0; i < colorOptions.length - 1; i++) {
if (value >= minNum + stepNum * i && value < minNum + stepNum * (i + 1)) {
return colorOptions[i + 1];
}
}
})
.shape('fill')
.style({
opacity: 1,
});
baseLayer.on('mousemove', (e) => {
if (e.feature.properties.name === 'Sea') {
return;
}
tipsPopup.setLnglat(e.lngLat).setHTML(`<span>${e.feature.properties.name}: ${e.feature.properties.value} (${indicator})</span>`);
tipsPopup.open();
});
baseLayer.on('mouseout', (e) => {
tipsPopup.close();
});

const lineLayer = new LineLayer({
zIndex: 2,
})
.source(antvL7MapData)
.color('#000')
.size(0.3)
.style({
opacity: 1,
});

return { baseLayer, lineLayer, tipsPopup, dataSource };
};

将生成的图层添加到地图中

1
2
3
4
5
6
7
8
if (antvL7Scene.current.getLayers().length > 0) {
antvL7Scene.current.removeAllLayer();
}
const layerResult = await parseAntvL7Layer(item.dataCache.chartOptions);
antvL7Scene.current.addLayer(layerResult.baseLayer);
antvL7Scene.current.addLayer(layerResult.lineLayer);
antvL7Scene.current.addPopup(layerResult.tipsPopup);
item.dataCache.dataSource = layerResult.dataSource;

数据处理

其实这里才是最重要的, 上面代码中的antvL7MapData就是这一步得到的. antv支持的底图数据是geojson格式, 也提供了获取方式, 可以在http://datav.aliyun.com/tools/atlas/index.html获取到高德地图开放的地图数据. 这里下载到的数据细致到了市, 目前的需求是只需要省的数据, 所以将数据处理一下即可, 然后在highcharts能下载到完整的全球地图数据, 再将里面的china部分替换成我们的数据即可. 将这两份数据存为两份本地json, 在代码加载的时候动态引入, 可以减少代码体积(虽然写完之后看起来简单, 但是寻找资料和构造这份数据的过程才是最耗时的).

1
2
3
4
5
6
const parse = {
parseAntvL7Layer: async (result) => {
const antvL7MapData = _.cloneDeep((await import('../../../../static/map/china.geo.json')).default);
return genAntvL7Options(result, indicators, dimensions, 'china-map', antvL7MapData, ANTV_MAP_COLORS_CHINA);
},
};
作者

Mosby

发布于

2020-04-22

许可协议

评论