Skip to content
Go back

React之svg组件制作环形图

Updated:  at  11:28 PM

学以不仅致用,还要融汇贯通

一直以来,很多人都陷入一个误区,工作中用到的技术方案,搜集各方资料,做出来以后,交付就了事。这种态度只会让自己养成非常不好的做事态度。

本次使用svg来制作环形图动画就是一个例子,要深入了解一下其中的属性和原理,这样在下次遇到类似问题,就可以快速上手,避免再次从零开始。

import { useEffect, useState } from "react";

type RingPieItem = {
	value: number;
	color: string;
	label: string;
	hideLabel?: boolean;
};

export default (props: {
	ringRadius?: number;
	strokeWitdh?: number;
	startAngle?: number;
	patterns: RingPieItem[];
	visible: boolean;
}) => {
	const { ringRadius = 100, strokeWitdh = 4, startAngle = 0, patterns, visible } = props;
	let radius = ringRadius / 2 < strokeWitdh ? ringRadius / 2 - 2 : ringRadius / 2 - strokeWitdh / 2;
	let stroke = ringRadius / 2 < strokeWitdh ? 2 : strokeWitdh;
	const centerPointer = ringRadius / 2;
	const gapPercent = 0.005;
	const gapAngle = gapPercent * 360;
	const total = patterns.reduce((acc, cur) => acc + cur.value, 0);
	const [anim, setAnim] = useState(false);
	useEffect(() => { setAnim(visible) }, [visible]);

	let angle = startAngle - 90;
	let patternsData = patterns.map((item, index) => {
		let preItemPercent = total === 0 ? 0 : (patterns[index - 1] || { value: 0 }).value / total;
		let percent = item.value / total;
		let legendValue = total === 0 ? 0 : Math.floor(item.value / total * 10000) / 100;
		angle += preItemPercent * 360;
		return {
			...item,
			angle,
			percent,
			legendValue,
			preItemPercent,
			initPercent: 0,
			x1: centerPointer + Math.sin((angle - gapAngle / 2) / 180 * Math.PI) * radius,
			y1: centerPointer - Math.cos((angle - gapAngle / 2) / 180 * Math.PI) * radius,
			x2: centerPointer + Math.sin((angle + gapAngle / 2) / 180 * Math.PI) * radius,
			y2: centerPointer - Math.cos((angle + gapAngle / 2) / 180 * Math.PI) * radius,
		};
	});

	let further = false;
	patternsData = patternsData.map((item, index) => {
		let legendValue = item.legendValue;
		if (!further && item.value !== 0) {
			further = true;
			legendValue = (10000 - patternsData.reduce((i, j, k) => i + (index === k ? 0 : j.legendValue * 100), 0)) / 100;
		}
		return { ...item, legendValue }
	})

	return <div style={{ display: 'flex', flexWrap: 'nowrap', justifyContent: 'space-between' }}>
		<div style={{ flexBasis: ringRadius }}>
			<svg style={{ width: ringRadius, height: ringRadius }} viewBox={`0 0 ${ringRadius} ${ringRadius}`}
				version="1.1" xmlns="http://www.w3.org/2000/svg">
				{
					patternsData.map(({ percent, color, angle, initPercent }, index) => {
						return <circle
							key={index}
							cx={centerPointer}
							cy={centerPointer}
							r={radius}
							strokeWidth={stroke}
							stroke={color}
							fill='none'
							style={{ transition: 'stroke-dasharray 0.2s ease-in-out', transform: `rotate(${angle}deg)`, transformOrigin: 'center' }}
							strokeDasharray={`${3.1415 * radius * 2 * (anim ? percent : initPercent)} ${3.1415 * radius * 2}`}
						/>
					})
				}
				{
					anim && patternsData.map(({ x1, x2, y1, y2 }, index) => {
						return <line
							{...{ x1, x2, y1, y2 }}
							key={index}
							stroke="#fff"
							strokeWidth={stroke}
							style={{ position: 'relative', zIndex: 100, transform: `rotate(${90}deg)`, transformOrigin: 'center' }}
						/>
					})
				}
			</svg>
		</div>
		<div className="legend" style={{ transition: 'all 0.35s ease-in-out', opacity: anim ? 1 : 0, transform: `translateX(${anim ? 0 : -8})`, marginTop: -5, display: 'flex', flexDirection: 'column', paddingBottom: 3, justifyContent: 'space-between', marginLeft: '24px' }}>
			{
				patternsData.map((item) => {
					return !item.hideLabel && <div className="legend-item" key={item.label} style={{ height: 18, whiteSpace: 'nowrap' }}>
						<div
							className="legend-item-color"
							style={{ background: item.color, display: 'inline-block', width: 14, height: 8, borderRadius: 2, marginRight: 8 }}
						/>
						<div
							className="legend-item-text"
							style={{ display: 'inline-block', width: 14, textAlign: 'center', color: 'rgba(0, 0, 0, 0.85)', marginRight: 8, fontSize: 12, }}
						>
							{item.label}
						</div>
						<div
							className="legend-item-value"
							style={{ display: 'inline-block', color: 'rgba(0, 0, 0, 0.85)', fontSize: 12, textAlign: 'left' }}
						>
							{item.legendValue}%
						</div>
					</div>
				})
			}
		</div>
	</div >
}


Previous Post
Vue-Router的简易实现
Next Post
Webgl的概念和示例