引言
Highcharts 是一个功能完备、生态成熟的前端图表库,拥有良好的类型定义与主题生态。而 Highcharts 的 3D 饼图能力(3D Donut)可以进一步呈现层次与空间感,适合用于大屏可视化与运营展示场景。
使用流程
准备工作
- 安装依赖:
highcharts(核心库)highcharts/highcharts-3d(3D 模块)- 可选:
highcharts-react-official(React 包装组件,后文会解释它与原生div方式的差异与坑位)
bash
yarn add highcharts highcharts-react-official
- 引入与初始化 3D 模块(注意路径与版本兼容):
ts
import Highcharts from 'highcharts';
import highcharts3d from 'highcharts/highcharts-3d';
// 初始化 3D 能力(必须调用)
highcharts3d(Highcharts);
说明:部分文章或旧版本文档使用
highcharts/modules/3d,在现代构建工具(Webpack 5/Vite)或较新版本下可能出现构建失败或类型不匹配,推荐使用highcharts/highcharts-3d路径。
创建第一个 3D 环形图
- HTML 容器(单个):
html
<div id="dought" style="height:300px;width:100%"></div>
- JavaScript 初始化(最小可运行示例):
ts
import Highcharts from 'highcharts';
import highcharts3d from 'highcharts/highcharts-3d';
highcharts3d(Highcharts);
const options: Highcharts.Options = {
chart: { type: 'pie', options3d: { enabled: true, alpha: 45 }, backgroundColor: 'transparent' },
title: { text: '总成本', align: 'center', verticalAlign: 'middle', floating: true, useHTML: true },
plotOptions: {
pie: {
innerSize: '65%',
depth: 12,
center: ['50%', '50%'],
size: '90%',
dataLabels: { enabled: true, format: '{point.name}: {point.y}元' },
},
},
series: [{ type: 'pie', name: '金额', data: [ ['成本', 1200], ['收益', 250], ['2收益', 158] ] }],
};
Highcharts.chart('dought', options);
注意事项与踩坑指南
版本兼容性问题
- 模块路径差异:
- 旧文档常见
highcharts/modules/3d,但在部分 bundler/类型定义配置下会报错或打包失败。 - 推荐:
import highcharts3d from 'highcharts/highcharts-3d'; highcharts3d(Highcharts);
- 旧文档常见
- ESM/CJS 混用:
- 在 Vite/webpack5 环境下,若 TS
moduleResolution或target不一致,会出现类型提示异常但能运行;务必保持tsconfig与 bundler 配置一致。 - 为什么新版更偏向 CJS:Highcharts 的部分功能模块(如
highcharts/highcharts-3d)以 UMD/CJS 形式发布,保持与旧版 bundler、Node 环境以及浏览器直引的最大兼容;多数构建工具会优先命中包的exports中require分支,使默认导出函数(highcharts3d)在 CJS 路径下更稳定,避免 ESM/默认导出互操作导致的undefined或类型不匹配。 - ESM 与 CJS 的核心区别(速查):
- 语法:ESM 使用
import/export(静态、可树摇),CJS 使用require/module.exports(动态、不可树摇)。 - 加载:ESM 静态依赖解析,利于 bundler 优化;CJS 运行时解析,允许条件/动态
require。 - 默认导出互操作:CJS 引入 ESM 时常见
require('pkg').default差异;ESM 引入 CJS 时需留意命名/默认导出包装。 - 环境支持:浏览器与现代 bundler更偏 ESM;Node 仍广泛支持 CJS,两者在解析与条件导出上存在差异。
- 语法:ESM 使用
- 实战建议:
- 统一
tsconfig.json与 bundler 的模块设置(如module: 'ESNext'、moduleResolution: 'bundler'或与工程一致的值),降低互操作差异。 - 对 Highcharts 3D 模块使用"安全默认写法":
import highcharts3d from 'highcharts/highcharts-3d'; highcharts3d(Highcharts);,该写法在 ESM/CJS 两种入口下都能正常工作。 - 如确需纯 CJS:
const Highcharts = require('highcharts'); const highcharts3d = require('highcharts/highcharts-3d'); highcharts3d(Highcharts);,但在 React+TS 项目里更推荐前述 ESM 写法。
- 统一
- 在 Vite/webpack5 环境下,若 TS
- 类型定义与 JSX:
- 在 React + TS 项目中,若使用
HighchartsReact且 JSX 类型不兼容,可改为React.createElement或直接Highcharts.chart方式以规避。
- 在 React + TS 项目中,若使用
3. 为什么使用 highcharts-react-official 渲染会出现饼图偏移,改用 div 就解决了?原理是什么
- 现象:
- 使用
HighchartsReact在某些布局(例如父容器display:flex、存在transform、或首次渲染时容器宽度尚未稳定)下,3D 饼图的中心点计算(依赖容器的实际尺寸与plotLeft/plotTop)可能产生偏移。
- 使用
- 原因:
HighchartsReact封装层在componentDidMount/useEffect阶段创建图表,有时容器尚未完成布局或尺寸为 0,之后再 reflow 会导致 3D 透视与center/size百分比计算出现"错位"。- 包装层会额外多一层
div,在包含position/transform的父级样式下,Highcharts 对getBoundingClientRect的读取与 3D 计算(alpha透视)可能受影响,中心与标签的像素对齐误差被放大。
- 解决:
- 改用原生
Highcharts.chart(containerId, options),保证仅一个容器层;在 React 中于useEffect里创建与销毁,确保容器尺寸稳定后再渲染。 - 若仍需使用
HighchartsReact:- 显式传
containerProps={{ style: { width: '100%', height: <固定高度>, position: 'relative', overflow: 'visible' } }}。 - 在外层布局稳定后再渲染组件,或在首次渲染后调用
chart.reflow()。 - 明确设置
plotOptions.pie.center = ['50%', '50%']与size = '90%',并尽量避免父级transform。
- 显式传
- 改用原生
数据加载与更新
- 动态加载:
- 在 React 中,使用
useEffect监听数据变化,调用chart.update({ series: [{ data }] }, false)再chart.redraw()。
- 在 React 中,使用
- 异步坑点:
- 初次数据为空,后续更新时若组件卸载重挂,需先
destroy()再重新chart(...),避免多实例叠加。 - 数据类型混用(字符串/数字)会导致格式化异常:统一在入参处做
Number(value) || 0的处理。
- 初次数据为空,后续更新时若组件卸载重挂,需先
性能优化
- 3D 饼图不适合超大量数据,建议控制项数(< 12)。
- 关闭不必要的动画与阴影;必要时降低
alpha或禁用后处理效果(在 Highcharts 3D 中主要与视觉相关)。 - 合理设置
dataLabels:- 使用两段引导线并控制长度,避免过密导致重叠。
- 小值可在
formatter中阈值过滤或缩短文本。
深入探讨:配置项详解
内置配置项
chart.options3d.enabled/alpha:开启 3D 与倾角,过大倾角会放大偏移与重叠问题。plotOptions.pie.innerSize:环形图的内径大小,百分比字符串更易与容器自适应配合。plotOptions.pie.depth:3D 厚度,过大可能导致遮挡。series[n].center:强制图心坐标(百分比数组),可稳定对齐。series[n].size:半径,百分比更利于自适应布局。dataLabels.format/useHTML/formatter:标签文本与 HTML 自定义;useHTML可实现两行样式。
自定义配置(项目实践)
- 透明度:
- 利用
Highcharts.color(color).setOpacity(sliceOpacity).get()为每个扇形生成半透明色,视觉更柔和。
- 利用
- 两段引导线:
plotOptions.pie.softConnector = true启用两段式;dataLabels.distance控制第二段长度;dataLabels.crookDistance控制第一段拐点位置(可以传百分比字符串);connectorColor/connectorWidth控制线条颜色与粗细。
- 居中与图例:
legend.enabled = true+center: ['50%', '50%']+size: '90%',确保图例与图形协调摆放。
案例代码(React 组件版,使用原生 div 渲染)
tsx
import Highcharts from 'highcharts';
import highcharts3d from 'highcharts/highcharts-3d';
import React from 'react';
highcharts3d(Highcharts);
const HCDonut3DChart: React.FC<any> = (props) => {
const {
dataList = [],
title = '',
innerSize = '65%',
sliceHeightRange = [8, 20],
depth,
colors,
containerId = 'dought',
sliceOpacity = 0.85,
labelDistance = 40,
connectorCrookDistance = '40%',
connectorColor = '#999',
connectorWidth = 1,
labelUseHTML = true,
labelNameColor = '#6A7570',
labelValueColor = '#6A7570',
labelNameFontSize = 12,
labelValueFontSize = 12,
} = props || {};
const [minH, maxH] = sliceHeightRange || [8, 20];
const computedDepth = typeof depth === 'number' ? depth : Math.max(1, Math.round((minH + maxH) / 2));
const seriesData = React.useMemo(() => {
const list = Array.isArray(dataList) ? dataList : [];
return list.map((item: any, idx: number) => {
const { name, value } = item || {};
const y = Number(value) || 0;
const color = Array.isArray(colors) && colors.length > 0 ? colors[idx % colors.length] : undefined;
const fillColor = color ? (Highcharts as any).color?.(color)?.setOpacity(sliceOpacity)?.get?.() || color : undefined;
return fillColor ? { name, y, color: fillColor } : { name, y };
});
}, [dataList, colors, sliceOpacity]);
const options = React.useMemo(() => {
const nameStyle = `color:${labelNameColor};font-size:${labelNameFontSize}px;font-weight:400;line-height:18px;`;
const valueStyle = `color:${labelValueColor};font-size:${labelValueFontSize}px;font-weight:400;line-height:18px;`;
return {
chart: { type: 'pie', options3d: { enabled: true, alpha: 45 }, backgroundColor: 'transparent' },
title: { text: title, useHTML: true, align: 'center', floating: true, verticalAlign: 'middle', y: 0 },
legend: { enabled: true, layout: 'horizontal', align: 'center', verticalAlign: 'bottom' },
credits: { enabled: false },
tooltip: { headerFormat: '', pointFormat: '{point.name}: <b>{point.y}元</b>' },
plotOptions: {
pie: {
innerSize,
depth: computedDepth,
allowPointSelect: false,
softConnector: true,
showInLegend: true,
dataLabels: {
enabled: true,
useHTML: labelUseHTML,
formatter: function () {
const p: any = (this as any).point || {};
const n = p.name ?? '';
const y = typeof p.y === 'number' ? p.y : Number(p.y) || 0;
return `<div><div style="${nameStyle}">${n}</div><div style="${valueStyle}">${y}元</div></div>`;
},
style: { textOutline: 'none' },
distance: labelDistance,
crookDistance: connectorCrookDistance,
connectorColor,
connectorWidth,
},
},
},
series: [{ type: 'pie', name: '金额', data: seriesData, center: ['50%', '50%'], size: '90%' } as any],
} as any;
}, [title, innerSize, computedDepth, seriesData, labelUseHTML, labelNameColor, labelValueColor, labelNameFontSize, labelValueFontSize, labelDistance, connectorCrookDistance, connectorColor, connectorWidth]);
const chartInstanceRef = React.useRef<any>(null);
React.useEffect(() => {
if (chartInstanceRef.current) { chartInstanceRef.current.destroy(); chartInstanceRef.current = null; }
chartInstanceRef.current = Highcharts.chart(containerId, options);
return () => { if (chartInstanceRef.current) { chartInstanceRef.current.destroy(); chartInstanceRef.current = null; } };
}, [containerId, options]);
return <div id={containerId} style={{ height: '100%', width: '100%' }} />;
};
export default HCDonut3DChart;
说明:示例中通过解构读取
props,并在必要处做数字归一化,减少不必要类型定义,贴近业务编程习惯。
动态更新与最佳实践
- 更新数据:
ts
chart.update({ series: [{ data: newData }] }, false);
chart.redraw();
- 更新样式(示例:调节两段引导线长度):
ts
chart.update({ plotOptions: { pie: { dataLabels: { distance: 48, crookDistance: '45%' } } } });
- 生命周期管理(React):
- 在
useEffect中创建与销毁;避免多实例与内存泄漏。 - 容器尺寸变化时调用
chart.reflow()。
- 在
踩坑与解决
- 图心偏移:明确设置
series.center与series.size,并在布局稳定后再初始化;改用原生Highcharts.chart可避免包装层带来的测量差异。 - 颜色不匹配:为数据点绑定颜色(而不是直接依赖
options.colors),以防主题覆盖或undefined导致第三方主题读取colors[0]报错。 - 标签拥挤:开启两段引导线并调节长度;小值可在
formatter中阈值过滤。 - 3D 模块未初始化:必须
highcharts3d(Highcharts),否则options3d不生效或报错。 - 类型不兼容:当 TS JSX 配置不一致时,使用
React.createElement或直接Highcharts.chart渲染。
配置项速查(精选)
chart.options3d.enabled/alpha:启用 3D 与倾角。plotOptions.pie.innerSize/depth:环形内径与厚度。plotOptions.pie.softConnector:启用两段式引导线。dataLabels.useHTML/formatter/style:两行标签与样式;textOutline: 'none'去描边更清晰。dataLabels.distance/crookDistance:两段线长度与拐点位置;connectorColor/connectorWidth控线型。series.center/size:居中与自适应半径,强烈建议使用百分比字符串。tooltip.pointFormat:统一金额单位;结合格式化函数确保数值正确。
案例分析:从需求到实现
- 需求:在"日前经济最优/微协同"页面展示"优化总成本组成"与"实际总成本组成",标签两行显示(名称 + 金额),图例与颜色对齐,支持导入导出。
- 实现:
- 颜色映射固定三类(负荷/光伏/储能),为数据点绑定
itemStyle.color; - 两段引导线:
distance=40、crookDistance='45%'; - 中心标题用
useHTML输出,并拆解为两行(总成本与金额--/xxx元); - 动态数据更新时,销毁旧实例再创建,避免偏移与堆叠;
- 上传下载逻辑与图表刷新在 hook 中解耦,实现稳态渲染。
- 颜色映射固定三类(负荷/光伏/储能),为数据点绑定
- 效果:在不同数据规模与容器布局下,图心稳定、颜色一致、标签清晰,运营展示可读性显著提升。
结语
Highcharts 的 3D 环形图兼具表现力与稳定性。生产落地的关键在于:正确初始化 3D 模块、保证容器尺寸稳定、强制图心与半径、为数据点绑定颜色、合理管理生命周期与更新流程。结合本文的实战代码与优化建议,你可以快速在任意页面构建高质量的 3D Donut。进一步可尝试主题切换、渐变色、动效过渡以及与其他图表的联动,释放更多可视化潜力。