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({