Bootstrap

React中使用echarts写出3d旋转扇形图

效果

技术

 React + TypeScript + Less + Echarts

代码块

import * as echarts from "echarts";
import React, { useEffect, useRef } from "react";
import "echarts-gl";
import "./index.less";

const LeftEcharts = () => {
    const chartDom = useRef(null);

    useEffect(() => {
        const myChart = echarts.init(chartDom.current);
        // 数据源
        const optionsData: any = [
            {
                name: "IT运营管控团队",
                value: 1000,
                itemStyle: {
                    color: "#dd4b3d",
                },
            },
            {
                name: "业务支撑团队",
                value: 600,
                itemStyle: {
                    color: "#dd9c3c",
                },
            },
            {
                name: "计费结算团队",
                value: 900,
                itemStyle: {
                    color: "#f6bb50",
                },
            },
            {
                name: "数据应用运营团队",
                value: 800,
                itemStyle: {
                    color: "#5ec7f8",
                },
            },
            {
                name: "Paas组件运营团队",
                value: 400,
                itemStyle: {
                    color: "#31dda1",
                },
            },
            {
                name: "云数安全团队",
                value: 300,
                itemStyle: {
                    color: "#637aff",
                },
            },
        ];

        // 生成扇形的曲面参数方程,用于 series-surface.parametricEquation
        function getParametricEquation(
            startRatio,
            endRatio,
            isSelected,
            isHovered,
            k,
            h
        ) {
            // 计算
            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;
            // }
            isSelected = false;
            // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
            k = typeof k !== "undefined" ? k : 1 / 3;

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

            // 计算高亮效果的放大比例(未高亮,则比例为 1)
            let 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: function (u, v) {
                    if (u < startRadian) {
                        return (
                            offsetX +
                            Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate
                        );
                    }
                    if (u > endRadian) {
                        return (
                            offsetX +
                            Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate
                        );
                    }
                    return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
                },

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

                z: function (u, v) {
                    if (u < -Math.PI * 0.5) {
                        return Math.sin(u);
                    }
                    if (u > Math.PI * 2.5) {
                        return Math.sin(u) * h * 0.1;
                    }
                    return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
                },
            };
        }

        // 生成模拟 3D 饼图的配置项
        function getPie3D(pieData: any, internalDiameterRatio) {
            let series: any = [];
            let sumValue = 0;
            let startValue = 0;
            let endValue = 0;
            let legendData: any = [];
            let k =
                typeof internalDiameterRatio !== "undefined"
                    ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio)
                    : 1 / 3;
            // 为每一个饼图数据,生成一个 series-surface 配置
            for (let i = 0; i < pieData.length; i++) {
                sumValue += pieData[i].value;
                let 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: 1 / 10,
                    },
                };
                if (typeof pieData[i].itemStyle != "undefined") {
                    let itemStyle: any = {};
                    typeof pieData[i].itemStyle.color != "undefined" ? (itemStyle.color = pieData[i].itemStyle.color) : null;
                    typeof pieData[i].itemStyle.opacity != "undefined" ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : null;
                    seriesItem.itemStyle = itemStyle;
                }
                series.push(seriesItem);
            }
            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,
                    k,
                    series[i].pieData.value
                );
                startValue = endValue;
                legendData.push(series[i].name);
            }
            return series;
        }

        const series: any = getPie3D(optionsData, 0.6);
        series.push({
            name: "pie2d",
            type: "pie",
            label: {
                opacity: 1,
                fontSize: 14,
                lineHeight: 20,
                textStyle: {
                    fontSize: 14,
                    color: "#fff",
                },
                show: false,
                position: "center",
            },
            labelLine: {
                length: 10,
                length2: 10,
                show: false,
            },
            startAngle: 2, //起始角度,支持范围[0, 360]。
            clockwise: false, //饼图的扇区是否是顺时针排布。上述这两项配置主要是为了对齐3d的样式
            radius: ["50%", "60%"],
            center: ["62%", "50%"],
            data: optionsData,
            itemStyle: {
                opacity: 0,
            },
        });
        // 准备待返回的配置项,把准备好的 legendData、series 传入。
        const option = {
            legend: {
                show: true, // 显示图例
                tooltip: {
                    show: true, // 显示图例的提示信息
                },
                orient: "vertical", // 图例的排列方向
                data: ["IT运营管控团队", "业务支撑团队", "计费结算团队", "数据应用运营团队", "Paas组件运营团队", '云数安全团队'], // 图例的内容
                top: 20, // 图例距离顶部的距离
                itemGap: 10, // 图例项之间的间距
                itemHeight: 20, // 图例项的高度
                itemWidth: 24, // 图例项的宽度
                right: "5%", // 图例距离右边的距离
                textStyle: { // 图例的文本样式
                    color: "#fff", // 文本颜色
                    fontSize: 10, // 文本字体大小
                    rich: {
                        name: {
                            width: 60, // 名称部分的宽度
                            fontSize: 14, // 名称部分字体大小
                            color: "#B0D8DF", // 名称部分颜色
                            fontFamily: "Source Han Sans CN", // 名称部分字体
                        },
                        value: {
                            width: 50, // 数值部分的宽度
                            fontSize: 4, // 数值部分字体大小
                            padding: [0, 5, 0, 5], // 数值部分的内边距
                            color: "#fff", // 数值部分颜色
                            fontFamily: "Source Han Sans CN", // 数值部分字体
                        },
                        A: {
                            fontSize: 20, // A部分的字体大小
                            color: "#B0D8DF", // A部分颜色
                            fontFamily: "Source Han Sans CN", // A部分字体
                        },
                        rate: {
                            width: 60, // 比率部分的宽度
                            fontSize: 14, // 比率部分字体大小
                            padding: [0, 5, 0, 10], // 比率部分的内边距
                            color: "#579ed2", // 比率部分颜色
                            fontFamily: "Source Han Sans CN", // 比率部分字体
                        },
                    },
                },
                formatter: function (name) { // 格式化图例项的显示内容
                    let total = 0; // 总值
                    let target; // 目标值
                    for (let i = 0; i < optionsData.length; i++) {
                        total += optionsData[i].value; // 计算总值
                        if (optionsData[i].name === name) { // 查找目标值
                            target = optionsData[i].value;
                        }
                    }
                    let arr = [
                        "{name|" + name + "}", // 名称
                        "{value|" + "}", // 数值(未赋值需补充)
                        "{rate|" + ((target / total) * 100).toFixed(1) + "%}", // 比率
                    ];
                    return arr.join(""); // 返回格式化后的字符串
                },
            },
            animation: true, // 开启动画效果
            tooltip: {
                backgroundColor: "rgba(64, 180, 176, 0.6)", // 提示框的背景颜色
                borderColor: "rgba(64, 180, 176, 0.6)", // 提示框的边框颜色
                textStyle: {
                    color: "#fff", // 提示文本颜色
                    fontSize: 24, // 提示文本字体大小
                },
                formatter: (params) => { // 格式化提示框内容
                    if (
                        params.seriesName !== "mouseoutSeries" &&
                        params.seriesName !== "pie2d" // 排除特定系列
                    ) {
                        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 + "万人"
                            }`; // 返回系列名称和数值
                    }
                },
            },
            labelLine: {
                show: true, // 显示标签连接线
                lineStyle: {
                    color: "#7BC0CB", // 标签连接线颜色
                },
                normal: {
                    show: true, // 正常状态显示
                    length: 10, // 连接线长度
                    length2: 10, // 连接线第二段长度
                },
            },
            label: {
                show: true, // 显示标签
                position: "outside", // 标签位置
                formatter: "{b} \n{c}\n{d}%", // 标签格式
                textStyle: {
                    color: "rgba(176, 216, 223, 1)", // 标签文本颜色
                    fontSize: 24, // 标签字体大小
                },
            },
            xAxis3D: {
                min: -1, // x轴最小值
                max: 1, // x轴最大值
            },
            yAxis3D: {
                min: -1, // y轴最小值
                max: 1, // y轴最大值
            },
            zAxis3D: {
                min: -1, // z轴最小值
                max: 1, // z轴最大值
            },
            grid3D: {
                show: false, // 是否显示3D网格
                boxHeight: 1, // 3D盒子的高度
                left: -40, // 3D图形左边距
                top: -10, // 3D图形顶部边距
                width: "50%", // 3D图形宽度
                viewControl: {
                    distance: 280, // 视距
                    alpha: 20, // 视角的俯仰角
                    beta: 15, // 视角的旋转角
                    autoRotate: true, // 是否自动旋转
                    rotateSensitivity: 1, // 旋转灵敏度
                    zoomSensitivity: 0, // 缩放灵敏度
                    panSensitivity: 0, // 平移灵敏度
                },
            },
            series: series, // 数据系列
        };
        

        myChart.setOption(option);
    }, []);

    return (
        <div className='left-echarts'>
            <div className='left-top-nav'>
                团队概况
            </div>
            <div style={{ width: "496px", height: "270px", position: "relative" }}>
                <div ref={chartDom} style={{ width: "100%", height: "100%", zIndex: "5" }}></div>
                <div className="bg"></div>
            </div>
        </div>
    );
};

export default LeftEcharts;

;