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

效果

技术

React + TypeScript + Less + Echarts

代码块

javascript 复制代码
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;
相关推荐
xjt_0901几秒前
浅析Web存储系统
前端
foxhuli22938 分钟前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
青皮桔1 小时前
CSS实现百分比水柱图
前端·css
DataGear1 小时前
如何在DataGear 5.4.1 中快速制作SQL服务端分页的数据表格看板
javascript·数据库·sql·信息可视化·数据分析·echarts·数据可视化
影子信息1 小时前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
青阳流月1 小时前
1.vue权衡的艺术
前端·vue.js·开源
样子20181 小时前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿1 小时前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js
孤水寒月2 小时前
给自己网站增加一个免费的AI助手,纯HTML
前端·人工智能·html
CoderLiu2 小时前
用这个MCP,只给大模型一个figma链接就能直接导出图片,还能自动压缩上传?
前端·llm·mcp