diff --git a/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx b/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx index a583d60d6b..948f62af3f 100644 --- a/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx @@ -15,7 +15,7 @@ import { useRef, useState } from 'react'; import { PanelWrapperProps, TooltipData } from './panelWrapper.types'; import { lightenColor, tooltipStyles } from './utils'; -// refernce: https://www.youtube.com/watch?v=bL3P9CqQkKw +// reference: https://www.youtube.com/watch?v=bL3P9CqQkKw function PiePanelWrapper({ queryResponse, widget, @@ -60,6 +60,7 @@ function PiePanelWrapper({ })) .filter((d) => d !== undefined) as never[]), ); + pieChartData = pieChartData.filter( (arc) => arc.value && !isNaN(parseFloat(arc.value)) && parseFloat(arc.value) > 0, @@ -76,13 +77,63 @@ function PiePanelWrapper({ width = offsetWidth; height = offsetHeight; } - const half = size / 2; + + // Adjust the size to leave room for external labels + const radius = size * 0.35; + // Add inner radius for donut chart + const innerRadius = radius * 0.6; + + // Calculate total value for center display + const totalValue = pieChartData.reduce( + (sum, data) => sum + parseFloat(data.value || '0'), + 0, + ); + + // Format total for display with the same unit as segments + const formattedTotal = getYAxisFormattedValue( + totalValue.toString(), + widget?.yAxisUnit || 'none', + ); + + // Extract numeric part and unit separately for styling + const matches = formattedTotal.match(/([\d.]+[KMB]?)(.*)$/); + const numericTotal = matches?.[1] || formattedTotal; + const unitTotal = matches?.[2]?.trim() || ''; + + // Dynamically calculate font size based on text length to prevent overflow + const getScaledFontSize = ({ + text, + baseSize, + innerRadius, + }: { + text: string; + baseSize: number; + innerRadius: number; + }): number => { + if (!text) return baseSize; + + const { length } = text; + // More aggressive scaling for very long numbers + const scaleFactor = Math.max(0.3, 1 - (length - 3) * 0.09); + + // Ensure text fits in the inner circle (roughly) + const maxSize = innerRadius * 0.9; // Don't use more than 90% of inner radius + + return Math.min(baseSize * scaleFactor, maxSize); + }; + + const numericFontSize = getScaledFontSize({ + text: numericTotal, + baseSize: radius * 0.3, + innerRadius, + }); + const unitFontSize = numericFontSize * 0.5; // Unit size is half of numeric size const getFillColor = (color: string): string => { if (active === null) { return color; } - const lightenedColor = lightenColor(color, 0.4); // Adjust the opacity value (0.7 in this case) + const lightenedColor = lightenColor(color, 0.4); // Adjust the opacity value (0.4 in this case) return active.color === color ? color : lightenedColor; }; @@ -101,10 +152,8 @@ function PiePanelWrapper({ value: string; color: string; }): number => parseFloat(data.value)} - outerRadius={({ data }): number => { - if (!active) return half - 3; - return data.label === active.label ? half : half - 3; - }} + outerRadius={radius} + innerRadius={innerRadius} padAngle={0.01} cornerRadius={3} width={size} @@ -113,36 +162,53 @@ function PiePanelWrapper({ { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type (pie) => - pie.arcs.map((arc, index) => { + pie.arcs.map((arc) => { const { label } = arc.data; const [centroidX, centroidY] = pie.path.centroid(arc); - const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.6; const arcPath = pie.path(arc); const arcFill = arc.data.color; - // Calculate available space for label text - const arcSize = arc.endAngle - arc.startAngle; - const maxLabelLength = Math.floor(arcSize * 15); - const labelText = arc.data.label; - const displayLabel = - labelText.length > maxLabelLength - ? `${labelText.substring(0, maxLabelLength - 3)}...` - : labelText; + // Calculate angle bisector for the arc (midpoint of the arc) + const angle = (arc.startAngle + arc.endAngle) / 2; + + // Calculate outer point for the label + const labelRadius = radius * 1.3; // Label position + const labelX = Math.sin(angle) * labelRadius; + const labelY = -Math.cos(angle) * labelRadius; + + // Calculate endpoint for the connecting line + const lineEndRadius = radius * 1.1; + const lineEndX = Math.sin(angle) * lineEndRadius; + const lineEndY = -Math.cos(angle) * lineEndRadius; + + // Format the value for display + const displayValue = getYAxisFormattedValue( + arc.data.value, + widget?.yAxisUnit || 'none', + ); + + // Determine text anchor based on position in the circle + const textAnchor = Math.sin(angle) > 0 ? 'start' : 'end'; + + // Shorten label if too long + const shortenedLabel = + label.length > 15 ? `${label.substring(0, 12)}...` : label; + + const shouldShowLabel = + parseFloat(arc.data.value) / + pieChartData.reduce((sum, d) => sum + parseFloat(d.value), 0) > + 0.03; return ( { showTooltip({ tooltipData: { label, - // do not update the unit in the data as the arc allotment is based on value - // and treats 4K smaller than 40 - value: getYAxisFormattedValue( - arc.data.value, - widget?.yAxisUnit || 'none', - ), + value: displayValue, color: arc.data.color, key: label, }, @@ -157,24 +223,78 @@ function PiePanelWrapper({ }} > - {hasSpaceForLabel && ( - - {displayLabel} - + + {shouldShowLabel && ( + <> + {/* Connecting line */} + + + {/* Line from arc edge to label */} + + + {/* Label text */} + + {shortenedLabel} + + + {/* Value text */} + + {displayValue} + + )} ); }) } + + {/* Add total value in the center */} + + + {numericTotal} + + {unitTotal && ( + + {unitTotal} + + )} + {tooltipOpen && tooltipData && (