首页 前端知识 分享个轮播的3D饼图,分别用Echarts和HighCharts实现

分享个轮播的3D饼图,分别用Echarts和HighCharts实现

2024-02-26 20:02:39 前端知识 前端哥 928 761 我要收藏

轮播的3D饼图,效果如下

Echarts效果图

在这里插入图片描述

有这方面需求的朋友肯定有在Echarts社区上找过相关3D饼图的方案。
大同小异,所有3D饼图的实现方式,基本上使用了surface曲面的原理,我也是在这个基础上进行的二开。
他的核心代码是使用surface的parametricEquation属性,为每一项数据生成了一个扇形的曲面参数方程,然后生成了不同的曲面,这里的方程完全看不懂,没关系,直接拿来用就行了。

function getParametricEquation(
    startRatio: any,
    endRatio: any,
    isSelected: any,
    isHovered: any,
    k: any,
    h: any
  ) {
    // 计算
    const midRatio = (startRatio + endRatio) / 2;

    const startRadian = startRatio * Math.PI * 2;
    const endRadian = endRatio * Math.PI * 2;
    const midRadian = midRatio * Math.PI * 2;

    // 如果只有一个扇形,则不实现选中效果。
    if (startRatio === 0 && endRatio === 1) {
      // eslint-disable-next-line no-param-reassign
      isSelected = false;
    }

    // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
    // eslint-disable-next-line no-param-reassign
    k = typeof k !== "undefined" ? k : 1 / 3;

    // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
    const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
    const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;

    // 计算高亮效果的放大比例(未高亮,则比例为 1)
    // const hoverRate = isHovered ? 1.05 : 1;

    // 返回曲面参数方程
    return {
      u: {
        min: -Math.PI,
        max: Math.PI * 3,
        step: Math.PI / 32,
      },

      v: {
        min: 0,
        max: Math.PI * 2,
        step: Math.PI / 20,
      },

      x(u: any, v: any) {
        if (u < startRadian) {
          return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k);
        }
        if (u > endRadian) {
          return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k);
        }
        return offsetX + Math.cos(u) * (1 + Math.cos(v) * k);
      },

      y(u: any, v: any) {
        if (u < startRadian) {
          return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k);
        }
        if (u > endRadian) {
          return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k);
        }
        return offsetY + Math.sin(u) * (1 + Math.cos(v) * k);
      },

      z(u: any, v: any) {
        if (u < -Math.PI * 0.5) {
          return Math.sin(u);
        }
        if (u > Math.PI * 2.5) {
          return Math.sin(u) * h * 0.1;
        }
        // 当前图形的高度是Z根据h(每个value的值决定的)
        return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
      },
    };
  }

强调一下:我的需求是个轮播,只需要当前这部分“高”起来,其他的部分统一高度即可。因此我的getParametricEquation函数的第六个参数h完全可以写死。需要展示不同高度的朋友这里的k就要根据实际情况传了。

接着来说一下轮播的原理

  1. 定义一个currentIndex变量,用来存放当前“高”的是哪个
let curIndex = 0;
  1. 获取到Echarts图表的实例,用ref存起来,后期轮播需要setOptions。我这里是React,如果是原生就更好获取了。
const eChartsDom = useRef<any>();
<EChartsReact
   option={option}
   style={{ width: "700px", height: "500px"}}
   ref={(e) => {eChartsDom.current = e;pipeAnimation()}} // 示例获取完就可以执行动画了
 />
  1. 定时器循环currentIndex并setOptions
  // 示例获取完执行
  const pipeAnimation = async () => {
    let timer = setInterval(() => {
      curIndex = curIndex +1
      if(curIndex === 5) curIndex = 0
      highLight({
        seriesIndex: curIndex,
        seriesName: data[curIndex].name
      })
    }, 2000);
  };

  // 把需要高的那一项的高度h调高,然后setOption
  const highLight = (params: any)=>{
    let myChart = eChartsDom.current.getEchartsInstance();
    let isSelected;
    let isHovered;
    let startRatio;
    let endRatio;
    let k;
    // 如果触发 mouseover 的扇形当前已高亮,则不做操作
    if (hoveredIndex === params.seriesIndex) {
      return;
      // 否则进行高亮及必要的取消高亮操作
    } else {
      // 如果当前有高亮的扇形,取消其高亮状态(对 option 更新)
      if (hoveredIndex !== "") {
        // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。
        isSelected = option.series[hoveredIndex].pieStatus.selected;
        isHovered = false;
        startRatio = option.series[hoveredIndex].pieData.startRatio;
        endRatio = option.series[hoveredIndex].pieData.endRatio;
        k = option.series[hoveredIndex].pieStatus.k;
        // 取消之前高的
        option.series[hoveredIndex].parametricEquation =
          getParametricEquation(
            startRatio,
            endRatio,
            isSelected,
            isHovered,
            k,
            30
          );
        option.series[hoveredIndex].pieStatus.hovered = isHovered;
        // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
        hoveredIndex = "";
      }
      // 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新)
      if (params.seriesName !== "mouseoutSeries") {
        // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
        isSelected = option.series[params.seriesIndex].pieStatus.selected;
        // isHovered = true;
        startRatio = option.series[params.seriesIndex].pieData.startRatio;
        endRatio = option.series[params.seriesIndex].pieData.endRatio;
        k = option.series[params.seriesIndex].pieStatus.k;
        // 在这里的一项调高了
        option.series[params.seriesIndex].parametricEquation =
          getParametricEquation(
            startRatio,
            endRatio,
            isSelected,
            isHovered,
            k,
            80
          );
        option.series[params.seriesIndex].pieStatus.hovered = isHovered;
        // 记录上次高亮的扇形对应的系列号 seriesIndex
        hoveredIndex = params.seriesIndex;
      }
      // 使用更新后的 option,渲染图表
      myChart.setOption(option);
    }
  }

PS:参考的Demo,它的Echarts版本比较高,没有在低版本上测试过,不过理论上来说是可行的。

OKK,这样一个简单的3D的轮播饼图的Demo就出来了。

放一下这个Demo的代码,一些点击事件、Label没有再开发,而且代码没有整理很杂乱,见谅!

/*
 * @Author:
 * @Date: 2022-08-06 07:46:56
 * @LastEditors: atwLee
 * @LastEditTime: 2022-08-07 01:02:21
 * @FilePath: /piethreed/src/pieThreeD.tsx
 * @Description:
 */
import React, { useRef } from "react";
import type { EChartsOption } from "echarts";
import EChartsReact from "echarts-for-react";
import "echarts-gl";

function PieThreeD() {
  let selectedIndex = "";
  let hoveredIndex = "";
  let curIndex = 0;
  let data = [
    {
      name: "cc",
      value: 2,
      itemStyle: {
        color: "#f77b66",
      },
    },
    {
      name: "aa",
      value: 1,
      itemStyle: {
        color: "#3edce0",
      },
    },
    {
      name: "bb",
      value: 1,
      itemStyle: {
        color: "#f94e76",
      },
    },
    {
      name: "ee",
      value: 1,
      itemStyle: {
        color: "#018ef1",
      },
    },
    {
      name: "dd",
      value: 1,
      itemStyle: {
        color: "#9e60f9",
      },
    },
  ];
  let option = getPie3D(
    data,
    0.59
  );
  // 生成扇形的曲面参数方程
  function getParametricEquation(
    startRatio: any,
    endRatio: any,
    isSelected: any,
    isHovered: any,
    k: any,
    h: any
  ) {
    // 计算
    const midRatio = (startRatio + endRatio) / 2;

    const startRadian = startRatio * Math.PI * 2;
    const endRadian = endRatio * Math.PI * 2;
    const midRadian = midRatio * Math.PI * 2;

    // 如果只有一个扇形,则不实现选中效果。
    if (startRatio === 0 && endRatio === 1) {
      // eslint-disable-next-line no-param-reassign
      isSelected = false;
    }

    // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
    // eslint-disable-next-line no-param-reassign
    k = typeof k !== "undefined" ? k : 1 / 3;

    // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
    const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
    const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;

    // 计算高亮效果的放大比例(未高亮,则比例为 1)
    // const hoverRate = isHovered ? 1.05 : 1;

    // 返回曲面参数方程
    return {
      u: {
        min: -Math.PI,
        max: Math.PI * 3,
        step: Math.PI / 32,
      },

      v: {
        min: 0,
        max: Math.PI * 2,
        step: Math.PI / 20,
      },

      x(u: any, v: any) {
        if (u < startRadian) {
          return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k);
        }
        if (u > endRadian) {
          return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k);
        }
        return offsetX + Math.cos(u) * (1 + Math.cos(v) * k);
      },

      y(u: any, v: any) {
        if (u < startRadian) {
          return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k);
        }
        if (u > endRadian) {
          return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k);
        }
        return offsetY + Math.sin(u) * (1 + Math.cos(v) * k);
      },

      z(u: any, v: any) {
        if (u < -Math.PI * 0.5) {
          return Math.sin(u);
        }
        if (u > Math.PI * 2.5) {
          return Math.sin(u) * h * 0.1;
        }
        // 当前图形的高度是Z根据h(每个value的值决定的)
        return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
      },
    };
  }
  // 生成模拟 3D 饼图的配置项
  function getPie3D(pieData: any, internalDiameterRatio: any) {
    const series: any = [];
    // 总和
    let sumValue = 0;
    let startValue = 0;
    let endValue = 0;
    const legendData = [];
    const k =
      typeof internalDiameterRatio !== "undefined"
        ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio)
        : 1 / 3;

    // 为每一个饼图数据,生成一个 series-surface 配置
    for (let i = 0; i < pieData.length; i += 1) {
      sumValue += pieData[i].value;

      const seriesItem: any = {
        name:
          typeof pieData[i].name === "undefined"
            ? `series${i}`
            : pieData[i].name,
        type: "surface",
        parametric: true,
        wireframe: {
          show: false,
        },
        pieData: pieData[i],
        pieStatus: {
          selected: false,
          hovered: false,
          k,
        },
      };

      if (typeof pieData[i].itemStyle !== "undefined") {
        const { itemStyle } = pieData[i];

        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
        typeof pieData[i].itemStyle.color !== "undefined"
          ? (itemStyle.color = pieData[i].itemStyle.color)
          : null;
        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
        typeof pieData[i].itemStyle.opacity !== "undefined"
          ? (itemStyle.opacity = pieData[i].itemStyle.opacity)
          : null;

        seriesItem.itemStyle = itemStyle;
      }
      series.push(seriesItem);
    }
    // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
    // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
    for (let i = 0; i < series.length; i += 1) {
      endValue = startValue + series[i].pieData.value;

      series[i].pieData.startRatio = startValue / sumValue;
      series[i].pieData.endRatio = endValue / sumValue;
      series[i].parametricEquation = getParametricEquation(
        series[i].pieData.startRatio,
        series[i].pieData.endRatio,
        false,
        false,
        k,
        // 我这里做了一个处理,使除了第一个之外的值都是10
        30
      );

      startValue = endValue;

      legendData.push(series[i].name);
    }

    // 准备待返回的配置项,把准备好的 legendData、series 传入。
    const option = {
      // animation: false,
      tooltip: {
        show: false,
        formatter: (params: any) => {
          if (params.seriesName !== "mouseoutSeries") {
            return `${
              params.seriesName
            }<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${
              params.color
            };"></span>${option.series[params.seriesIndex].pieData.value}`;
          }
          return "";
        },
      },
      xAxis3D: {
        min: -1,
        max: 1,
      },
      yAxis3D: {
        min: -1,
        max: 1,
      },
      zAxis3D: {
        min: -1,
        max: 1,
      },
      grid3D: {
        show: false,
        boxHeight: 5,
        top: "-20%",
        viewControl: {
          // 3d效果可以放大、旋转等,请自己去查看官方配置
          alpha: 35,
          // beta: 30,
          rotateSensitivity: 1,
          zoomSensitivity: 0,
          panSensitivity: 0,
          autoRotate: true,
          distance: 150,
        },
        // 后处理特效可以为画面添加高光、景深、环境光遮蔽(SSAO)、调色等效果。可以让整个画面更富有质感。
        postEffect: {
          // 配置这项会出现锯齿,请自己去查看官方配置有办法解决
          enable: false,
          bloom: {
            enable: true,
            bloomIntensity: 0.1,
          },
          SSAO: {
            enable: true,
            quality: "medium",
            radius: 2,
          },
          // temporalSuperSampling: {
          //   enable: true,
          // },
        },
      },
      series,
    };
    return option;
  }

  const highLight = (params: any)=>{

    console.log('params',params)

    let myChart = eChartsDom.current.getEchartsInstance();
    let isSelected;
    let isHovered;
    let startRatio;
    let endRatio;
    let k;


    // 如果触发 mouseover 的扇形当前已高亮,则不做操作
    if (hoveredIndex === params.seriesIndex) {
      return;

      // 否则进行高亮及必要的取消高亮操作
    } else {
      // 如果当前有高亮的扇形,取消其高亮状态(对 option 更新)
      if (hoveredIndex !== "") {
        // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。
        isSelected = option.series[hoveredIndex].pieStatus.selected;
        isHovered = false;
        startRatio = option.series[hoveredIndex].pieData.startRatio;
        endRatio = option.series[hoveredIndex].pieData.endRatio;
        k = option.series[hoveredIndex].pieStatus.k;
        // 对当前点击的扇形,执行取消高亮操作(对 option 更新)
        option.series[hoveredIndex].parametricEquation =
          getParametricEquation(
            startRatio,
            endRatio,
            isSelected,
            isHovered,
            k,
            30
          );
        option.series[hoveredIndex].pieStatus.hovered = isHovered;

        // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
        hoveredIndex = "";
      }

      // 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新)
      if (params.seriesName !== "mouseoutSeries") {
        // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
        isSelected = option.series[params.seriesIndex].pieStatus.selected;
        // isHovered = true;
        startRatio = option.series[params.seriesIndex].pieData.startRatio;
        endRatio = option.series[params.seriesIndex].pieData.endRatio;
        k = option.series[params.seriesIndex].pieStatus.k;
        // 对当前点击的扇形,执行高亮操作(对 option 更新)
        option.series[params.seriesIndex].parametricEquation =
          getParametricEquation(
            startRatio,
            endRatio,
            isSelected,
            isHovered,
            k,
            80
          );
        option.series[params.seriesIndex].pieStatus.hovered = isHovered;

        // 记录上次高亮的扇形对应的系列号 seriesIndex
        hoveredIndex = params.seriesIndex;
      }

      // 使用更新后的 option,渲染图表
      myChart.setOption(option);
    }
  }

  const onEvents = {
    // mouseover: (params:any) => highLight(params)
  };

  const eChartsDom = useRef<any>();

  const pipeAnimation = async () => {
    let timer = setInterval(() => {
      curIndex = curIndex +1
      if(curIndex === 5) curIndex = 0
      highLight({
        seriesIndex: curIndex,
        seriesName: data[curIndex].name
      })
    }, 2000);
  };

  return (
    <div>
      <EChartsReact
        option={option}
        style={{ width: "700px", height: "500px", margin: "auto" }}
        onEvents={onEvents}
        ref={(e) => {eChartsDom.current = e;pipeAnimation()}}
      />
    </div>
  );
}

export default PieThreeD;

HighCharts效果图

在这里插入图片描述
HighCharts其实是渲染了很多SVG,给人一种视觉上的3D。
相对于Echarts,他的饼图自带有3D属性,更易理解,例子。

实现原理与Echarts大同小异,都是一个currentIndex,来定时循环setOption。不过HC的文档不如EC易读。

说一下开发过程中遇到的问题

  1. 3D饼图有个重渲染数据下沉的bug,我也遇到了
    解决方法:找到了个大神的代码完美解决
  2. Label展示,原想着通过Renderer的方式,但后续无法准确定位到每一项的位置,因此使用了固定在中心的办法。(这里就完全可以在外边放个div了,就更简单,因为我已经用了renderer,就懒的再换)

贴下代码


// 调用
<Hcptd
   defaultH={10}
   highLightH={20}
   colors={['#058DC7', '#50B432', '#ED561B']}
   xNames={['Firefox', 'IE', 'Chrome']}
   yData={[30, 26.8, 12.8]}
   innerSize={200}
   LabelPosition={[135, 150]}
   onClickItem={(e:any)=>{
     console.log('eee',e);
   }}
 />

/*
 * @Author:
 * @Date: 2022-08-08 17:36:22
 * @LastEditors: atwLee
 * @LastEditTime: 2022-08-08 18:50:34
 * @FilePath: \shared-operation-capital-big-screen\src\pages\Dashboard\components\FundOperation\components\Right\components\hCPieTD\index.tsx
 * @Description:
 */
import React,{ useEffect, useRef } from 'react';
import Highcharts from 'highcharts';
import Highcharts3D from 'highcharts/highcharts-3d';
Highcharts3D(Highcharts);

const HCPTD: React.FC<{
  defaultH: number;
  highLightH:number;
  colors:string[];
  xNames:string[];
  yData:number[];
  innerSize:number;
  LabelPosition:number[];
  onClickItem?:any
}> = (props: any) => {
  // props数据
  // let defaultH = 20; // 默认高度
  // let highLightH = 50; // 模拟高亮的高度
  // let colors = ['#058DC7', '#50B432', '#ED561B'];
  // let xNames = ['Firefox', 'IE', 'Chrome']; // 饼图数据名称
  // let yData = [30, 26.8, 12.8]; // 饼图数据
  // let innerSize = 200; // 空心的比例
  // let LabelPosition = [135, 150]; // Label的位置

  let { defaultH, highLightH, colors, xNames, yData, innerSize, LabelPosition,onClickItem } = props;

  // highCharts实例,ref
  let chart = useRef<any>(null);
  // 模拟高亮的下标,ref
  let currentIndex = useRef<any>(0);
  // 周期性定时器,ref
  let intervalTimer = useRef<any>(null);
  // 一次性定时器,ref
  let timeoutTimer = useRef<any>(null);
  // 数据源
  let data: any = [];
  for (let index = 0; index < yData.length; index++) {
    let item = {
      name: xNames[index],
      y: yData[index],
      depth: defaultH,
    };
    data.push(item);
  }
  // 存放渲染的label
  let renderLabel = useRef<any>(null);

  useEffect(() => {
    (function (H) {
      Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'translate', function (proceed) {
        proceed.apply(this, [].slice.call(arguments, 1));
        if (!this.chart.is3d()) {
          return;
        }
        this.data.forEach((d) => {
          // 修改 3
          if (d.options.depth && typeof d.options.depth === 'number') {
            d.shapeArgs.depth = d.shapeArgs.depth * 0.75 + d.options.depth;
          }
        });
      });
      let cos = Math.cos;
      let sin = Math.sin;
      let PI = Math.PI;
      let dFactor = (4 * (Math.sqrt(2) - 1)) / 3 / (PI / 2);
      function curveTo(cx, cy, rx, ry, start, end, dx, dy) {
        let result = [];
        let arcAngle = end - start;
        if (end > start && end - start > Math.PI / 2 + 0.0001) {
          result = result.concat(curveTo(cx, cy, rx, ry, start, start + Math.PI / 2, dx, dy));
          result = result.concat(curveTo(cx, cy, rx, ry, start + Math.PI / 2, end, dx, dy));
          return result;
        }
        if (end < start && start - end > Math.PI / 2 + 0.0001) {
          result = result.concat(curveTo(cx, cy, rx, ry, start, start - Math.PI / 2, dx, dy));
          result = result.concat(curveTo(cx, cy, rx, ry, start - Math.PI / 2, end, dx, dy));
          return result;
        }
        return [
          [
            'C',
            cx + rx * Math.cos(start) - rx * dFactor * arcAngle * Math.sin(start) + dx,
            cy + ry * Math.sin(start) + ry * dFactor * arcAngle * Math.cos(start) + dy,
            cx + rx * Math.cos(end) + rx * dFactor * arcAngle * Math.sin(end) + dx,
            cy + ry * Math.sin(end) - ry * dFactor * arcAngle * Math.cos(end) + dy,
            cx + rx * Math.cos(end) + dx,
            cy + ry * Math.sin(end) + dy,
          ],
        ];
      }
      Highcharts.SVGRenderer.prototype.arc3dPath = function (shapeArgs) {
        let cx = shapeArgs.x || 0; // x coordinate of the center
        let cy = shapeArgs.y || 0; // y coordinate of the center
        let start = shapeArgs.start || 0; // start angle
        let end = (shapeArgs.end || 0) - 0.00001; // end angle
        let r = shapeArgs.r || 0; // radius
        let ir = shapeArgs.innerR || 0; // inner radius
        let d = shapeArgs.depth || 0; // depth
        let alpha = shapeArgs.alpha || 0; // alpha rotation of the chart
        let beta = shapeArgs.beta || 0; // beta rotation of the chart
        // Derived Variables
        const cs = Math.cos(start); // cosinus of the start angle
        const ss = Math.sin(start); // sinus of the start angle
        const ce = Math.cos(end); // cosinus of the end angle
        const se = Math.sin(end); // sinus of the end angle
        const rx = r * Math.cos(beta); // x-radius
        const ry = r * Math.cos(alpha); // y-radius
        const irx = ir * Math.cos(beta); // x-radius (inner)
        const iry = ir * Math.cos(alpha); // y-radius (inner)
        const dx = d * Math.sin(beta); // distance between top and bottom in x
        const dy = d * Math.sin(alpha); // distance between top and bottom in y
        // 修改 1
        cy -= dy;
        // TOP
        let top = [['M', cx + rx * cs, cy + ry * ss]];
        top = top.concat(curveTo(cx, cy, rx, ry, start, end, 0, 0));
        top.push(['L', cx + irx * ce, cy + iry * se]);
        top = top.concat(curveTo(cx, cy, irx, iry, end, start, 0, 0));
        top.push(['Z']);
        // OUTSIDE
        const b = beta > 0 ? Math.PI / 2 : 0;
        const a = alpha > 0 ? 0 : Math.PI / 2;
        const start2 = start > -b ? start : end > -b ? -b : start;
        const end2 = end < PI - a ? end : start < PI - a ? PI - a : end;
        const midEnd = 2 * PI - a;
        // When slice goes over bottom middle, need to add both, left and right
        // outer side. Additionally, when we cross right hand edge, create sharp
        // edge. Outer shape/wall:
        //
        //            -------
        //          /    ^    \
        //    4)   /   /   \   \  1)
        //        /   /     \   \
        //       /   /       \   \
        // (c)=> ====         ==== <=(d)
        //       \   \       /   /
        //        \   \<=(a)/   /
        //         \   \   /   / <=(b)
        //    3)    \    v    /  2)
        //            -------
        //
        // (a) - inner side
        // (b) - outer side
        // (c) - left edge (sharp)
        // (d) - right edge (sharp)
        // 1..n - rendering order for startAngle = 0, when set to e.g 90, order
        // changes clockwise (1->2, 2->3, n->1) and counterclockwise for
        // negative startAngle
        let out = [['M', cx + rx * cos(start2), cy + ry * sin(start2)]];
        out = out.concat(curveTo(cx, cy, rx, ry, start2, end2, 0, 0));
        // When shape is wide, it can cross both, (c) and (d) edges, when using
        // startAngle
        if (end > midEnd && start < midEnd) {
          // Go to outer side
          out.push(['L', cx + rx * cos(end2) + dx, cy + ry * sin(end2) + dy]);
          // Curve to the right edge of the slice (d)
          out = out.concat(curveTo(cx, cy, rx, ry, end2, midEnd, dx, dy));
          // Go to the inner side
          out.push(['L', cx + rx * cos(midEnd), cy + ry * sin(midEnd)]);
          // Curve to the true end of the slice
          out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end, 0, 0));
          // Go to the outer side
          out.push(['L', cx + rx * cos(end) + dx, cy + ry * sin(end) + dy]);
          // Go back to middle (d)
          out = out.concat(curveTo(cx, cy, rx, ry, end, midEnd, dx, dy));
          out.push(['L', cx + rx * cos(midEnd), cy + ry * sin(midEnd)]);
          // Go back to the left edge
          out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end2, 0, 0));
          // But shape can cross also only (c) edge:
        } else if (end > PI - a && start < PI - a) {
          // Go to outer side
          out.push(['L', cx + rx * Math.cos(end2) + dx, cy + ry * Math.sin(end2) + dy]);
          // Curve to the true end of the slice
          out = out.concat(curveTo(cx, cy, rx, ry, end2, end, dx, dy));
          // Go to the inner side
          out.push(['L', cx + rx * Math.cos(end), cy + ry * Math.sin(end)]);
          // Go back to the artifical end2
          out = out.concat(curveTo(cx, cy, rx, ry, end, end2, 0, 0));
        }
        out.push(['L', cx + rx * Math.cos(end2) + dx, cy + ry * Math.sin(end2) + dy]);
        out = out.concat(curveTo(cx, cy, rx, ry, end2, start2, dx, dy));
        out.push(['Z']);
        // INSIDE
        let inn = [['M', cx + irx * cs, cy + iry * ss]];
        inn = inn.concat(curveTo(cx, cy, irx, iry, start, end, 0, 0));
        inn.push(['L', cx + irx * Math.cos(end) + dx, cy + iry * Math.sin(end) + dy]);
        inn = inn.concat(curveTo(cx, cy, irx, iry, end, start, dx, dy));
        inn.push(['Z']);
        // SIDES
        const side1 = [
          ['M', cx + rx * cs, cy + ry * ss],
          ['L', cx + rx * cs + dx, cy + ry * ss + dy],
          ['L', cx + irx * cs + dx, cy + iry * ss + dy],
          ['L', cx + irx * cs, cy + iry * ss],
          ['Z'],
        ];
        const side2 = [
          ['M', cx + rx * ce, cy + ry * se],
          ['L', cx + rx * ce + dx, cy + ry * se + dy],
          ['L', cx + irx * ce + dx, cy + iry * se + dy],
          ['L', cx + irx * ce, cy + iry * se],
          ['Z'],
        ];
        // correction for changed position of vanishing point caused by alpha
        // and beta rotations
        let angleCorr = Math.atan2(dy, -dx);
        let angleEnd = Math.abs(end + angleCorr);
        let angleStart = Math.abs(start + angleCorr);
        let angleMid = Math.abs((start + end) / 2 + angleCorr);
        /**
         * set to 0-PI range
         * @private
         */
        function toZeroPIRange(angle) {
          angle = angle % (2 * Math.PI);
          if (angle > Math.PI) {
            angle = 2 * Math.PI - angle;
          }
          return angle;
        }
        angleEnd = toZeroPIRange(angleEnd);
        angleStart = toZeroPIRange(angleStart);
        angleMid = toZeroPIRange(angleMid);
        // *1e5 is to compensate pInt in zIndexSetter
        const incPrecision = 1e5;
        const a1 = angleMid * incPrecision;
        const a2 = angleStart * incPrecision;
        const a3 = angleEnd * incPrecision;
        let result = {
          top: top,
          // max angle is PI, so this is always higher
          zTop: Math.PI * incPrecision + 1,
          out: out,
          zOut: Math.max(a1, a2, a3),
          inn: inn,
          zInn: Math.max(a1, a2, a3),
          side1: side1,
          // to keep below zOut and zInn in case of same values
          zSide1: a3 * 0.99,
          side2: side2,
          zSide2: a2 * 0.99,
        };
        // 修改 2
        result.zTop = (result.zOut + 0.5) / 100;
        return result;
      };
    })(Highcharts);

    chart.current = Highcharts.chart('container', {
      chart: {
        type: 'pie',
        animation: true,
        events: {
          load: function () {
            let each = Highcharts.each;
            let points = this.series[0].points;
            each(points, (p: any) => {
              p.graphic.attr({
                translateY: -p.shapeArgs.ran,
              });
              p.graphic.side1.attr({
                translateY: -p.shapeArgs.ran,
              });
              p.graphic.side2.attr({
                translateY: -p.shapeArgs.ran,
              });
            });
          },
        },
        options3d: {
          enabled: true,
          alpha: 65,
          beta: 0,
        },
        backgroundColor: null,
      },
      colors,
      credits: {
        enabled: false,
      },
      title: {
        floating: true,
        text: '',
      },
      tooltip: {
        enabled: false,
      },
      plotOptions: {
        pie: {
          allowPointSelect: false,
          cursor: 'pointer',
          depth: 30,
          innerSize,
          dataLabels: {
            enabled: false,
          },
          states: {
            inactive: {
              opacity: 1,
            },
            hover: {
              enabled: false,
            },
          },
          events: {
            click: function (e: any) { 
              clearInterval(intervalTimer.current);
              currentIndex.current = e.point.index;
              onClickItem(currentIndex.current)
              highLight(currentIndex.current);
              labelRender(chart.current, e.point);
              if (timeoutTimer.current !== null) clearInterval(timeoutTimer.current);
              timeoutTimer.current = setTimeout(() => {
                Interval();
                clearTimeout(timeoutTimer.current);
              }, 10000);
            },
          },
        },
      },
      series: [
        {
          type: 'pie',
          name: 'Browser share',
          data: [...data],
        },
      ],
    });
    
    Interval();
    return () => {
      clearInterval(intervalTimer.current);
    };
  }, []);

  // Mock highLight
  const highLight = (currentIndex: number) => {
    let newData = [...data];
    newData.forEach((i, index) => {
      if (index === currentIndex) i.depth = highLightH;
      else i.depth = defaultH;
    });
    chart.current.series[0].update({
      data: newData,
    });
  };

  // Render Label
  function labelRender(chart: any, point: any) {
    if (renderLabel.current) {
      renderLabel.current.destroy();
    }
    renderLabel.current = chart.renderer
      .label(`${point.percentage.toFixed(2)}%`, LabelPosition[0], LabelPosition[1])
      .css({
        color: '#fff',
        fontSize: '42px',
      })
      .add()
      .toFront({ zIndex: 8 });
  }

  // Interval
  const Interval = () => {
    intervalTimer.current = setInterval(() => {
      currentIndex.current = currentIndex.current === yData.length - 1 ? 0 : currentIndex.current + 1;
      highLight(currentIndex.current);
      labelRender(chart.current, chart.current.series[0].points[currentIndex.current]);
    }, 4000);
  };

  return <div id="container" style={{ width: '400px', height: '400px', zIndex: 999 }} />;
};

export default HCPTD;

PS:HighCharts若用于商业,请购买版权!!!

感谢观看!

转载请注明出处或者链接地址:https://www.qianduange.cn//article/2764.html
标签
react.js3d
评论
会员中心 联系我 留言建议 回顶部
复制成功!