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


    轮播的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;
          },
        };
      }
    
    • 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

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

    接着来说一下轮播的原理

    1. 定义一个currentIndex变量,用来存放当前“高”的是哪个
    let curIndex = 0;
    
    • 1
    1. 获取到Echarts图表的实例,用ref存起来,后期轮播需要setOptions。我这里是React,如果是原生就更好获取了。
    const eChartsDom = useRef<any>();
    <EChartsReact
       option={option}
       style={{ width: "700px", height: "500px"}}
       ref={(e) => {eChartsDom.current = e;pipeAnimation()}} // 示例获取完就可以执行动画了
     />
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    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);
        }
      }
    
    • 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

    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
                }
    ${ params.color };">
    ${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;
    • 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
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376

    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;
    
    • 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
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398

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

    感谢观看!

  • 相关阅读:
    idea连接远程k8s集群使用kubernetes-client
    对比分析法、多维度拆解、辛普森悖论
    基于跳蛛算法的无人机航迹规划-附代码
    Qt,C++中QString,string,char数组等常用格式转换大总结
    Http请求get与post请求方式的各种相关面试总结
    互联网数字化管理升级,制造企业一站式智能管理,可定制-亿发
    华为汪涛:5.5G时代UBB目标网,跃升数字生产力
    数据结构 第一章作业 绪论 西安石油大学
    使用Watchtower实现Docker容器自动更新
    关于malloc源码中的bin_at宏定义的个人见解
  • 原文地址:https://blog.csdn.net/BWater_monster/article/details/126236362