feat: added label with value (result) in Pie charts (#7322)

* fix: fixed legend format not working for Pie Chart

* fix: added enhancement to legend fit and show, also made pie-chart responsive

* fix: made some css fixes

* fix: css fixes

* feat: added label with value (result) in Pie charts

* feat: added UI for inner radiud to have total value in the center of PIE

* feat: added formatting and unit support to innder total values
This commit is contained in:
SagarRajput-7 2025-03-18 22:04:56 +05:30 committed by GitHub
parent 4ede88cc1f
commit 53d3de4909
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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 (
<g
// eslint-disable-next-line react/no-array-index-key
key={`arc-${label}-${index}`}
key={`arc-${label}-${arc.data.value}-${arc.startAngle.toFixed(
6,
)}`}
onMouseEnter={(): void => {
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({
}}
>
<path d={arcPath || ''} fill={getFillColor(arcFill)} />
{hasSpaceForLabel && (
<text
x={centroidX}
y={centroidY}
dy=".33em"
fill={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
fontSize={10}
textAnchor="middle"
pointerEvents="none"
>
{displayLabel}
</text>
{shouldShowLabel && (
<>
{/* Connecting line */}
<line
x1={centroidX}
y1={centroidY}
x2={lineEndX}
y2={lineEndY}
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
strokeWidth={1}
/>
{/* Line from arc edge to label */}
<line
x1={lineEndX}
y1={lineEndY}
x2={labelX}
y2={labelY}
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
strokeWidth={1}
/>
{/* Label text */}
<text
x={labelX}
y={labelY - 8}
dy=".33em"
fill={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
fontSize={10}
textAnchor={textAnchor}
pointerEvents="none"
>
{shortenedLabel}
</text>
{/* Value text */}
<text
x={labelX}
y={labelY + 8}
dy=".33em"
fill={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
fontSize={10}
fontWeight="bold"
textAnchor={textAnchor}
pointerEvents="none"
>
{displayValue}
</text>
</>
)}
</g>
);
})
}
</Pie>
{/* Add total value in the center */}
<text
textAnchor="middle"
dominantBaseline="central"
fill={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
>
<tspan fontSize={numericFontSize} fontWeight="bold">
{numericTotal}
</tspan>
{unitTotal && (
<tspan fontSize={unitFontSize} opacity={0.9} dx={2}>
{unitTotal}
</tspan>
)}
</text>
</Group>
</svg>
{tooltipOpen && tooltipData && (