You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何计算SVG环形图中各路径的旋转角度?

SVG环形图分段旋转角度计算问题

以下代码用于生成SVG环形图的路径:

const getArcPath = (start, end, innerRadius, outerRadius) => {
    const startAngle = start * Math.PI * 2;
    const endAngle = end * Math.PI * 2;
    const x1 = innerRadius * Math.sin(startAngle);
    const y1 = innerRadius * -Math.cos(startAngle);
    const x2 = outerRadius * Math.sin(startAngle);
    const y2 = outerRadius * -Math.cos(startAngle);
    const x3 = outerRadius * Math.sin(endAngle);
    const y3 = outerRadius * -Math.cos(endAngle);
    const x4 = innerRadius * Math.sin(endAngle);
    const y4 = innerRadius * -Math.cos(endAngle);
    const bigArc = end - start >= 0.5;
    const outerFlags = bigArc ? '1 1 1' : '0 0 1';
    const innerFlags = bigArc ? '1 1 0' : '1 0 0';
    return `M ${x1},${y1} L ${x2},${y2} A ${outerRadius} ${outerRadius} ${outerFlags} ${x3},${y3} 
        L ${x4},${y4} A ${innerRadius} ${innerRadius} ${innerFlags} ${x1},${y1} Z`;
};

我需要获取环形图中各分段(红、绿、蓝、紫)的旋转角度,但尝试了网上找到的代码后无法正常工作,核心是没搞懂正确的计算逻辑:

const getRotationAngle = (start, end) => {
    const startAngle = start * 360;
    const endAngle = end * 360; 
    return (startAngle + endAngle) / 2;
};

完整React实现代码

JavaScript

const {useState, useEffect, useMemo} = React;

const PROGRESS_UNIT = 0.01;
const PROGRESS_TIMEOUT = 5;

const getArcPath = (start, end, innerRadius, outerRadius) => {
    const gap = 0.01; // 调整此值控制分段间的间隙
    const startAngle = start * Math.PI * 2;
    const endAngle = (end - gap) * Math.PI * 2; // 调整后的结束角度
    const x1 = innerRadius * Math.sin(startAngle);
    const y1 = innerRadius * -Math.cos(startAngle);
    const x2 = outerRadius * Math.sin(startAngle);
    const y2 = outerRadius * -Math.cos(startAngle);
    const x3 = outerRadius * Math.sin(endAngle);
    const y3 = outerRadius * -Math.cos(endAngle);
    const x4 = innerRadius * Math.sin(endAngle);
    const y4 = innerRadius * -Math.cos(endAngle);
    const bigArc = end - start - gap >= 0.5;
    const outerFlags = bigArc ? '1 1 1' : '0 0 1';
    const innerFlags = bigArc ? '1 1 0' : '1 0 0';

    return {
        path: `M ${x1},${y1} L ${x2},${y2} A ${outerRadius} ${outerRadius} ${outerFlags} ${x3},${y3} 
        L ${x4},${y4} A ${innerRadius} ${innerRadius} ${innerFlags} ${x1},${y1} Z`,
    };
};

const DonutChart = ({ width, height, items, innerRadius, outerRadius }) => {
    const [visiblePart, setVisiblePart] = useState(0);
    const [rotationAngle, setRotationAngle] = useState(0)

    useEffect(() => {
        if (visiblePart < 1) {
            setTimeout(() => setVisiblePart(visiblePart + PROGRESS_UNIT), PROGRESS_TIMEOUT);
        }
    }, [visiblePart]);

    const segments = useMemo(() => {
        const sum = items.reduce((sum, item) => sum + item.value, 0);
        let start = 0;
        return items.map((item) => {
            const delta = (item.value / sum) * visiblePart;
            const { path } = getArcPath(start, start + delta, innerRadius, outerRadius);
            // 计算当前分段的中心旋转角度
            const segmentStartDeg = start * 360;
            const segmentEndDeg = (start + delta) * 360;
            const centerRotation = (segmentStartDeg + segmentEndDeg) / 2;
            start += delta;
            return { ...item, path, rotationAngle: centerRotation };
        });
    }, [items, innerRadius, outerRadius, visiblePart]);
    
    useEffect(() => {
        if (segments.length > 0) {
            setRotationAngle(segments[0].rotationAngle);
        }
    }, [segments]) 

    return (
      <div className='container'>
        <div className='info'>{rotationAngle.toFixed(2)}deg / {segments[0]?.color || ''}</div>
        <div className='pointer'></div>
        <svg width={width} height={height}>
          <g transform={`translate(${width/2},${height/2})`}>
            {segments.map((segment) => (
              <path 
                key={segment.color} 
                stroke={segment.color} 
                fill={segment.color} 
                d={segment.path} 
                {/* 若需单个分段旋转,取消下方注释 */}
                {/* transform={`rotate(${segment.rotationAngle})`} */}
              />
            ))}
          </g>
        </svg>
      </div>
   );
};

const items = [
    {value: 100, color: 'red'},
    {value: 200, color: 'green'},
    {value: 300, color: 'blue'},
    {value: 150, color: 'purple'},
];

ReactDOM.render(<DonutChart width={500} height={300} items={items} innerRadius={100} outerRadius={140} />, document.querySelector("#app"))

CSS

svg {
  border: 1px solid #888;
}

.pointer {
  width: 20px;
  height: 20px;
  border-left: 15px solid transparent;
  border-right: 15px solid transparent;
  border-top: 15px solid #2f2f2f;
  margin: 0 auto;
}

div {
  position: relative;
}
.info {
  margin: 0 auto;
  text-align: center;
  font-weight: bold;
}

.container {
  width: 500px;
}

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id='app' />

问题解决说明

  1. 正确的旋转角度计算逻辑:
    • 每个分段的start和end是基于总数值占比的0-1区间值,乘以360即可转换为对应的角度。
    • 分段的中心旋转角度取起始角度与结束角度的平均值,即(start*360 + (start+delta)*360)/2,这个值就是该分段的中心旋转角度。
  2. 原代码问题分析:
    • 原getRotationAngle函数逻辑本身正确,但之前未正确传递start和end参数(原getArcPath未返回旋转角度,且计算时未结合visiblePart的动画进度)。
  3. 关键修改点:
    • 在生成分段的useMemo逻辑中,直接计算每个分段的中心旋转角度并挂载到分段对象上。
    • 移除了原代码中错误的整体旋转逻辑,若需要单个分段独立旋转,可在对应path标签的transform属性中设置旋转角度。

内容的提问来源于stack exchange,提问作者dinmavr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 08:37:04