react大屏画图处理方案记录

645 阅读7分钟

一、背景介绍:

最近一个月在画各种需求的图,不得不说,当前市场可以参考的库众多,但是api说明文档,却没有那么细致,总之查找起来并不方便,于是记录一下,最近画的图所遇到的各种坑,希望可以对大家有所帮助。

二、组件文档速查:

先简单介绍一下基本图中的各个组件的名称:

截屏2022-04-16 下午4.01.20.png

详情可以参考:点这里

三、选择组件库

由于公司项目是react开发,于是可以选择的组件库依次为

四、复杂图案例

(1)带气泡的中国地图:

(最初以为可以用ant design charts中的choropleth组件来实现一个地图,完成区域渲染后再次自定义一个标注,后面发现不可以,后面又想通过ant design charts中 dot组件来实现一个气泡地图,但是做不到 每个国家区域的颜色不同,同时也做不到,最终在L7Plot高级图表中找到了实例)

截屏2022-04-16 下午4.24.07.png

  • ⚠注意:自定义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查看

  1. 官方文档世界地图 拖拽 永远在可视范围之内
  2. 官方文档中国地图 拖拽 会被拖出可视范围,于是 采用 将加载完成地图 监听, 复杂图表是由俩层地图合成,于是先通过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

是否保留缓冲区数据。

截屏2022-04-16 下午4.24.17.png 完整详情代码如下:

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 中展示。这样可以避免出现如图问题

截屏2022-06-12 14.44.37.png

截屏2022-04-16 下午4.42.24.png

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转圈圈