mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 08:29:03 +08:00
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:
parent
4ede88cc1f
commit
53d3de4909
@ -15,7 +15,7 @@ import { useRef, useState } from 'react';
|
|||||||
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
|
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
|
||||||
import { lightenColor, tooltipStyles } from './utils';
|
import { lightenColor, tooltipStyles } from './utils';
|
||||||
|
|
||||||
// refernce: https://www.youtube.com/watch?v=bL3P9CqQkKw
|
// reference: https://www.youtube.com/watch?v=bL3P9CqQkKw
|
||||||
function PiePanelWrapper({
|
function PiePanelWrapper({
|
||||||
queryResponse,
|
queryResponse,
|
||||||
widget,
|
widget,
|
||||||
@ -60,6 +60,7 @@ function PiePanelWrapper({
|
|||||||
}))
|
}))
|
||||||
.filter((d) => d !== undefined) as never[]),
|
.filter((d) => d !== undefined) as never[]),
|
||||||
);
|
);
|
||||||
|
|
||||||
pieChartData = pieChartData.filter(
|
pieChartData = pieChartData.filter(
|
||||||
(arc) =>
|
(arc) =>
|
||||||
arc.value && !isNaN(parseFloat(arc.value)) && parseFloat(arc.value) > 0,
|
arc.value && !isNaN(parseFloat(arc.value)) && parseFloat(arc.value) > 0,
|
||||||
@ -76,13 +77,63 @@ function PiePanelWrapper({
|
|||||||
width = offsetWidth;
|
width = offsetWidth;
|
||||||
height = offsetHeight;
|
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 => {
|
const getFillColor = (color: string): string => {
|
||||||
if (active === null) {
|
if (active === null) {
|
||||||
return color;
|
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;
|
return active.color === color ? color : lightenedColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,10 +152,8 @@ function PiePanelWrapper({
|
|||||||
value: string;
|
value: string;
|
||||||
color: string;
|
color: string;
|
||||||
}): number => parseFloat(data.value)}
|
}): number => parseFloat(data.value)}
|
||||||
outerRadius={({ data }): number => {
|
outerRadius={radius}
|
||||||
if (!active) return half - 3;
|
innerRadius={innerRadius}
|
||||||
return data.label === active.label ? half : half - 3;
|
|
||||||
}}
|
|
||||||
padAngle={0.01}
|
padAngle={0.01}
|
||||||
cornerRadius={3}
|
cornerRadius={3}
|
||||||
width={size}
|
width={size}
|
||||||
@ -113,36 +162,53 @@ function PiePanelWrapper({
|
|||||||
{
|
{
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
(pie) =>
|
(pie) =>
|
||||||
pie.arcs.map((arc, index) => {
|
pie.arcs.map((arc) => {
|
||||||
const { label } = arc.data;
|
const { label } = arc.data;
|
||||||
const [centroidX, centroidY] = pie.path.centroid(arc);
|
const [centroidX, centroidY] = pie.path.centroid(arc);
|
||||||
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.6;
|
|
||||||
const arcPath = pie.path(arc);
|
const arcPath = pie.path(arc);
|
||||||
const arcFill = arc.data.color;
|
const arcFill = arc.data.color;
|
||||||
|
|
||||||
// Calculate available space for label text
|
// Calculate angle bisector for the arc (midpoint of the arc)
|
||||||
const arcSize = arc.endAngle - arc.startAngle;
|
const angle = (arc.startAngle + arc.endAngle) / 2;
|
||||||
const maxLabelLength = Math.floor(arcSize * 15);
|
|
||||||
const labelText = arc.data.label;
|
// Calculate outer point for the label
|
||||||
const displayLabel =
|
const labelRadius = radius * 1.3; // Label position
|
||||||
labelText.length > maxLabelLength
|
const labelX = Math.sin(angle) * labelRadius;
|
||||||
? `${labelText.substring(0, maxLabelLength - 3)}...`
|
const labelY = -Math.cos(angle) * labelRadius;
|
||||||
: labelText;
|
|
||||||
|
// 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 (
|
return (
|
||||||
<g
|
<g
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
key={`arc-${label}-${arc.data.value}-${arc.startAngle.toFixed(
|
||||||
key={`arc-${label}-${index}`}
|
6,
|
||||||
|
)}`}
|
||||||
onMouseEnter={(): void => {
|
onMouseEnter={(): void => {
|
||||||
showTooltip({
|
showTooltip({
|
||||||
tooltipData: {
|
tooltipData: {
|
||||||
label,
|
label,
|
||||||
// do not update the unit in the data as the arc allotment is based on value
|
value: displayValue,
|
||||||
// and treats 4K smaller than 40
|
|
||||||
value: getYAxisFormattedValue(
|
|
||||||
arc.data.value,
|
|
||||||
widget?.yAxisUnit || 'none',
|
|
||||||
),
|
|
||||||
color: arc.data.color,
|
color: arc.data.color,
|
||||||
key: label,
|
key: label,
|
||||||
},
|
},
|
||||||
@ -157,24 +223,78 @@ function PiePanelWrapper({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<path d={arcPath || ''} fill={getFillColor(arcFill)} />
|
<path d={arcPath || ''} fill={getFillColor(arcFill)} />
|
||||||
{hasSpaceForLabel && (
|
|
||||||
|
{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
|
<text
|
||||||
x={centroidX}
|
x={labelX}
|
||||||
y={centroidY}
|
y={labelY - 8}
|
||||||
dy=".33em"
|
dy=".33em"
|
||||||
fill={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
|
fill={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400}
|
||||||
fontSize={10}
|
fontSize={10}
|
||||||
textAnchor="middle"
|
textAnchor={textAnchor}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
>
|
>
|
||||||
{displayLabel}
|
{shortenedLabel}
|
||||||
</text>
|
</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>
|
</g>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</Pie>
|
</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>
|
</Group>
|
||||||
</svg>
|
</svg>
|
||||||
{tooltipOpen && tooltipData && (
|
{tooltipOpen && tooltipData && (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user