一、背景介绍:
最近一个月在画各种需求的图,不得不说,当前市场可以参考的库众多,但是api说明文档,却没有那么细致,总之查找起来并不方便,于是记录一下,最近画的图所遇到的各种坑,希望可以对大家有所帮助。
二、组件文档速查:
先简单介绍一下基本图中的各个组件的名称:
详情可以参考:点这里
三、选择组件库
由于公司项目是react开发,于是可以选择的组件库依次为
四、复杂图案例
(1)带气泡的中国地图:
(最初以为可以用ant design charts中的choropleth组件来实现一个地图,完成区域渲染后再次自定义一个标注,后面发现不可以,后面又想通过ant design charts中 dot组件来实现一个气泡地图,但是做不到 每个国家区域的颜色不同,同时也做不到,最终在L7Plot高级图表中找到了实例)
- ⚠注意:自定义Tooltip的设置,要判断items[0]?.value?是否存在,因为有些地区鼠标悬浮在地图上 ,省份名字返回的是undefined(例如某些群岛)
const domTooltip = (items, data) => {
const item = data.filter((d) => d.name === items[0]?.value);
const da = item[0] || {};
const nameDom = `<div class =${styles.row}>
<div class=${styles.mapName}>${items[0]?.value}</div>
</div>`;
const resultNull = '<div></div>';
return items[0]?.value?resultDom:resultNull;
};
完整详情代码如下:
配置文件:
const domTooltip = (items, data) => {
const item = data.filter((d) => d.name === items[0]?.value);
const da = item[0] || {};
const nameDom = `<div class =${styles.row}>
<div class=${styles.mapName}>${items[0]?.value}</div>
</div>`;
const resultNull = '<div></div>';
return items[0]?.value?resultDom:resultNull;
};
export const chinaConfig:any = (data) => {
const config = {
map: {
type: 'mapbox',
style: 'blank',
center: [120.19382669582967, 30.258134],
zoom: 3,
minZoom: 2,
pitch: 0,
},
logo: false,
plots: [
{
type: 'choropleth',
zIndex: 1,
source: {
data: data,
joinBy: {
sourceField: 'name',
geoField: 'name',
},
},
viewLevel: {
level: 'country',
adcode: '100000',
},
chinaBorder: false,
autoFit: true,
style: {
opacity: 1,
stroke: '#F2F7F7',
lineWidth: 0.6,
lineOpacity: 0.8,
},
label: {
visible: true,
field: 'name',
style: {
fill: '#000',
opacity: 0.8,
fontSize: 10,
stroke: '#f0f0f0',
strokeWidth: 2,
textAllowOverlap: false,
padding: [5, 5],
textOffset: [0, 40],
},
},
color: {
field: 'value',
value: ['#B8E1FF', '#7DAAFF', '#3D76DD', '#0047A5', '#001D70'],
scale: {
type: 'quantize',
},
},
tooltip: {
// offsets: [0, 100],
// anchor: 'center',
items: ['name', 'adcode', 'value'],
customContent(e, items) {
return domTooltip(items, data);
},
},
},
{
type: 'dot',
zIndex: 2,
source: {
data: data,
parser: {type: 'json', x: 'lng', y: 'lat'},
},
color: '#1AA9FF',
size: 10,
style: {
opacity: 1,
stroke: '#fff',
strokeWidth: 1,
},
tooltip: {
// offsets: [0, 100],
// anchor: 'center',
items: ['name', 'adcode', 'value'],
customContent(e, items) {
return domTooltip(items, data);
},
},
},
],
layers: [],
zoom: {
position: 'bottomright',
},
};
return config;
};
react.jsx文件
const ProvinceData = [
{
name: '云南省',
value: 17881.12,
lng: 102.709372,
lat: 25.046432,
top: '1',
}
];
import {L7Plot} from '@antv/l7plot';
const [chinaConfigChart] = useState(chinaConfig(ProvinceData));
new L7Plot('china', chinaConfigChart);
<div id="china" style={{width: '100%', height: '400px'}}></div>
(2)世界地图配置:气泡改为自定义图标
⚠注意:color:#fff,否则不能实现。
问题1:自定义图标保存的引入方式。
方式1:
import {registerImages} from '@antv/l7plot';
const images = [
{id: '01', image: require('assets/images/DataCenter/location.png')},
];
registerImages(images);
方式2:
import {registerImages} from '@antv/l7plot';
const images = [
{id: '01', image: 'https://gw.alipayobjects.com/zos/basement_prod/604b5e7f-309e-40db-b95b-4fac746c5153.svg'}
];
registerImages(images);
⚠注意:首次加载地图 图标图层可能会渲染层级错误?
方式1:不建议 可能发生渲染层级错误
{
type: 'dot',
zIndex: 2,
source: {
data: data,
parser: {type: 'json', x: 'lng', y: 'lat'},
},
color: '#fff',
shape: {
field: 's',
value: ({s}) => {
return s ==='01' ? '01': '02';
},
},
size: 10,
style: {
opacity: 1,
stroke: '#fff',
strokeWidth: 1,
},
},
方式2:建议采用 不发生渲染层级错误
{
type: 'dot',
zIndex: 2,
source: {
data: data,
parser: {type: 'json', x: 'lng', y: 'lat'},
},
color: '#fff',
shape: {
field: 'top5',
value: ['01'],
},
size: 10,
},
问题2:如何禁止鼠标缩放地图?
先查看L7Plot官方文档,找不到内容,再查看底图 底图mapbox Api查看
采用 将加载完成地图 监听, 复杂图表是由俩层地图合成,于是先通过mapPlot,getPlots()获取所有图表,getPlots()返回一个数组,第一个是 choropleth,第二个是 dot ,再通过getMap()获取图表的实例,禁止 鼠标缩放地图。
方法1: 不建议
mapChina.current.on('loaded', () => {
const [mapPlot] = mapChina.current.getPlots();
if (mapPlot) {
mapPlot.getMap().scrollZoom.disable();
}
方法2:建议
配置中添加
{
map: {
type: 'mapbox',
scrollZoom: false,
}
}
问题3:如何设置地图拖拽在可视范围之内?
先查看L7Plot官方文档,找不到内容,底图mapbox Api查看
- 官方文档世界地图 拖拽 永远在可视范围之内
- 官方文档中国地图 拖拽 会被拖出可视范围,于是 采用 将加载完成地图 监听, 复杂图表是由俩层地图合成,于是先通过mapPlot,getPlots()获取所有图表,getPlots()返回一个数组,第一个是 choropleth,第二个是 dot ,再通过getMap()获取图表的实例,设置边界范围:经纬度
mapChina.current.on('loaded', () => {
const [mapPlot] = mapChina.current.getPlots();
if (mapPlot) {
方式1;不建议 底图api
mapPlot.getMap().setMaxBounds([ [50, 3.52], // [west, south]
[150, 53.33] // [east, north]
]);
方式2:不建议官网提供设置地图缩放范围。 多次切换后失效
// mapPlot.fitBounds([[50, 3.52], [150, 53.33]]);
}
});
方法3:建议
配置中添加
{
map: {
type: 'mapbox',
dragPan: false,
}
}
问题4:如何只更新数据 ,不更新配置?
当实例不存在时,new L7Plot,创建地图实例,实例存在时,用 changeData 只更新数据
// dataConfig 为数据+配置
// data 只数据
if (!mapChina.current) { // 新建地图实例
mapChina.current = new L7Plot(mapChinaContainer.current, dataConfig);
} else { // 只更新地图数据
mapChina.current.getPlots().forEach((plot) => plot.changeData(data));
}
问题5:如何设置气泡不超出地图边界范围呢?
handleMousemove(mapChina.current, 'china'); // 鼠标悬浮 地图气泡定位
具体数值要进行调整和计算
// 地图气泡永远展示在可视区域;
const handleMousemove = (plot, mapType) => {
plot.on('mousemove', (event) => {
const options = plot.plots[0].tooltip.options;
if (mapType=='world') {
// 世界气泡定位
event.point.x > 457 ?
(options.offsets[0] = -100) :
(options.offsets[0] = 200);
event.point.y > 146 ?
(options.offsets[1] = 40) :
(options.offsets[1] = -100);
} else {
// 中国气泡定位
event.point.x > 457 ?
(options.offsets[0] = -120) :
(options.offsets[0] = 120);
event.point.y > 146 ?
(options.offsets[1] = 60) :
(options.offsets[1] = -90);
}
});
};
问题6:当世界地图和中国地图来回切换时,暂无数据时,卸载组件时如何销毁组件?
⚠注意:因为暂无数据时,不渲染地图,渲染 Empty组件。 所以也要销毁组件,重置实例,否则,渲染模块,可能出现白屏。
useEffect(() => {
return () => {
if (mapChina.current) {
mapChina.current.destroy();
mapChina.current = null;
}
if (mapGloal.current) {
mapGloal.current.destroy();
mapGloal.current = null;
}
};
}, []);
// 展示地图为空,消毁实例,展示空数据
useEffect(() => {
if (mapChina.current&&mapChinaData.length==0) {
mapChina.current.destroy();
mapChina.current = null;
}
if (mapGloal.current&&mapWorldData.length==0) {
mapGloal.current.destroy();
mapGloal.current = null;
}
}, [mapChinaData, mapWorldData]);
// 切换卡片销毁实例
useUpdateEffect(() => {
if (tabKey === '1') {
if (mapChina.current) {
mapChina.current.destroy();
mapChina.current = null;
}
} else {
if (mapGloal.current) {
mapGloal.current.destroy();
mapGloal.current = null;
}
}
}, [tabKey]);
问题7:下载地图时,要在地图添加上如下属性,否则 下载出的pdf 无地图踪影?
preserveDrawingBuffer:true
是否保留缓冲区数据。
完整详情代码如下:
import {registerImages} from '@antv/l7plot';
const images = [
{id: '01', image: 'https://gw.alipayobjects.com/zos/basement_prod/604b5e7f-309e-40db-b95b-4fac746c5153.svg'},
{id: '02', image: 'https://gw.alipayobjects.com/zos/basement_prod/30580bc9-506f-4438-8c1a-744e082054ec.svg'},
{id: '03', image: 'https://gw.alipayobjects.com/zos/basement_prod/7aa1f460-9f9f-499f-afdf-13424aa26bbf.svg'},
];
registerImages(images);
export const globalConfig:any = (data) => {
console.log('data!!!!', data);
const config = {
map: {
type: 'mapbox',
style: 'blank',
center: [120.19382669582967, 30.258134],
zoom: 3,
pitch: 0,
},
logo: false,
plots: [
{
type: 'choropleth',
zIndex: 1,
source: {
data: data,
joinBy: {
sourceField: 'name',
geoField: 'name',
},
},
viewLevel: {
level: 'world',
adcode: 'all',
},
autoFit: true,
color: {
field: 'value',
value: ['#B8E1FF', '#7DAAFF', '#3D76DD', '#0047A5', '#001D70'],
scale: {
type: 'quantize',
},
},
style: {
opacity: 1,
stroke: '#bdbdbd',
lineWidth: 0.6,
lineOpacity: 0.8,
},
chinaBorder: false,
label: {
visible: true,
field: 'name',
style: {
fill: '#000',
opacity: 0.8,
fontSize: 10,
stroke: '#fff',
strokeWidth: 2,
textAllowOverlap: false,
padding: [5, 5],
textOffset: [0, 60],
},
},
tooltip: {
items: ['name'],
customContent(e, items) {
return domTooltip(items, data);
},
},
},
{
type: 'dot',
zIndex: 2,
source: {
data: data,
parser: {type: 'json', x: 'lng', y: 'lat'},
},
color: '#fff',
shape: {
field: 's',
value: ({s}) => {
return s ==='01' ? '01': '02';
},
},
size: 10,
style: {
opacity: 1,
stroke: '#fff',
strokeWidth: 1,
},
tooltip: {
items: ['name'],
customContent(e, items) {
return domTooltip(items, data);
},
},
},
],
layers: [],
zoom: {
position: 'bottomright',
},
};
return config;
};
react.jsx
import {L7Plot} from '@antv/l7plot';
const [globalConfigChart] = useState(globalConfig(globalData));
new L7Plot('world', globalConfigChart);
<div id="china" style={{width: '100%', height: '400px'}}></div>
问题8:外部网络使用不了,如何在内部网络下访问行政数据资源?
//外网:
{
geoArea: {
url: 'https://gw.alipayobjects.com/os/alisis/geo-data-v0.1.2/choropleth-data',
type: 'topojson',
},
}
//内网:
{
geoArea: {
url: 'https:公司内部资源地址',
type: 'topojson',
},
}
// 备注:将地图需要json 均打包下载 上传到 cdn
(3)玉珏图
⚠注意:官方文档圆角 是从中间开始渲染,都渲染完成,再次渲染两边圆角,用户体检效果不佳,于是建议修改属性,变成方形,会避免渲染看起来是抖动的问题。
barStyle: { lineCap: 'round', }, // 去掉这个配置
⚠注意:图例主要目的 就是各种符号和颜色所代表内容与指标的说明,因此 不建议 图例上 再添加其他数据展示,其他数据可以放入 气泡Tooltip 中展示。这样可以避免出现如图问题
import {RadialBar} from '@ant-design/plots';
export const applicationConfig = (data, titleHtml, sum)=>{
const config = {
data,
xField: 'name',
yField: 'num',
// seriesField: 'num',
maxAngle: 350,
// 最大旋转角度,
radius: 0.8,
innerRadius: 0.2,
legend: {
position: 'top',
maxRow: 2,
itemValue: {
formatter: (text, item) => {
const items = data.filter((d) => d.name === item.value);
const da = items[0] || {};
return `${titleHtml} ${da.num} ${da.percentage}`;
},
},
},
tooltip: {
showMarkers: false,
enterable: true,
domStyles: {
'g2-tooltip': {
width: '200px',
padding: '0px',
},
},
customContent: (title, items) => {
console.log('items', items);
const data = items[0]?.data || {};
const nameDom = `<div class=${styles.between}>
<div class=${styles.p_10}>应用名:</div>
<div class=${styles.p_10}>${data.name}</div>
</div>`;
const titleDom = `<div class =${styles.between}>
<div class=${styles.p_10}>数据类型:</div>
<div class=${styles.p_10}>${data.title}</div>
</div>`;
const numDom = `<div class = ${styles.between}>
<div class=${styles.p_10}>数量:</div>
<div class=${styles.p_10}>${data.num}</div>
</div>`;
const percentDom = `<div class = ${styles.between}>
<div class=${styles.p_10}>占比%:</div>
<div class=${styles.p_10}>${data.percentage}</div>
</div>`;
return `<div class=${styles.bg}>${nameDom}${titleDom}${numDom}${percentDom}</div>`;
},
},
colorField: 'name',
color: ({name}) => {
return data.find((d) => d.name === name).color;
},
// barBackground: {},
barStyle: {
lineCap: 'round',
},
annotations: [
{
type: 'html',
position: ['50%', '50%'],
// content: 'Music',
style: {
textAlign: 'center',
fontSize: 24,
},
html: (container, view) => {
return `<div style="transform:translate(-50%,-46%)">
<div style="font-size:24px;text-align:center">${titleHtml}</div>
<div style="font-size:20px;text-align:center">${sum}</div>
</div>`;
},
},
],
};
return config;
};
const [applicationConfigChart] = useState(applicationConfig(data, 'Reject', '111'));
<div style={{height: '462px'}}>
<RadialBar {...applicationConfigChart}/>
</div>
最后 所有图表都需要对他进行 useMemo包裹,添加loading转圈圈