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