轮播的3D饼图,效果如下
Echarts效果图
有这方面需求的朋友肯定有在Echarts社区上找过相关3D饼图的方案。
大同小异,所有3D饼图的实现方式,基本上使用了surface曲面的原理,我也是在这个基础上进行的二开。
他的核心代码是使用surface的parametricEquation属性,为每一项数据生成了一个扇形的曲面参数方程,然后生成了不同的曲面,这里的方程完全看不懂,没关系,直接拿来用就行了。
function getParametricEquation( startRatio: any, endRatio: any, isSelected: any, isHovered: any, k: any, h: any ) { // 计算 const midRatio = (startRatio + endRatio) / 2; const startRadian = startRatio * Math.PI * 2; const endRadian = endRatio * Math.PI * 2; const midRadian = midRatio * Math.PI * 2; // 如果只有一个扇形,则不实现选中效果。 if (startRatio === 0 && endRatio === 1) { // eslint-disable-next-line no-param-reassign isSelected = false; } // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3) // eslint-disable-next-line no-param-reassign k = typeof k !== "undefined" ? k : 1 / 3; // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0) const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0; const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0; // 计算高亮效果的放大比例(未高亮,则比例为 1) // const hoverRate = isHovered ? 1.05 : 1; // 返回曲面参数方程 return { u: { min: -Math.PI, max: Math.PI * 3, step: Math.PI / 32, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x(u: any, v: any) { if (u < startRadian) { return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k); } return offsetX + Math.cos(u) * (1 + Math.cos(v) * k); }, y(u: any, v: any) { if (u < startRadian) { return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k); } return offsetY + Math.sin(u) * (1 + Math.cos(v) * k); }, z(u: any, v: any) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * h * 0.1; } // 当前图形的高度是Z根据h(每个value的值决定的) return Math.sin(v) > 0 ? 1 * h * 0.1 : -1; }, }; }
复制
强调一下:我的需求是个轮播,只需要当前这部分“高”起来,其他的部分统一高度即可。因此我的getParametricEquation函数的第六个参数h完全可以写死。需要展示不同高度的朋友这里的k就要根据实际情况传了。
接着来说一下轮播的原理
- 定义一个currentIndex变量,用来存放当前“高”的是哪个
let curIndex = 0;
复制
- 获取到Echarts图表的实例,用ref存起来,后期轮播需要setOptions。我这里是React,如果是原生就更好获取了。
const eChartsDom = useRef<any>(); <EChartsReact option={option} style={{ width: "700px", height: "500px"}} ref={(e) => {eChartsDom.current = e;pipeAnimation()}} // 示例获取完就可以执行动画了 />
复制
- 定时器循环currentIndex并setOptions
// 示例获取完执行 const pipeAnimation = async () => { let timer = setInterval(() => { curIndex = curIndex +1 if(curIndex === 5) curIndex = 0 highLight({ seriesIndex: curIndex, seriesName: data[curIndex].name }) }, 2000); }; // 把需要高的那一项的高度h调高,然后setOption const highLight = (params: any)=>{ let myChart = eChartsDom.current.getEchartsInstance(); let isSelected; let isHovered; let startRatio; let endRatio; let k; // 如果触发 mouseover 的扇形当前已高亮,则不做操作 if (hoveredIndex === params.seriesIndex) { return; // 否则进行高亮及必要的取消高亮操作 } else { // 如果当前有高亮的扇形,取消其高亮状态(对 option 更新) if (hoveredIndex !== "") { // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。 isSelected = option.series[hoveredIndex].pieStatus.selected; isHovered = false; startRatio = option.series[hoveredIndex].pieData.startRatio; endRatio = option.series[hoveredIndex].pieData.endRatio; k = option.series[hoveredIndex].pieStatus.k; // 取消之前高的 option.series[hoveredIndex].parametricEquation = getParametricEquation( startRatio, endRatio, isSelected, isHovered, k, 30 ); option.series[hoveredIndex].pieStatus.hovered = isHovered; // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空 hoveredIndex = ""; } // 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新) if (params.seriesName !== "mouseoutSeries") { // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。 isSelected = option.series[params.seriesIndex].pieStatus.selected; // isHovered = true; startRatio = option.series[params.seriesIndex].pieData.startRatio; endRatio = option.series[params.seriesIndex].pieData.endRatio; k = option.series[params.seriesIndex].pieStatus.k; // 在这里的一项调高了 option.series[params.seriesIndex].parametricEquation = getParametricEquation( startRatio, endRatio, isSelected, isHovered, k, 80 ); option.series[params.seriesIndex].pieStatus.hovered = isHovered; // 记录上次高亮的扇形对应的系列号 seriesIndex hoveredIndex = params.seriesIndex; } // 使用更新后的 option,渲染图表 myChart.setOption(option); } }
复制
PS:参考的Demo,它的Echarts版本比较高,没有在低版本上测试过,不过理论上来说是可行的。
OKK,这样一个简单的3D的轮播饼图的Demo就出来了。
放一下这个Demo的代码,一些点击事件、Label没有再开发,而且代码没有整理很杂乱,见谅!
/* * @Author: * @Date: 2022-08-06 07:46:56 * @LastEditors: atwLee * @LastEditTime: 2022-08-07 01:02:21 * @FilePath: /piethreed/src/pieThreeD.tsx * @Description: */ import React, { useRef } from "react"; import type { EChartsOption } from "echarts"; import EChartsReact from "echarts-for-react"; import "echarts-gl"; function PieThreeD() { let selectedIndex = ""; let hoveredIndex = ""; let curIndex = 0; let data = [ { name: "cc", value: 2, itemStyle: { color: "#f77b66", }, }, { name: "aa", value: 1, itemStyle: { color: "#3edce0", }, }, { name: "bb", value: 1, itemStyle: { color: "#f94e76", }, }, { name: "ee", value: 1, itemStyle: { color: "#018ef1", }, }, { name: "dd", value: 1, itemStyle: { color: "#9e60f9", }, }, ]; let option = getPie3D( data, 0.59 ); // 生成扇形的曲面参数方程 function getParametricEquation( startRatio: any, endRatio: any, isSelected: any, isHovered: any, k: any, h: any ) { // 计算 const midRatio = (startRatio + endRatio) / 2; const startRadian = startRatio * Math.PI * 2; const endRadian = endRatio * Math.PI * 2; const midRadian = midRatio * Math.PI * 2; // 如果只有一个扇形,则不实现选中效果。 if (startRatio === 0 && endRatio === 1) { // eslint-disable-next-line no-param-reassign isSelected = false; } // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3) // eslint-disable-next-line no-param-reassign k = typeof k !== "undefined" ? k : 1 / 3; // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0) const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0; const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0; // 计算高亮效果的放大比例(未高亮,则比例为 1) // const hoverRate = isHovered ? 1.05 : 1; // 返回曲面参数方程 return { u: { min: -Math.PI, max: Math.PI * 3, step: Math.PI / 32, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x(u: any, v: any) { if (u < startRadian) { return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k); } return offsetX + Math.cos(u) * (1 + Math.cos(v) * k); }, y(u: any, v: any) { if (u < startRadian) { return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k); } return offsetY + Math.sin(u) * (1 + Math.cos(v) * k); }, z(u: any, v: any) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * h * 0.1; } // 当前图形的高度是Z根据h(每个value的值决定的) return Math.sin(v) > 0 ? 1 * h * 0.1 : -1; }, }; } // 生成模拟 3D 饼图的配置项 function getPie3D(pieData: any, internalDiameterRatio: any) { const series: any = []; // 总和 let sumValue = 0; let startValue = 0; let endValue = 0; const legendData = []; const k = typeof internalDiameterRatio !== "undefined" ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3; // 为每一个饼图数据,生成一个 series-surface 配置 for (let i = 0; i < pieData.length; i += 1) { sumValue += pieData[i].value; const seriesItem: any = { name: typeof pieData[i].name === "undefined" ? `series${i}` : pieData[i].name, type: "surface", parametric: true, wireframe: { show: false, }, pieData: pieData[i], pieStatus: { selected: false, hovered: false, k, }, }; if (typeof pieData[i].itemStyle !== "undefined") { const { itemStyle } = pieData[i]; // eslint-disable-next-line @typescript-eslint/no-unused-expressions typeof pieData[i].itemStyle.color !== "undefined" ? (itemStyle.color = pieData[i].itemStyle.color) : null; // eslint-disable-next-line @typescript-eslint/no-unused-expressions typeof pieData[i].itemStyle.opacity !== "undefined" ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : null; seriesItem.itemStyle = itemStyle; } series.push(seriesItem); } // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数, // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。 for (let i = 0; i < series.length; i += 1) { endValue = startValue + series[i].pieData.value; series[i].pieData.startRatio = startValue / sumValue; series[i].pieData.endRatio = endValue / sumValue; series[i].parametricEquation = getParametricEquation( series[i].pieData.startRatio, series[i].pieData.endRatio, false, false, k, // 我这里做了一个处理,使除了第一个之外的值都是10 30 ); startValue = endValue; legendData.push(series[i].name); } // 准备待返回的配置项,把准备好的 legendData、series 传入。 const option = { // animation: false, tooltip: { show: false, formatter: (params: any) => { if (params.seriesName !== "mouseoutSeries") { return `${ params.seriesName }<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${ params.color };"></span>${option.series[params.seriesIndex].pieData.value}`; } return ""; }, }, xAxis3D: { min: -1, max: 1, }, yAxis3D: { min: -1, max: 1, }, zAxis3D: { min: -1, max: 1, }, grid3D: { show: false, boxHeight: 5, top: "-20%", viewControl: { // 3d效果可以放大、旋转等,请自己去查看官方配置 alpha: 35, // beta: 30, rotateSensitivity: 1, zoomSensitivity: 0, panSensitivity: 0, autoRotate: true, distance: 150, }, // 后处理特效可以为画面添加高光、景深、环境光遮蔽(SSAO)、调色等效果。可以让整个画面更富有质感。 postEffect: { // 配置这项会出现锯齿,请自己去查看官方配置有办法解决 enable: false, bloom: { enable: true, bloomIntensity: 0.1, }, SSAO: { enable: true, quality: "medium", radius: 2, }, // temporalSuperSampling: { // enable: true, // }, }, }, series, }; return option; } const highLight = (params: any)=>{ console.log('params',params) let myChart = eChartsDom.current.getEchartsInstance(); let isSelected; let isHovered; let startRatio; let endRatio; let k; // 如果触发 mouseover 的扇形当前已高亮,则不做操作 if (hoveredIndex === params.seriesIndex) { return; // 否则进行高亮及必要的取消高亮操作 } else { // 如果当前有高亮的扇形,取消其高亮状态(对 option 更新) if (hoveredIndex !== "") { // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。 isSelected = option.series[hoveredIndex].pieStatus.selected; isHovered = false; startRatio = option.series[hoveredIndex].pieData.startRatio; endRatio = option.series[hoveredIndex].pieData.endRatio; k = option.series[hoveredIndex].pieStatus.k; // 对当前点击的扇形,执行取消高亮操作(对 option 更新) option.series[hoveredIndex].parametricEquation = getParametricEquation( startRatio, endRatio, isSelected, isHovered, k, 30 ); option.series[hoveredIndex].pieStatus.hovered = isHovered; // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空 hoveredIndex = ""; } // 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新) if (params.seriesName !== "mouseoutSeries") { // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。 isSelected = option.series[params.seriesIndex].pieStatus.selected; // isHovered = true; startRatio = option.series[params.seriesIndex].pieData.startRatio; endRatio = option.series[params.seriesIndex].pieData.endRatio; k = option.series[params.seriesIndex].pieStatus.k; // 对当前点击的扇形,执行高亮操作(对 option 更新) option.series[params.seriesIndex].parametricEquation = getParametricEquation( startRatio, endRatio, isSelected, isHovered, k, 80 ); option.series[params.seriesIndex].pieStatus.hovered = isHovered; // 记录上次高亮的扇形对应的系列号 seriesIndex hoveredIndex = params.seriesIndex; } // 使用更新后的 option,渲染图表 myChart.setOption(option); } } const onEvents = { // mouseover: (params:any) => highLight(params) }; const eChartsDom = useRef<any>(); const pipeAnimation = async () => { let timer = setInterval(() => { curIndex = curIndex +1 if(curIndex === 5) curIndex = 0 highLight({ seriesIndex: curIndex, seriesName: data[curIndex].name }) }, 2000); }; return ( <div> <EChartsReact option={option} style={{ width: "700px", height: "500px", margin: "auto" }} onEvents={onEvents} ref={(e) => {eChartsDom.current = e;pipeAnimation()}} /> </div> ); } export default PieThreeD;
复制
HighCharts效果图
HighCharts其实是渲染了很多SVG,给人一种视觉上的3D。
相对于Echarts,他的饼图自带有3D属性,更易理解,例子。
实现原理与Echarts大同小异,都是一个currentIndex,来定时循环setOption。不过HC的文档不如EC易读。
说一下开发过程中遇到的问题吧
- 3D饼图有个重渲染数据下沉的bug,我也遇到了
解决方法:找到了个大神的代码完美解决 - 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;
复制