Files
MemoryBear/web/src/components/Charts/PieChart.tsx
2026-03-16 14:53:52 +08:00

205 lines
5.8 KiB
TypeScript

/*
* @Author: ZhaoYing
* @Date: 2026-02-10 13:35:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 11:34:30
*/
/*
* PieChart Component
*
* A reusable pie chart component built with ECharts that displays data distribution
* in a donut chart format with customizable colors and responsive behavior.
*
* Features:
* - Donut-style pie chart with percentage labels
* - Customizable color palette
* - Responsive resizing using ResizeObserver
* - Hover tooltips showing percentage values
* - Legend at the bottom with horizontal layout
* - Empty state when no data is available
* - Shadow effects for better visual depth
*/
import { type FC, useEffect, useRef } from 'react'
import ReactEcharts from 'echarts-for-react';
import Empty from '@/components/Empty'
/** Default color palette for pie chart segments */
const Colors = ['#171719', '#155EEF', '#4DA8FF', '#9C6FFF', '#ABEBFF', '#DFE4ED']
/**
* Data structure for each pie chart segment
*
* @interface ChartData
* @property {string} name - Label for the segment (displayed in legend)
* @property {number} value - Numeric value for the segment (determines size)
*/
export interface ChartData {
name: string;
value: number;
}
/**
* Props for the PieChart component
*
* @interface PieChartProps
* @property {ChartData[]} chartData - Array of data points to display in the chart
* @property {number} [height=260] - Height of the chart in pixels
* @property {string[]} [colors] - Custom color array for chart segments (defaults to Colors)
*/
interface PieChartProps {
chartData: ChartData[];
height?: number;
colors?: string[];
itemGap?: number;
seriesWidth?: number;
seriesHeight?: number;
seriesLabel?: boolean;
seriesTop?: number;
}
/**
* PieChart Component
*
* Renders a donut-style pie chart with percentage labels and legend.
* Automatically resizes when container dimensions change.
*
* @param {PieChartProps} props - Component props
* @returns {JSX.Element} Rendered pie chart or empty state
*
* @example
* ```tsx
* <PieChart
* chartData={[
* { name: 'Category A', value: 30 },
* { name: 'Category B', value: 70 }
* ]}
* height={300}
* />
* ```
*/
const PieChart: FC<PieChartProps> = ({
chartData,
height = 260,
seriesWidth = 182,
seriesHeight = 182,
colors = Colors,
itemGap = 48,
seriesLabel = true,
seriesTop = 24,
}) => {
/** Reference to the ECharts instance for programmatic control */
const chartRef = useRef<ReactEcharts>(null);
/** Flag to prevent multiple simultaneous resize operations */
const resizeScheduledRef = useRef(false)
/**
* Set up responsive behavior using ResizeObserver
* Resizes chart when parent container dimensions change
*/
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
// Use requestAnimationFrame for smooth resize performance
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
// Cleanup: disconnect observer when component unmounts
return () => {
resizeObserver.disconnect()
}
}, [chartData])
return (
<div style={{ height: `${height}px` }}>
{chartData && chartData.length > 0
? <ReactEcharts
ref={chartRef}
option={{
color: colors,
tooltip: {
trigger: 'item',
textStyle: {
color: '#5B6167',
fontSize: 12,
width: 27,
height: 16,
},
formatter: '{d}%',
padding: [8, 5],
backgroundColor: '#FFFFFF',
borderColor: '#DFE4ED',
extraCssText: 'width: 36px; height: 36px; box-shadow: 0px 2px 4px 0px rgba(33,35,50,0.12);border-radius: 36px;'
},
legend: {
bottom: 0,
padding: 0,
itemWidth: 12,
itemHeight: 12,
borderRadius: 2,
orient: 'horizontal',
itemGap: itemGap,
textStyle: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 16,
}
},
series: [
{
type: 'pie',
radius: ['60%', '100%'],
avoidLabelOverlap: false,
percentPrecision: 0,
padAngle: 1,
width: seriesWidth,
height: seriesHeight,
left: 'center',
top: seriesTop,
itemStyle: {
borderRadius: 2,
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 2,
shadowColor: 'rgba(0,0,0,0.25)',
},
label: {
show: seriesLabel,
fontWeight: 'bold',
color: '#171719',
formatter: '{d}%',
fontFamily: 'MiSans-Demibold',
},
labelLine: {
lineStyle: {
color: '#DFE4ED'
}
},
data: chartData
}
]
}}
style={{ height: `${height}px`, width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
opts={{ renderer: 'canvas' }}
notMerge={true}
lazyUpdate={true}
/>
: <Empty size={120} className="rb:h-full!" />
}
</div>
)
}
export default PieChart