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}
}
{showLegend && (
{props.data.labels.map((_, i) => (
{props.data.labels[i] || `Item ${i + 1}`}
))}
)}
);
};