• Echarts 3D饼图开发


    在这里插入图片描述
    近期的开发需求,需要开发一个3D饼图。不同于echarts的二维饼图,有完善的API,开发起来比较顺手。3D类的图资料较少,就连Echarts官网提供的相关API信息也是模模糊糊的,理解起来不容易。

    以饼图为例子。一个完整的2D饼图是由一个或者多个扇形组成的;而一个完整的3D饼图是由一个或者多个扇形曲面组成。

    Echarts曲面绘制通过series-surface. type ="surface"配置项来设置,详细参数说明,请参考官网。
    |——》任意门

    其实光看官网的配置参数,会很难理解。因为没有足够的示例,无法进行调试测试,导致对于知识难掌握,容易产生厌烦情绪。(没错!说的就是俺)

    最好的办法就是先去社区或者网络找找有没有相关案例,感谢这些乐于分享知识的开发者们。——》Echarts社区入口
    在这里插入图片描述
    其实也有不少,但是并不能完全满足我的开发需求。只能通过现有的满足条件的案例,拿到代码后再跑一遍,理解里面的配置项的涵义,再自己慢慢调整开发。

    下面对实现流程进行简单解析。

    准备工作

    依赖除了必要的echarts依赖外,还需echarts-gl。
    |——》任意门

    在这里要注意版本匹配,否则会报错:

    echarts-gl 2.x版本的是和echarts 5.X的版本相匹配的。
    echarts-gl 1.x版本的是和echarts 4.X的版本相匹配的。

    也就是说如果你echarts是4.x的,但是echarts-gl是2.x的,这是万万使不得的,会报错哦~

    3D饼图实现

    如下图的3D饼图,是由4个扇形曲面实现。
    在这里插入图片描述

    生成扇形的曲面参数方程

    用于 series-surface.parametricEquation

     function getParametricEquation(startRatio, endRatio, isSelected, isHovered) {
                // 计算
                let midRatio = (startRatio + endRatio) / 2;
                let startRadian = startRatio * Math.PI * 2;
                let endRadian = endRatio * Math.PI * 2;
                let midRadian = midRatio * Math.PI * 2;
                // 如果只有一个扇形,则不实现选中效果。
                if (startRatio === 0 && endRatio === 1) {
                    isSelected = false;
                }
                // 计算选中效果分别在 x 轴、y 轴方向上的位移(位移均为 0)
                let offsetX = 0;
                let offsetY = 0;
                // 计算选中效果在 z 轴方向上的位移(未选中,位移均为 0)
                let offsetZ = isSelected ? 0.15 : 0;
                // 计算高亮效果的放大比例(未高亮,则比例为 1)
                let hoverRate = isHovered ? 1.05 : 1;
                let tmp = 0;
                // 返回曲面参数方程
                return {
                    u: {
                        min: 0,
                        max: Math.PI * 2,
                        step: Math.PI / 100,
                    },
                    v: {
                        min: 0,
                        max: Math.PI,
                        step: Math.PI / 50,
                    },
                    x: function (u, v) {
                        if (midRatio - 0.5 < 0) {
                            if (u < startRadian || u > midRadian + Math.PI) {
                                tmp =
                                    u - Math.PI - midRadian < 0
                                        ? u + Math.PI - midRadian
                                        : u - Math.PI - midRadian;
                                return (
                                    offsetX +
                                    ((Math.sin(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian && u < midRadian + Math.PI) {
                                tmp = midRadian + Math.PI - u;
                                return (
                                    offsetX +
                                    ((Math.sin(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        } else {
                            if (u < startRadian && u > midRadian - Math.PI) {
                                tmp = u + Math.PI - midRadian;
                                return (
                                    offsetX +
                                    ((Math.sin(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian || u < midRadian - Math.PI) {
                                tmp =
                                    midRadian - Math.PI - u < 0
                                        ? midRadian + Math.PI - u
                                        : midRadian - Math.PI - u;
                                return (
                                    offsetX +
                                    ((Math.sin(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        }
                        return offsetX + Math.sin(v) * Math.sin(u) * hoverRate;
                    },
                    y: function (u, v) {
                        if (midRatio - 0.5 < 0) {
                            if (u < startRadian || u > midRadian + Math.PI) {
                                tmp =
                                    u - Math.PI - midRadian < 0
                                        ? u + Math.PI - midRadian
                                        : u - Math.PI - midRadian;
                                return (
                                    offsetY +
                                    ((Math.cos(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian && u < midRadian + Math.PI) {
                                tmp = midRadian + Math.PI - u;
                                return (
                                    offsetY +
                                    ((Math.cos(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        } else {
                            if (u < startRadian && u > midRadian - Math.PI) {
                                tmp = u + Math.PI - midRadian;
                                return (
                                    offsetY +
                                    ((Math.cos(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian || u < midRadian - Math.PI) {
                                tmp =
                                    midRadian - Math.PI - u < 0
                                        ? midRadian + Math.PI - u
                                        : midRadian - Math.PI - u;
                                return (
                                    offsetY +
                                    ((Math.cos(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        }
                        return offsetY + Math.sin(v) * Math.cos(u) * hoverRate;
                    },
                    z: function (u, v) {
                        return offsetZ + (Math.cos(v) > 0 ? 0.1 : -0.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
    • 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

    大部分小可爱肯定会在这一步感到疑惑,u、v、x、y、z这都是啥呀(((o(゚▽゚)o)))?赶紧去官网查查。
    然而,
    在这里插入图片描述
    ┻━┻︵╰(‵□′)╯︵┻━┻

    没错,依然还是不懂呀!!!(希望官网能再完善一下,写得再详细一些。)

    其实,u、v、x、y、z是与球坐标系相关的参数。

    球坐标系是三维坐标系的一种,用以确定三维空间中点、线、面以及体的位置,它以坐标原点为参考点,由方位角、仰角和距离构成。
    在这里插入图片描述
    在数学里,球坐标系(英语:Spherical coordinate system)是一种利用球坐标表示一个点 p 在三维空间的位置的三维正交坐标系。图1显示了球坐标的几何意义:原点到 P 点的距离 r ,原点到点 P 的连线与正 z-轴之间的天顶角以及原点到点 P 的连线,在 xy-平面的投影线,与正 x-轴之间的方位角。
    在这里插入图片描述
    例子:
    假设P(x,y,z)为空间内一点,则点P也可用这样三个有次序的数(r,θ,φ)来确定,其中r为原点O与点P间的距离;θ为有向线段OP与z轴正向的夹角;φ为从正z轴来看自x轴按逆时针方向转到OM所转过的角,这里M为点P在xOy面上的投影;。这样的三个数r,θ,φ叫做点P的球面坐标,显然,这里r,θ,φ的变化范围为r∈[0,+∞),θ∈[0, π], φ∈[0,2π] ,如图所示。
    当r,θ或φ分别为常数时,可以表示如下特殊曲面:r = 常数,即以原点为心的球面;θ= 常数,即以原点为顶点、z轴为轴的圆锥面;φ= 常数,即过z轴的半平面。
    转化:
    1).球坐标系(r,θ,φ)与直角坐标系(x,y,z)的转换关系:
    x=rsinθcosφ.
    y=rsinθsinφ.
    z=rcosθ.
    2).反之,直角坐标系(x,y,z)与球坐标系(r,θ,φ)的转换关系为:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    在这里,u就代表球坐标系中的φ,v代表球坐标系中的θ。

    有了这层知识护航,再去看上面的曲面方程,理解立刻+1了!↖(⁎⁍̴̛ᴗ⁍̴̛⁎)↗

    再搭配这个3D球示例看,会更上一层楼哦~
    |——》任意门

    调用曲面方程生成3D图

    为每一个配置项生成曲面3D扇形。

      // 生成模拟 3D 饼图的配置项
            function getPie3D(pieData) {
                let series = [];
                let sumValue = 0;
                let startValue = 0;
                let endValue = 0;
                let legendData = [];
                // 为每一个饼图数据,生成一个 series-surface 配置
                for (let i = 0; i < pieData.length; i++) {
                    sumValue += pieData[i].value;
                    let seriesItem = {
                        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,
                        },
                    };
    
                    if (typeof pieData[i].itemStyle != "undefined") {
                        let itemStyle = {};
                        if (typeof pieData[i].itemStyle.color != "undefined") {
                            itemStyle.color = pieData[i].itemStyle.color;
                        }
                        if (typeof pieData[i].itemStyle.opacity != "undefined") {
                            itemStyle.opacity = pieData[i].itemStyle.opacity;
                        }
                        seriesItem.itemStyle = itemStyle;
                    }
                    series.push(seriesItem);
                }
                // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
                // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
                for (let i = 0; i < series.length; i++) {
                    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
                    );
                    startValue = endValue;
                    legendData.push(series[i].name);
                }
                // 补充一个透明的圆环,用于支撑高亮功能的近似实现。
                series.push({
                    name: "mouseoutSeries",
                    type: "surface",
                    parametric: true,
                    wireframe: {
                        show: false,
                    },
                    itemStyle: {
                        opacity: 0,
                    },
                    parametricEquation: {
                        u: {
                            min: 0,
                            max: Math.PI * 2,
                            step: Math.PI / 20,
                        },
                        v: {
                            min: 0,
                            max: Math.PI,
                            step: Math.PI / 20,
                        },
                        x: function (u, v) {
                            return Math.sin(v) * Math.sin(u) + Math.sin(u);
                        },
                        y: function (u, v) {
                            return Math.sin(v) * Math.cos(u) + Math.cos(u);
                        },
                        z: function (u, v) {
                            return Math.cos(v) > 0 ? 0.1 : -0.1;
                        },
                    },
                });
                // 准备待返回的配置项,把准备好的 legendData、series 传入。
                let option = {
                    legend: {
                        show: false,
                        data: legendData,
                    },
                    xAxis3D: {
                        min: -1,
                        max: 1,
                    },
                    yAxis3D: {
                        min: -1,
                        max: 1,
                    },
                    zAxis3D: {
                        min: -1,
                        max: 1,
                    },
                    grid3D: {
                        show: false,
                        boxHeight: 100, // 厚度
                        top: 4,
                        left: 0,
                        boxWidth: 144,
                        viewControl: {
                            //3d效果可以放大、旋转等,请自己去查看官方配置
                            alpha: 43, // 角度
                            beta: 0, // 饼块开始位置角度
                            rotateSensitivity: 0,
                            zoomSensitivity: 0,
                            panSensitivity: 0,
                            autoRotate: false,
                        },
                        //后处理特效可以为画面添加高光、景深、环境光遮蔽(SSAO)、调色等效果。可以让整个画面更富有质感。
                        postEffect: {
                            //配置这项会出现锯齿,请自己去查看官方配置有办法解决
                            enable: true,
                            bloom: {
                                enable: true,
                                bloomIntensity: 0.1,
                            },
                            SSAO: {
                                enable: true,
                                quality: "medium",
                                radius: 2,
                            },
                        },
                        light: {
                            main: {
                                color: "rgb(85, 84, 84)", // 主光源的颜色。
                                shadow: true, // 主光源是否投射阴影
                                alpha: 80, // 主光源绕 x 轴,即上下旋转的角度
                            },
                        },
                    },
                    series: series,
                };
                return 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
    • 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

    指示线实现

    方案:

    1.用series-line3D,生成指示线。
    我试了一下,这种不好看,生硬,不灵活。而且很容易超出边界。
    示范例子
    2.在原来的曲面上,再生成一层2D饼图,调整2D饼图的位置和大小,再使2D饼图透明,保留其指示线和标签。
    (这里比较难的是,调整2D图的位置和大小,因为2D图和标签无法完美发生角度偏转,若3D图旋转角度过大,则2D标签无法调整到和3D图完美匹配。)

    透明前:
    在这里插入图片描述
    透明后:
    在这里插入图片描述
    关键代码:

            option.series.push({
                name: '酒水销售占比', //自己根据场景修改
                type: 'pie',
                hoverAnimation: false,// 悬停不放大
                label: {
                    position: "bottom",
                    formatter: function (params) {
                        return `{percentSty|${params.percent}%}\n{nameSty|${params.name}}`;
                    },
                    rich: {
                        nameSty: {
                            fontSize: 16,
                            lineHeight: 22,
                            fontFamily: "PingFangSC-Regular",
                            fintWeight: 400,
                        },
                        percentSty: {
                            fontSize: 14,
                            lineHeight: 20,
                            fontFamily: "PingFangSC-Regular",
                            fintWeight: 400,
                            color: '#FFFFFF',
                        },
                        countSty: {
                            fontSize: 14,
                            lineHeight: 20,
                            fontFamily: "PingFangSC-Regular",
                            fintWeight: 400,
                            color: '#B9D3ED',
                            padding: [0, 8, 0, 8],
                            backgroundColor: 'rgb(90,156,241,0.3)',
                            borderRadius: 2,
                        },
                    },
                },
                labelLine: {
                    showAbove: false,
                    length: 20, // 视觉引导线第一段的长度
                    length2: 40, // 视觉引导项第二段的长度
                    lineStyle: {
                        color: "#686868", // 改变标示线的颜色
                        width: 1,
                        type: 'solid', // 线的类型
                    },
                },
                startAngle: 60, // 起始角度,支持范围[0, 360]。
                clockwise: true, // 饼图的扇区是否是顺时针排布。上述这两项配置主要是为了对齐3d的样式
                radius: ['40%', '52%'],
                center: ['50%', '53%'],
                data: paramsList,
                itemStyle: {
                    opacity: 0  //这里必须是0,不然2d的图会覆盖在表面
                }
            })
    
    • 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
    到此,3D饼图的难点之处已介绍完毕,有需要的小可爱可以自己着手试试。看看指示线部分还有没有其他更好的方法实现,若找到,务必@我,让我学习一下☆〜(ゝ。∂)

    完整代码贴上

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Ehcarts Demo</title>
        <script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
        <script src="https://echarts.baidu.com/resource/echarts-gl-latest/dist/echarts-gl.min.js"></script>
    </head>
    
    <body>
        <div class="container">
            <div id="chartsContent" style="width:560px;height:150px;"></div>
        </div>
        <script>
            var myChart = echarts.init(document.getElementById('chartsContent'));
            // 生成扇形的曲面参数方程,用于 series-surface.parametricEquation
            function getParametricEquation(startRatio, endRatio, isSelected, isHovered) {
                // 计算
                let midRatio = (startRatio + endRatio) / 2;
    
                let startRadian = startRatio * Math.PI * 2;
                let endRadian = endRatio * Math.PI * 2;
                let midRadian = midRatio * Math.PI * 2;
    
                // 如果只有一个扇形,则不实现选中效果。
                if (startRatio === 0 && endRatio === 1) {
                    isSelected = false;
                }
    
                // 计算选中效果分别在 x 轴、y 轴方向上的位移(位移均为 0)
                let offsetX = 0;
                let offsetY = 0;
                // 计算选中效果在 z 轴方向上的位移(未选中,位移均为 0)
                let offsetZ = isSelected ? 0.15 : 0;
                // 计算高亮效果的放大比例(未高亮,则比例为 1)
                let hoverRate = isHovered ? 1.05 : 1;
    
                let tmp = 0;
    
                // 返回曲面参数方程
                return {
                    u: {
                        min: 0,
                        max: Math.PI * 2,
                        step: Math.PI / 100,
                    },
    
                    v: {
                        min: 0,
                        max: Math.PI,
                        step: Math.PI / 50,
                    },
    
                    x: function (u, v) {
                        if (midRatio - 0.5 < 0) {
                            if (u < startRadian || u > midRadian + Math.PI) {
                                tmp =
                                    u - Math.PI - midRadian < 0
                                        ? u + Math.PI - midRadian
                                        : u - Math.PI - midRadian;
                                return (
                                    offsetX +
                                    ((Math.sin(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian && u < midRadian + Math.PI) {
                                tmp = midRadian + Math.PI - u;
                                return (
                                    offsetX +
                                    ((Math.sin(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        } else {
                            if (u < startRadian && u > midRadian - Math.PI) {
                                tmp = u + Math.PI - midRadian;
                                return (
                                    offsetX +
                                    ((Math.sin(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian || u < midRadian - Math.PI) {
                                tmp =
                                    midRadian - Math.PI - u < 0
                                        ? midRadian + Math.PI - u
                                        : midRadian - Math.PI - u;
                                return (
                                    offsetX +
                                    ((Math.sin(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        }
                        return offsetX + Math.sin(v) * Math.sin(u) * hoverRate;
                    },
    
                    y: function (u, v) {
                        if (midRatio - 0.5 < 0) {
                            if (u < startRadian || u > midRadian + Math.PI) {
                                tmp =
                                    u - Math.PI - midRadian < 0
                                        ? u + Math.PI - midRadian
                                        : u - Math.PI - midRadian;
                                return (
                                    offsetY +
                                    ((Math.cos(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian && u < midRadian + Math.PI) {
                                tmp = midRadian + Math.PI - u;
                                return (
                                    offsetY +
                                    ((Math.cos(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        } else {
                            if (u < startRadian && u > midRadian - Math.PI) {
                                tmp = u + Math.PI - midRadian;
                                return (
                                    offsetY +
                                    ((Math.cos(startRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                            if (u > endRadian || u < midRadian - Math.PI) {
                                tmp =
                                    midRadian - Math.PI - u < 0
                                        ? midRadian + Math.PI - u
                                        : midRadian - Math.PI - u;
                                return (
                                    offsetY +
                                    ((Math.cos(endRadian) * tmp) /
                                        (Math.PI - midRadian + startRadian)) *
                                    hoverRate
                                );
                            }
                        }
                        return offsetY + Math.sin(v) * Math.cos(u) * hoverRate;
                    },
    
                    z: function (u, v) {
                        return offsetZ + (Math.cos(v) > 0 ? 0.1 : -0.1);
                    },
                };
            }
            // 生成模拟 3D 饼图的配置项
            function getPie3D(pieData) {
                let series = [];
                let sumValue = 0;
                let startValue = 0;
                let endValue = 0;
                let legendData = [];
                // 为每一个饼图数据,生成一个 series-surface 配置
                for (let i = 0; i < pieData.length; i++) {
                    sumValue += pieData[i].value;
                    let seriesItem = {
                        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,
                        },
                    };
    
                    if (typeof pieData[i].itemStyle != "undefined") {
                        let itemStyle = {};
                        if (typeof pieData[i].itemStyle.color != "undefined") {
                            itemStyle.color = pieData[i].itemStyle.color;
                        }
                        if (typeof pieData[i].itemStyle.opacity != "undefined") {
                            itemStyle.opacity = pieData[i].itemStyle.opacity;
                        }
                        seriesItem.itemStyle = itemStyle;
                    }
                    series.push(seriesItem);
                }
                // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
                // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
                for (let i = 0; i < series.length; i++) {
                    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
                    );
                    startValue = endValue;
                    legendData.push(series[i].name);
                }
                // 补充一个透明的圆环,用于支撑高亮功能的近似实现。
                series.push({
                    name: "mouseoutSeries",
                    type: "surface",
                    parametric: true,
                    wireframe: {
                        show: false,
                    },
                    itemStyle: {
                        opacity: 0,
                    },
                    parametricEquation: {
                        u: {
                            min: 0,
                            max: Math.PI * 2,
                            step: Math.PI / 20,
                        },
                        v: {
                            min: 0,
                            max: Math.PI,
                            step: Math.PI / 20,
                        },
                        x: function (u, v) {
                            return Math.sin(v) * Math.sin(u) + Math.sin(u);
                        },
                        y: function (u, v) {
                            return Math.sin(v) * Math.cos(u) + Math.cos(u);
                        },
                        z: function (u, v) {
                            return Math.cos(v) > 0 ? 0.1 : -0.1;
                        },
                    },
                });
                // 准备待返回的配置项,把准备好的 legendData、series 传入。
                let option = {
                    legend: {
                        show: false,
                        data: legendData,
                    },
                    xAxis3D: {
                        min: -1,
                        max: 1,
                    },
                    yAxis3D: {
                        min: -1,
                        max: 1,
                    },
                    zAxis3D: {
                        min: -1,
                        max: 1,
                    },
                    grid3D: {
                        show: false,
                        boxHeight: 100, // 厚度
                        top: 4,
                        left: 0,
                        boxWidth: 144,
                        viewControl: {
                            //3d效果可以放大、旋转等,请自己去查看官方配置
                            alpha: 43, // 角度
                            beta: 0, // 饼块开始位置角度
                            rotateSensitivity: 0,
                            zoomSensitivity: 0,
                            panSensitivity: 0,
                            autoRotate: false,
                        },
                        //后处理特效可以为画面添加高光、景深、环境光遮蔽(SSAO)、调色等效果。可以让整个画面更富有质感。
                        postEffect: {
                            //配置这项会出现锯齿,请自己去查看官方配置有办法解决
                            enable: true,
                            bloom: {
                                enable: true,
                                bloomIntensity: 0.1,
                            },
                            SSAO: {
                                enable: true,
                                quality: "medium",
                                radius: 2,
                            },
                        },
                        light: {
                            main: {
                                color: "rgb(85, 84, 84)", // 主光源的颜色。
                                shadow: true, // 主光源是否投射阴影
                                alpha: 80, // 主光源绕 x 轴,即上下旋转的角度
                            },
                        },
                    },
                    series: series,
                };
                return option;
            }
    
            // 监听鼠标事件,实现饼图选中效果(单选),近似实现高亮(放大)效果。
            let selectedIndex = '';
            let hoveredIndex = '';
    
            // 监听点击事件,实现选中效果(单选)
            myChart.on('click', function (params) {
                // 从 option.series 中读取重新渲染扇形所需的参数,将是否选中取反。
                let isSelected = !option.series[params.seriesIndex].pieStatus.selected;
                let isHovered = option.series[params.seriesIndex].pieStatus.hovered;
                let startRatio = option.series[params.seriesIndex].pieData.startRatio;
                let endRatio = option.series[params.seriesIndex].pieData.endRatio;
    
                // 如果之前选中过其他扇形,将其取消选中(对 option 更新)
                if (selectedIndex !== '' && selectedIndex !== params.seriesIndex) {
                    option.series[selectedIndex].parametricEquation = getParametricEquation(option.series[selectedIndex].pieData.startRatio, option.series[selectedIndex].pieData.endRatio, false, false);
                    option.series[selectedIndex].pieStatus.selected = false;
                }
    
                // 对当前点击的扇形,执行选中/取消选中操作(对 option 更新)
                option.series[params.seriesIndex].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, isHovered);
                option.series[params.seriesIndex].pieStatus.selected = isSelected;
    
                // 如果本次是选中操作,记录上次选中的扇形对应的系列号 seriesIndex
                isSelected ? selectedIndex = params.seriesIndex : null;
    
                console.log('option-click: ', option)
                // 使用更新后的 option,渲染图表
                myChart.setOption(option);
            });
    
            // 监听 mouseover,近似实现高亮(放大)效果
            myChart.on('mouseover', function (params) {
    
                // 准备重新渲染扇形所需的参数
                let isSelected;
                let isHovered;
                let startRatio;
                let endRatio;
    
                // 如果触发 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;
    
                        // 对当前点击的扇形,执行取消高亮操作(对 option 更新)
                        option.series[hoveredIndex].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, isHovered);
                        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;
    
                        // 对当前点击的扇形,执行高亮操作(对 option 更新)
                        option.series[params.seriesIndex].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, isHovered);
                        option.series[params.seriesIndex].pieStatus.hovered = isHovered;
    
                        // 记录上次高亮的扇形对应的系列号 seriesIndex
                        hoveredIndex = params.seriesIndex;
                    }
                    // 使用更新后的 option,渲染图表
                    myChart.setOption(option);
                }
            });
    
            // 修正取消高亮失败的 bug
            myChart.on('globalout', function () {
    
    
                if (hoveredIndex !== '') {
                    // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
                    isSelected = option.series[hoveredIndex].pieStatus.selected;
                    isHovered = false;
                    startRatio = option.series[hoveredIndex].pieData.startRatio;
                    endRatio = option.series[hoveredIndex].pieData.endRatio;
    
                    // 对当前点击的扇形,执行取消高亮操作(对 option 更新)
                    option.series[hoveredIndex].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, isHovered);
                    option.series[hoveredIndex].pieStatus.hovered = isHovered;
    
                    // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
                    hoveredIndex = '';
                }
    
                // 使用更新后的 option,渲染图表
                myChart.setOption(option);
            });
    
            const colorList = ['#D98053', '#E2B062', '#5A9CF1', '#6ED3D3']
            const dataSource = [{
                name: "啤酒",
                value: 25,
            },
            {
                name: "高粱酒",
                value: 25,
            },
            {
                name: "桃花酿",
                value: 30,
            },
            {
                name: "白酒",
                value: 20,
            },]
            const paramsList = dataSource.map((item, index) => {
                return {
                    ...item,
                    shading: 'realistic',
                    itemStyle: {
                        color: colorList[index]
                    },
                }
            })
            // 传入数据生成 option
            let option = getPie3D(paramsList);
    
            // 是否需要label指引线,如果要就添加一个透明的2d饼状图并调整角度使得labelLine和3d的饼状图对齐,并再次setOption
            option.series.push({
                name: '酒水销售占比', //自己根据场景修改
                type: 'pie',
                hoverAnimation: false,// 悬停不放大
                label: {
                    position: "bottom",
                    formatter: function (params) {
                        return `{percentSty|${params.percent}%}\n{nameSty|${params.name}}`;
                    },
                    rich: {
                        nameSty: {
                            fontSize: 16,
                            lineHeight: 22,
                            fontFamily: "PingFangSC-Regular",
                            fintWeight: 400,
                        },
                        percentSty: {
                            fontSize: 14,
                            lineHeight: 20,
                            fontFamily: "PingFangSC-Regular",
                            fintWeight: 400,
                            color: '#FFFFFF',
                        },
                        countSty: {
                            fontSize: 14,
                            lineHeight: 20,
                            fontFamily: "PingFangSC-Regular",
                            fintWeight: 400,
                            color: '#B9D3ED',
                            padding: [0, 8, 0, 8],
                            backgroundColor: 'rgb(90,156,241,0.3)',
                            borderRadius: 2,
                        },
                    },
                },
                labelLine: {
                    showAbove: false,
                    length: 20, // 视觉引导线第一段的长度
                    length2: 40, // 视觉引导项第二段的长度
                    lineStyle: {
                        color: "#686868", // 改变标示线的颜色
                        width: 1,
                        type: 'solid', // 线的类型
                    },
                },
                startAngle: 60, // 起始角度,支持范围[0, 360]。
                clockwise: true, // 饼图的扇区是否是顺时针排布。上述这两项配置主要是为了对齐3d的样式
                radius: ['40%', '52%'],
                center: ['50%', '53%'],
                data: paramsList,
                itemStyle: {
                    opacity: 0  //这里必须是0,不然2d的图会覆盖在表面
                }
            })
            myChart.setOption(option)
        </script>
        <style>
            .container {
                width: 560px;
                height: 164px;
                background-color: #000000;
                position: relative;
            }
    
            .imgContent {
                width: 164px;
                height: 86px;
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                z-index: 5;
            }
        </style>
    </body>
    
    </html>
    
    • 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
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
    • 420
    • 421
    • 422
    • 423
    • 424
    • 425
    • 426
    • 427
    • 428
    • 429
    • 430
    • 431
    • 432
    • 433
    • 434
    • 435
    • 436
    • 437
    • 438
    • 439
    • 440
    • 441
    • 442
    • 443
    • 444
    • 445
    • 446
    • 447
    • 448
    • 449
    • 450
    • 451
    • 452
    • 453
    • 454
    • 455
    • 456
    • 457
    • 458
    • 459
    • 460
    • 461
    • 462
    • 463
    • 464
    • 465
    • 466
    • 467
    • 468
    • 469
    • 470
    • 471
    • 472
    • 473
    • 474
    • 475
    • 476
    • 477
    • 478
    • 479
    • 480
    • 481
    • 482
    • 483
    • 484
    • 485
    • 486
    • 487
    • 488
    • 489
    • 490
    • 491
    • 492
    • 493
    • 494
    • 495
    • 496
    • 497
    • 498
    • 499
    • 500
    • 501
    • 502
    • 503
    • 504
    • 505
    • 506
    • 507
    • 508
    • 509
    • 510
    • 511
    • 512
    • 513
    • 514
    • 515
    • 516
  • 相关阅读:
    大二学生web期末大作业 在线电影网站 HTML+CSS+JS
    Python爬取各大外包网站需求
    Nginx基础02:配置文件nginx.conf(Part1)
    Java System.gc()具有什么功能呢?
    【多线程】CAS 详解
    数据湖仓一体(一) 编译hudi
    SpringBoot项目使用jasypt加解密
    Linux 内核irq_stack遍历
    React 补充
    SpringBoot 3.0 来啦!
  • 原文地址:https://blog.csdn.net/sinat_28071063/article/details/125393621