diff --git a/packages/app/src/components/DBPieChart.tsx b/packages/app/src/components/DBPieChart.tsx
index e2821cfeeb..952ef4c217 100644
--- a/packages/app/src/components/DBPieChart.tsx
+++ b/packages/app/src/components/DBPieChart.tsx
@@ -5,7 +5,7 @@ import {
BuilderChartConfigWithOptTimestamp,
RawSqlConfigWithDateRange,
} from '@hyperdx/common-utils/dist/types';
-import { Flex } from '@mantine/core';
+import { Box, Flex, ScrollArea, Text } from '@mantine/core';
import {
buildMVDateRangeIndicator,
@@ -16,7 +16,7 @@ import { useQueriedChartConfig } from '@/hooks/useChartConfig';
import { useMVOptimizationExplanation } from '@/hooks/useMVOptimizationExplanation';
import { useResolvedNumberFormat, useSource } from '@/source';
import type { NumberFormat } from '@/types';
-import { getColorProps } from '@/utils';
+import { formatNumber, getColorProps, truncateMiddle } from '@/utils';
import ChartContainer from './charts/ChartContainer';
import ChartErrorState, {
@@ -51,6 +51,54 @@ const PieChartTooltip = memo(
},
);
+const PieChartLegend = memo(
+ ({
+ data,
+ numberFormat,
+ }: {
+ data: { label: string; value: number; color: string }[];
+ numberFormat?: NumberFormat;
+ }) => {
+ if (!data.length) return null;
+ return (
+
+
+ {data.map(entry => (
+
+
+
+ {truncateMiddle(entry.label, 40)}
+
+
+ {numberFormat
+ ? formatNumber(entry.value, numberFormat)
+ : entry.value}
+
+
+ ))}
+
+
+ );
+ },
+);
+
export const DBPieChart = ({
config,
title,
@@ -168,7 +216,7 @@ export const DBPieChart = ({
align="center"
justify="center"
h="100%"
- style={{ flexGrow: 1 }}
+ style={{ flexGrow: 1, overflow: 'hidden' }}
>
{pieChartData.map(entry => (
|
@@ -196,6 +243,10 @@ export const DBPieChart = ({
/>
+
)}
diff --git a/packages/app/src/components/__tests__/DBPieChart.test.tsx b/packages/app/src/components/__tests__/DBPieChart.test.tsx
index 1b33d82050..5d087ac28c 100644
--- a/packages/app/src/components/__tests__/DBPieChart.test.tsx
+++ b/packages/app/src/components/__tests__/DBPieChart.test.tsx
@@ -89,6 +89,57 @@ describe('DBPieChart', () => {
expect(screen.getByTestId('pie-chart-container')).toBeInTheDocument();
});
+ it('should render pie chart legend with labels', () => {
+ mockUseQueriedChartConfig.mockReturnValue({
+ data: {
+ data: [
+ { status: 'success', count: 100 },
+ { status: 'error', count: 50 },
+ { status: 'timeout', count: 25 },
+ ],
+ meta: [
+ { name: 'status', type: 'String' },
+ { name: 'count', type: 'UInt64' },
+ ],
+ },
+ isLoading: false,
+ isError: false,
+ });
+
+ renderWithMantine();
+ const legend = screen.getByTestId('pie-chart-legend');
+ expect(legend).toBeInTheDocument();
+ expect(screen.getByText('success')).toBeInTheDocument();
+ expect(screen.getByText('error')).toBeInTheDocument();
+ expect(screen.getByText('timeout')).toBeInTheDocument();
+ });
+
+ it('should render scrollable legend with many groups', () => {
+ const manyGroups = Array.from({ length: 30 }, (_, i) => ({
+ status: `group-${i}`,
+ count: 100 - i,
+ }));
+
+ mockUseQueriedChartConfig.mockReturnValue({
+ data: {
+ data: manyGroups,
+ meta: [
+ { name: 'status', type: 'String' },
+ { name: 'count', type: 'UInt64' },
+ ],
+ },
+ isLoading: false,
+ isError: false,
+ });
+
+ renderWithMantine();
+ const legend = screen.getByTestId('pie-chart-legend');
+ expect(legend).toBeInTheDocument();
+ expect(legend).toHaveStyle({ alignSelf: 'stretch' });
+ expect(screen.getByText('group-0')).toBeInTheDocument();
+ expect(screen.getByText('group-29')).toBeInTheDocument();
+ });
+
it('passes the same config to useMVOptimizationExplanation, useQueriedChartConfig, and MVOptimizationIndicator', () => {
// Mock useSource to return a source so MVOptimizationIndicator is rendered
jest.mocked(useSource).mockReturnValue({