使用Highcharts创建3D环形图

引言

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 moduleResolutiontarget 不一致,会出现类型提示异常但能运行;务必保持 tsconfig 与 bundler 配置一致。
    • 为什么新版更偏向 CJS:Highcharts 的部分功能模块(如 highcharts/highcharts-3d)以 UMD/CJS 形式发布,保持与旧版 bundler、Node 环境以及浏览器直引的最大兼容;多数构建工具会优先命中包的 exportsrequire 分支,使默认导出函数(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,两者在解析与条件导出上存在差异。
    • 实战建议:
      • 统一 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 写法。
  • 类型定义与 JSX:
    • 在 React + TS 项目中,若使用 HighchartsReact 且 JSX 类型不兼容,可改为 React.createElement 或直接 Highcharts.chart 方式以规避。

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()
  • 异步坑点:
    • 初次数据为空,后续更新时若组件卸载重挂,需先 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.centerseries.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=40crookDistance='45%'
    • 中心标题用 useHTML 输出,并拆解为两行(总成本 与金额 --/xxx元);
    • 动态数据更新时,销毁旧实例再创建,避免偏移与堆叠;
    • 上传下载逻辑与图表刷新在 hook 中解耦,实现稳态渲染。
  • 效果:在不同数据规模与容器布局下,图心稳定、颜色一致、标签清晰,运营展示可读性显著提升。

结语

Highcharts 的 3D 环形图兼具表现力与稳定性。生产落地的关键在于:正确初始化 3D 模块、保证容器尺寸稳定、强制图心与半径、为数据点绑定颜色、合理管理生命周期与更新流程。结合本文的实战代码与优化建议,你可以快速在任意页面构建高质量的 3D Donut。进一步可尝试主题切换、渐变色、动效过渡以及与其他图表的联动,释放更多可视化潜力。

相关推荐
我的div丢了肿么办1 小时前
js中async和await 的详细讲解
前端·javascript·vue.js
程序员小寒2 小时前
前端性能优化之CSS篇
前端·css·性能优化
种时光的人2 小时前
关于人人开源框架renren-fast-vue前端npm install安装报错的问题解决方法
前端·vue.js·npm
Z***25802 小时前
React增强现实案例
前端·react.js·ar
IT_陈寒3 小时前
SpringBoot 3.2 性能优化全攻略:7个让你的应用提速50%的关键技巧
前端·人工智能·后端
普通码农3 小时前
Vue-Konva 使用(缩放 / 还原 / 拖动) 示例
前端·javascript·vue.js
renxhui3 小时前
Flutter 布局 ↔ Android XML 布局 对照表(含常用属性)
前端
俺叫啥好嘞4 小时前
日志输出配置
java·服务器·前端
一 乐4 小时前
运动会|基于SpingBoot+vue的高校体育运动会管理系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·学习·springboot