import React, { useState } from 'react'; import './DonutChart.scss'; import { Status, DonutChartProps, StatusValue } from './type'; const calculateArc = ( x: number, y: number, radius: number, startAngle: number, endAngle: number ): string => { const startX = x + radius * Math.cos(startAngle); const startY = y + radius * Math.sin(startAngle); const endX = x + radius * Math.cos(endAngle); const endY = y + radius * Math.sin(endAngle); const largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0; return `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`; }; const useColorMappings = () => ({ colorMapping: { passed: 'var(--status-success-text-color)', failed: 'var(--status-rejected-text-color)', warning: 'var(--status-warning-text-color)', skipped: 'var(--status-skipped-text-color)', default: 'var(--brand-color)', }, hoverMapping: { passed: 'var(--status-success-bg-color)', failed: 'var(--status-rejected-bg-color)', warning: 'var(--status-warning-bg-color)', skipped: 'var(--status-skipped-bg-color)', default: 'var(--status-percentage-growth-bg-color)', }, }); const DonutChart: React.FC = ({ radius = 60, lineWidth = 15, statusValues = [], gapAngle = 0.06, legendDetailsType = '', isLegendDetails = true, }) => { const [hoveredStatus, setHoveredStatus] = useState(null); const { colorMapping, hoverMapping } = useColorMappings(); const total = statusValues?.reduce((acc, { value }) => acc + value, 0); const nonZeroValues = statusValues?.filter(({ value }) => value > 0); const statusColors = ['passed', 'failed', 'warning', 'skipped']; // Calculate angles and gaps const TOTAL_GAP_ANGLE = gapAngle * nonZeroValues.length; let remainingAngle = 2 * Math.PI - TOTAL_GAP_ANGLE; let currentAngle = Math.PI / 2; const MIN_PERCENTAGE = 1; const MIN_ANGLE = (MIN_PERCENTAGE / 100) * (2 * Math.PI); let minAngleTotal = 0; // Adjust for small angles nonZeroValues.forEach(({ value }) => { const valuePercentage = value / total; const angle = Math.max(valuePercentage * (2 * Math.PI), MIN_ANGLE); minAngleTotal += angle; remainingAngle -= angle; }); const handleMouseEnter = (status: Status) => setHoveredStatus(status); const handleMouseLeave = () => setHoveredStatus(null); const SVG_PADDING = 4; const DONUT_SVG_SIZE = (radius + lineWidth) * 2 + SVG_PADDING * 2; const renderArc = (statusValue: StatusValue, endAngle: number, i: number) => { const normalizedStatus = statusValue?.status?.toLowerCase() as Status; const isFullCircle = nonZeroValues.length === 1; // Full circle handling const foregroundArcPath = isFullCircle ? calculateArc(0, 0, radius, 0, 2 * Math.PI) : calculateArc(0, 0, radius, currentAngle, endAngle); // Outer arc for hover effect const outerArcRadius = radius + lineWidth - 1; const outerArcPath = isFullCircle ? calculateArc(0, 0, outerArcRadius, 0, 2 * Math.PI) : calculateArc(0, 0, outerArcRadius, currentAngle, endAngle); currentAngle = endAngle + gapAngle; return ( {/* Main arc */} handleMouseEnter(normalizedStatus)} onMouseLeave={handleMouseLeave} strokeOpacity={0.8} /> {/* Hover effect */} {hoveredStatus === normalizedStatus && ( )} ); }; const renderLegendItem = (statusKey: Status) => { const statusData = statusValues?.find( (s) => s.status?.toLowerCase() === statusKey?.toLowerCase() ); const value = Math.round(statusData?.value || 0); const percentage = ((statusData?.value || 0) / total) * 100; return (
{statusKey?.charAt(0)?.toUpperCase() + statusKey?.slice(1)?.toLowerCase()}
{value} {legendDetailsType}
({percentage?.toFixed(2)}%)
); }; return (
{nonZeroValues?.map((status, i) => { const valuePercentage = status.value / total; let angle = Math.max(valuePercentage * (2 * Math.PI), MIN_ANGLE); angle += remainingAngle * (valuePercentage / (total / total)); const endAngle = currentAngle + angle; return renderArc(status, endAngle, i); })} {hoveredStatus ? hoveredStatus?.toUpperCase() : 'TOTAL'} {hoveredStatus ? `${ statusValues?.find( (s) => s?.status?.toLowerCase() === hoveredStatus )?.value } ${legendDetailsType}` : `${total} ${legendDetailsType}`} {hoveredStatus ? `${( ((statusValues?.find( (s) => s?.status?.toLowerCase() === hoveredStatus )?.value || 0) / total) * 100 )?.toFixed(2)}%` : '100%'}
{isLegendDetails && (
{statusColors?.map((status) => renderLegendItem(status as Status))}
)}
); }; export default DonutChart;