import './PiePanelWrapper.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Group } from '@visx/group'; import { Pie } from '@visx/shape'; import { useTooltip, useTooltipInPortal } from '@visx/tooltip'; import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import { themeColors } from 'constants/theme'; import { useIsDarkMode } from 'hooks/useDarkMode'; import getLabelName from 'lib/getLabelName'; import { generateColor } from 'lib/uPlotLib/utils/generateColor'; import { isNaN } from 'lodash-es'; import { useRef, useState } from 'react'; import { PanelWrapperProps, TooltipData } from './panelWrapper.types'; import { lightenColor, tooltipStyles } from './utils'; // reference: https://www.youtube.com/watch?v=bL3P9CqQkKw function PiePanelWrapper({ queryResponse, widget, }: PanelWrapperProps): JSX.Element { const [active, setActive] = useState<{ label: string; value: string; color: string; } | null>(null); const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip, } = useTooltip(); const { containerRef, TooltipInPortal } = useTooltipInPortal({ scroll: true, detectBounds: true, }); const panelData = queryResponse.data?.payload?.data?.result || []; const isDarkMode = useIsDarkMode(); let pieChartData: { label: string; value: string; color: string; }[] = [].concat( ...(panelData .map((d) => { const label = getLabelName(d.metric, d.queryName || '', d.legend || ''); return { label, value: d?.values?.[0]?.[1], color: widget?.customLegendColors?.[label] || generateColor( label, isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, ), }; }) .filter((d) => d !== undefined) as never[]), ); pieChartData = pieChartData.filter( (arc) => arc.value && !isNaN(parseFloat(arc.value)) && parseFloat(arc.value) > 0, ); let size = 0; let width = 0; let height = 0; const chartRef = useRef(null); if (chartRef.current) { const { offsetWidth, offsetHeight } = chartRef.current; size = Math.min(offsetWidth, offsetHeight); width = offsetWidth; height = offsetHeight; } // 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.4 in this case) return active.color === color ? color : lightenedColor; }; return (
{!pieChartData.length &&
No data
} {pieChartData.length > 0 && ( <>
parseFloat(data.value)} outerRadius={radius} innerRadius={innerRadius} padAngle={0.01} cornerRadius={3} width={size} height={size} > { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type (pie) => pie.arcs.map((arc) => { const { label } = arc.data; const [centroidX, centroidY] = pie.path.centroid(arc); const arcPath = pie.path(arc); const arcFill = arc.data.color; // 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, value: displayValue, color: arc.data.color, key: label, }, tooltipTop: centroidY + height / 2, tooltipLeft: centroidX + width / 2, }); setActive(arc.data); }} onMouseLeave={(): void => { hideTooltip(); setActive(null); }} > {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 && (
{tooltipData.key}
{tooltipData.value}
)}
{pieChartData.map((data) => (
{ setActive(data); }} onMouseLeave={(): void => { setActive(null); }} >
{data.label}
))}
)}
); } export default PiePanelWrapper;