From 8e363558a5ab66ce14fca49695f16602a46889dd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 01:57:47 +0000 Subject: [PATCH 1/3] feat: add legend to pie charts Add a legend panel alongside pie charts that maps each color to its corresponding label and value. The legend: - Displays a colored swatch, label text, and numeric value for each slice - Sits on the right side of the pie chart - Scrolls vertically when there are many items - Is capped at 40% width to preserve chart visibility - Uses Mantine ScrollArea for smooth overflow handling - Respects the chart's number format configuration Resolves HDX-4136 Co-authored-by: Mike Shi --- packages/app/src/components/DBPieChart.tsx | 59 +++++++++++++++++-- .../components/__tests__/DBPieChart.test.tsx | 25 ++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/DBPieChart.tsx b/packages/app/src/components/DBPieChart.tsx index e2821cfeeb..1baaa34747 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..bfc7a5e468 100644 --- a/packages/app/src/components/__tests__/DBPieChart.test.tsx +++ b/packages/app/src/components/__tests__/DBPieChart.test.tsx @@ -89,6 +89,31 @@ 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('passes the same config to useMVOptimizationExplanation, useQueriedChartConfig, and MVOptimizationIndicator', () => { // Mock useSource to return a source so MVOptimizationIndicator is rendered jest.mocked(useSource).mockReturnValue({ From 40b0ff5c1931803af9069f8979db7c4ab53ff96a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 02:45:12 +0000 Subject: [PATCH 2/3] fix: make pie chart legend scrollable when many groups overflow Co-authored-by: Mike Shi --- packages/app/src/components/DBPieChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/DBPieChart.tsx b/packages/app/src/components/DBPieChart.tsx index 1baaa34747..952ef4c217 100644 --- a/packages/app/src/components/DBPieChart.tsx +++ b/packages/app/src/components/DBPieChart.tsx @@ -64,7 +64,7 @@ const PieChartLegend = memo( From 143e5bb0ad8f0e0d8645631a57f9a57250c9076c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 02:50:59 +0000 Subject: [PATCH 3/3] test: add unit test for scrollable legend with many groups Co-authored-by: Mike Shi --- .../components/__tests__/DBPieChart.test.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/app/src/components/__tests__/DBPieChart.test.tsx b/packages/app/src/components/__tests__/DBPieChart.test.tsx index bfc7a5e468..5d087ac28c 100644 --- a/packages/app/src/components/__tests__/DBPieChart.test.tsx +++ b/packages/app/src/components/__tests__/DBPieChart.test.tsx @@ -114,6 +114,32 @@ describe('DBPieChart', () => { 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({