使用 React 和 ECharts 创建地球模拟扩散和飞线效果

在本博客中,我们将学习如何使用 React 和 ECharts 创建一个酷炫的地球模拟扩散效果。我们将使用 ECharts 作为可视化库,以及 React 来构建我们的应用。地球贴图在文章的结尾。

最终效果

3d地球最终效果

准备工作

首先,确保你已经安装了 React,并创建了一个新的 React 应用。如果你还没有安装 React,可以使用以下命令:

npx create-react-app earth-echarts-demo

然后进入项目目录:

cd earth-echarts-demo

接下来,我们需要安装 ECharts:

npm install echarts --save
npm install echarts-gl --save

创建index.d.ts文件

在typescript中,为了使用不包含typescript类型的库,需要创建一个types文件夹,在下面创建一个index.d.ts文件,然后引入需要使用的库。

  • index.d.ts
declare module 'echarts-gl/components';
declare module 'lodash';

创建 EChartOption 类型

// 声明ECharts的数据类型
import * as echarts from 'echarts/core';
import {
  DatasetComponentOption,
  DataZoomComponentOption,
  GridComponentOption,
  LegendComponentOption,
  TitleComponentOption,
  ToolboxComponentOption,
  TooltipComponentOption,
  GeoComponentOption
} from 'echarts/components';
import {
  BarSeriesOption,
  LineSeriesOption,
  PieSeriesOption,
  GaugeSeriesOption,
  ScatterSeriesOption
} from 'echarts/charts';

export type EChartOption = echarts.ComposeOption<
  | DatasetComponentOption
  | DataZoomComponentOption
  | GridComponentOption
  | LegendComponentOption
  | TitleComponentOption
  | ToolboxComponentOption
  | TooltipComponentOption
  | LineSeriesOption
  | BarSeriesOption
  | PieSeriesOption
  | GaugeSeriesOption
  | GeoComponentOption
  | ScatterSeriesOption
>;

创建 CommonEcahrt 组件

import React, {
  ForwardedRef,
  useEffect,
  useImperativeHandle,
  useRef
} from 'react';
import * as echarts from 'echarts/core';
import { EChartsType } from 'echarts/core';
import {
  DatasetComponent,
  DataZoomComponent,
  GeoComponent,
  GraphicComponent,
  GridComponent,
  LegendComponent,
  MarkPointComponent,
  PolarComponent,
  TitleComponent,
  ToolboxComponent,
  TooltipComponent
} from 'echarts/components';
import {
  BarChart,
  EffectScatterChart,
  GaugeChart,
  LineChart,
  MapChart,
  PieChart,
  RadarChart,
  ScatterChart
} from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { ECElementEvent } from 'echarts/types/src/util/types';
import { EChartOption } from '../EChartOption';
import { CircularProgress } from '@mui/material';
import { GlobeComponent } from 'echarts-gl/components';
import _ from 'lodash';

// 注册 ECharts 组件和图表类型
echarts.use([
  BarChart,
  CanvasRenderer,
  DatasetComponent,
  DataZoomComponent,
  EffectScatterChart,
  GaugeChart,
  GlobeComponent,
  GeoComponent,
  GraphicComponent,
  GridComponent,
  LegendComponent,
  LineChart,
  MapChart,
  MarkPointComponent,
  PieChart,
  PolarComponent,
  RadarChart,
  ScatterChart,
  TitleComponent,
  TooltipComponent,
  ToolboxComponent,
  UniversalTransition
]);

// 定义组件属性和方法
export interface CommonChartProps {
  option: EChartOption | null | undefined;
  width?: number | string;
  height?: number | string;
  merge?: boolean;
  loading?: boolean;
  empty?: React.ReactElement;

  onClick?(event: ECElementEvent): any;
}

export interface CommonChartRef {
  instance(): EChartsType | undefined;
}

// 定义组件内部实现
const CommonChartInner: React.ForwardRefRenderFunction<
  CommonChartRef,
  CommonChartProps
> = (
  { option, width, height, loading = false, onClick },
  ref: ForwardedRef<CommonChartRef>
) => {
  const chartRef = useRef<HTMLDivElement>(null); // 保存 DOM 节点
  const chartInstanceRef = useRef<EChartsType>(); // 保存 ECharts 实例
  const resizeObserverRef = useRef<any>(null); // 保存 ResizeObserver 实例

  // 初始化注册组件,监听 chartRef 和 option 变化
  useEffect(() => {
    if (chartRef.current) {
      // 校验 Dom 节点上是否已经挂载了 ECharts 实例,只有未挂载时才初始化
      chartInstanceRef.current = echarts.getInstanceByDom(chartRef.current);
      if (!chartInstanceRef.current && chartInstanceRef) {
        // eslint-disable-next-line no-undefined
        chartInstanceRef.current = echarts.init(chartRef.current);

        // 监听点击事件并触发回调函数
        chartInstanceRef.current.on('click', (event) => {
          const ec = event as ECElementEvent;

          if (ec && onClick) {
            onClick(ec);
          }
        });
      }

      // 设置配置项
      if (!_.isEmpty(option) && option) {
        chartInstanceRef.current?.setOption(option, false, true);
      }
    }
    return () => {
      chartInstanceRef.current?.setOption({}); // 清空配置项
      //如果将清空配置放在这里,那么每次option变化就会注销
      // 导致视图的数据如果有变化的话,页面会经常重新加载,所以还是只在初始化的时候注销
      // chartInstanceRef.current?.dispose();
    };
  }, [chartRef, option]);

  // 重新适配大小并开启过渡动画
  const resize = () => {
    if (chartInstanceRef) {
      chartInstanceRef.current?.resize({
        animation: { duration: 300 }
      });
    }
  };

  // 监听窗口大小变化重绘
  useEffect(() => {
    window.addEventListener('resize', resize);
    return () => {
      window.removeEventListener('resize', resize);
    };
  }, [option]);

  // 监听高度变化
  useEffect(() => {
    if (chartRef?.current) {
      resize();
    }
  }, [width, height]);

  // 监听父元素高度变化
  useEffect(() => {
    resizeObserverRef.current = new ResizeObserver(() => {
      resize();
    });
    resizeObserverRef?.current.observe(chartRef?.current as any);

    // 取消所有被 ResizeObserver 对象监听的节点
    return () => {
      resizeObserverRef?.current?.disconnect();
    };
  }, []);

  // 获取实例
  const instance = () => {
    return chartInstanceRef.current;
  };

  // 对父组件暴露的方法
  useImperativeHandle(
    ref,
    () => ({
      instance
    }),
    [chartInstanceRef.current]
  );

  // 渲染组件
  return loading ?
    <div style={{ position: 'relative', height: '100%' }}>
      <div
        style={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)'
        }}
      >
        <CircularProgress />
      </div>
    </div> :
    <div
      ref={chartRef}
      style={{
        width: width,
        height: height,
        cursor: 'pointer',
        minHeight: '1px',
        minWidth: '1px'
      }}
    />
  ;
};

// 对外暴露组件
const CommonChart = React.forwardRef(CommonChartInner);

export default React.memo(CommonChart);

创建 EarthEcharts 组件

在你的 React 应用中,创建一个名为 EarthEcharts.ts 的组件文件,并将以下代码添加到该文件中:

import React from 'react';
iimport { EChartOption } from '../EChartOption';
import CommonChart from '../CommonChart';
import { Box } from '@mui/material';
import 'echarts-gl';

export default function EarthEcharts() {
  // 这里放入你提供的 EarthEcharts 组件代码
}

export default EarthEcharts;

组件代码解析

现在让我们来解析 EarthEcharts 组件的代码。

数据准备

首先,我们需要准备一些地点的数据和连接这些地点的数据。这些数据将用于创建地球上的点和飞线效果。

const areaPointes = [
  {
    name: '杭州',
    point: [120.12, 30.16],
    itemStyleColor: '#ff9917',
    labelText: '杭州'
  },
  {
    name: '德国',
    point: [13.402393, 52.518569, 0],
    itemStyleColor: '#ff9917',
    labelText: '德国'
  },
  {
    name: '美国',
    point: [-100.696295, 33.679979, 0],
    itemStyleColor: '#ff9917',
    labelText: '美国'
  }
];

// 设置地理坐标映射
let geoCoordMap: any = {
   杭州: [120.12, 30.16],
   美国: [-100.696295, 33.679979],
   德国: [13.402393, 52.518569],
   加拿大: [-102.646409, 59.994255]
 };
  
const HZData = [
  [{ name: '杭州' }, { name: '加拿大', value: 80 }],
  [{ name: '杭州' }, { name: '美国', value: 100 }],
  [{ name: '杭州' }, { name: '德国', value: 95 }]
];

let convertData = function (data: any) {
   let res = [];

   for (let i = 0; i < data.length; i++) {
     let dataItem = data[i];
     let fromCoord = geoCoordMap[dataItem[1].name];
     let toCoord = geoCoordMap[dataItem[0].name];

     if (fromCoord && toCoord) {
       res.push([fromCoord, toCoord]);
     }
   }
    return res;
  };

创建 ECharts 图表

然后,我们根据上面的数据创建 ECharts 图表。在 render 方法中,我们设置了地球的外观和视角控制参数,并创建了散点和线条系列。

 const series = areaPointes.map((item) => {
    return {
      name: item.name, // 是否显示左上角图例
      type: 'scatter3D',
      coordinateSystem: 'globe',
      blendMode: 'source-over',
      symbol: 'circle',
      animation: true,
      symbolSize: 10, // 点位大小
      itemStyle: {
        color: item.itemStyleColor, // 各个点位的颜色设置
        opacity: 1, // 透明度
        borderWidth: 0, // 边框宽度
        borderColor: 'rgba(255,255,255,0.8)', //rgba(180, 31, 107, 0.8)
        shadowBlur: 20, // 设置发光效果的模糊程度
        shadowColor: 'rgba(255, 153, 23, 0.8)', // 设置发光的颜色
        emphasis: {
          // 强调显示效果
          label: {
            show: true
          },
          itemStyle: {
            color: '#fff',
            borderColor: 'red',
            borderWidth: 20
          }
        }
      },

      animationDelay: 1000, // 动画延迟1秒播放
      label: {
        show: false, // 是否显示字体
        position: 'left', // 字体位置。top、left、right、bottom
        formatter: item.labelText, // 具体显示的值
        textStyle: {
          color: '#fff', // 字体颜色
          borderWidth: 0, // 字体边框宽度
          borderColor: '#fff', // 字体边框颜色
          fontFamily: 'sans-serif', // 字体格式
          fontSize: 18, // 字体大小
          fontWeight: 700 // 字体加粗
        }
      },
      data: [item.point] // 数据来源
    };
  });

// 设置飞线
const lineSeries = [];
[['杭州', NNData]].forEach(function (item) {
  lineSeries.push({
    type: 'lines3D',
    effect: {
      show: true,
      period: 3,
      trailLength: 0.1
    },
     lineStyle: {
        //航线的视图效果
        color: '#ff9917',
        width: 2,
        opacity: 0.7
      },
    data: convertData(item[1])
  });
});
//  设置扩散坐标样式
const middleSeries = series.map((item) => {
    return {
      ...item,
      symbolSize: 20,
      itemStyle: {
        ...item.itemStyle,
        opacity: 0.4 // 透明度
      }
    };
  });

最终配置参数

最后,我们将所有的系列合并到 ECharts 的配置对象中,并返回一个包含地球图和图例的 React 组件。

const option = {
  backgroundColor: 'transparent',
  //地球配置
  globe: {
      //地球的半径。单位相对于三维空间
      globeRadius: 56,
      // 基础图片
      baseTexture: '/src/assets/images/widget-images/earth-skin-blue.jpg',
      // heightTexture: '/src/assets/images/widget-images/lines.png',
      // 地球顶点位移的大小。
      displacementScale: 0.1,
      // 地球中三维图形的着色效果
      // 'color' 只显示颜色,不受光照等其它因素的影响。
      // 'lambert' 通过经典的 lambert 着色表现光照带来的明暗。
      // 'realistic' 真实感渲染
      shading: 'lambert',
      //环境贴图。支持纯色、渐变色、全景贴图的 url
      // environment: '/src/assets/images/widget-images/earth-background.jpg',
      // displacementTexture: '/src/assets/images/widget-images/lines.png',
      //roughness属性用于表示材质的粗糙度,0为完全光滑,1完全粗糙,中间的值则是介于这两者之间
      realisticMaterial: {
        roughness: 0.1
      },
      atmosphere: {
        show: false // 大气层
      },
      light: {
        // 场景主光源的设置
        main: {
          // 主光源的颜色
          color: '#fff', // 光照颜色
          intensity: 0.8, // 光照强度
          shadow: true, // 是否显示阴影
          alpha: 40, // 主光源绕 x 轴,即上下旋转的角度
          beta: -30 //主光源绕 y 轴,即左右旋转的角度。
        },
        // 全局的环境光设置。
        ambient: {
          // /环境光的强度
          intensity: 1
        }
      },
      viewControl: {
        center: [0, 15, 0],
        autoRotate: true, // 是否开启视角绕物体的自动旋转查看
        autoRotateSpeed: 2, //物体自转的速度,单位为角度 / 秒,默认为10 ,也就是36秒转一圈。
        autoRotateAfterStill: 2, // 在鼠标静止操作后恢复自动旋转的时间间隔,默认 3s
        rotateSensitivity: 2, // 旋转操作的灵敏度,值越大越灵敏.设置为0后无法旋转。[1, 0]只能横向旋转.[0, 1]只能纵向旋转
        targetCoord: [116.46, 15], // 定位到北京
        zoomSensitivity: 0 // 禁止缩放
      }
    },
  series: [...series, ...middleSeries, ...lineSeries]
} as EChartOption;

return (
  <Box
    sx={{
      width: '100%',
      height: '100%',
      position: 'relative'
    }}
  >
    <CommonChart option={option} width="100%" height="100%" />
  </Box>
);

引入 EarthEcharts 组件

最后,将 EarthEcharts 组件引入到你的应用中的任何页面或组件中。你可以在需要的地方使用它,例如在一个页面组件中:

import React from 'react';
import EarthEcharts from './EarthEcharts';

function App() {
  return (
    <div className="App">
      <EarthEcharts />
    </div>
  );
}

export default App;

现在,你的 React 应用应该显示一个带有地球模拟扩散效果的图表了!

这就是如何使用 React 和 ECharts 创建地球模拟扩散效果的简要教程。希望这个示例对你有所帮助,你可以根据自己的需求进

背景图

背景图

地球贴图

在这里插入图片描述

  • 185
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值