import { RefProps, VNode, bindGlobalStyle, getGlobalStylesId } from 'lupine.web'; import { chartCommonCss, getChartColor, describeArc, BasicChartProps } from './chart-utils'; import { Tooltip } from '../tooltip'; export type PieChartProps = BasicChartProps & { innerRadiusRatio?: number; // 0 for Pie, > 0 for Donut (e.g., 0.6) }; export const PieChart = (props: PieChartProps) => { const globalCssId = getGlobalStylesId(chartCommonCss); bindGlobalStyle(globalCssId, chartCommonCss); const showLegend = props.showLegend !== false; const innerRadiusRatio = props.innerRadiusRatio ?? 0; const seriesList = props.data.series; if (!seriesList || seriesList.length === 0) { return
No data
; } // SVG viewBox settings const viewBoxSize = 200; const center = viewBoxSize / 2; const maxRadius = (viewBoxSize / 2) * 0.9; // 90% of container to leave margin for stroke/anti-aliasing const globalInnerRadius = maxRadius * innerRadiusRatio; const numSeries = seriesList.length; // If only 1 series and innerRadiusRatio is 0, ringThickness is maxRadius. // If multiple series, we divide the available space (maxRadius - globalInnerRadius) among them. const ringThickness = (maxRadius - globalInnerRadius) / numSeries; const allSlices: any[] = []; seriesList.forEach((series, seriesIndex) => { const values = series.data; if (!values || values.length === 0) return; const total = values.reduce((sum, val) => sum + val, 0); let currentAngle = 0; // Outermost ring is series 0 const outerR = maxRadius - seriesIndex * ringThickness; const innerR = outerR - ringThickness; values.forEach((val, index) => { // Prevent 0 area slices. Also handle case where 1 val is 100% const ratio = total > 0 ? val / total : 0; const angleDelta = ratio * 360; // Fix for 100% slice (SVG arcs don't draw well at exactly 360) const isFullCircle = angleDelta === 360; const adjustedAngleDelta = isFullCircle ? 359.99 : angleDelta; const startAngle = currentAngle; const endAngle = currentAngle + adjustedAngleDelta; currentAngle += adjustedAngleDelta; const pathD = describeArc(center, center, outerR, startAngle, endAngle, innerR); const color = getChartColor(index); const label = props.data.labels[index] || `Item ${index + 1}`; const handleMouseEnter = (e: any) => { const percentage = (ratio * 100).toFixed(1) + '%'; Tooltip.show( e,
{series.name ? `${series.name} - ` : ''}{label}
Value: {val}
Share: {percentage}
, { position: 'auto' } ); e.target.style.opacity = 0.8; }; const handleMouseLeave = (e: any) => { Tooltip.hide(); e.target.style.opacity = 1; }; allSlices.push( ); }); }); const ref: RefProps = { referToCssId: globalCssId, }; const ratio = props.aspectRatio ?? 16 / 9; const paddingTop = `${(1 / ratio) * 100}%`; const styleStr = `width: ${props.width || '100%'}; min-height: 200px;`; return (
{props.title &&
{props.title}
}
{allSlices}
{showLegend && (
{props.data.labels.map((_, i) => (
{props.data.labels[i] || `Item ${i + 1}`}
))}
)}
); };