Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/healthy-squids-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hyperdx/common-utils": minor
"@hyperdx/app": minor
---

feat: pie chart now available for chart visualization
6 changes: 6 additions & 0 deletions packages/app/src/ChartUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,12 @@ export function convertToNumberChartConfig(
return omit(config, ['granularity', 'groupBy']);
}

export function convertToPieChartConfig(
config: ChartConfigWithOptTimestamp,
): ChartConfigWithOptTimestamp {
return omit(config, ['granularity']);
}

export function convertToTableChartConfig(
config: ChartConfigWithOptTimestamp,
): ChartConfigWithOptTimestamp {
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
} from '@/dashboard';

import ChartContainer from './components/charts/ChartContainer';
import { DBPieChart } from './components/DBPieChart';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import OnboardingModal from './components/OnboardingModal';
import { Tags } from './components/Tags';
Expand Down Expand Up @@ -395,6 +396,14 @@ const Tile = forwardRef(
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Pie && (
<DBPieChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={toolbar}
config={queriedConfig}
/>
)}
{/* Markdown charts may not have queriedConfig, if source is not set */}
{(queriedConfig?.displayType === DisplayType.Markdown ||
(!queriedConfig &&
Expand Down
22 changes: 21 additions & 1 deletion packages/app/src/components/DBEditTimeChartForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
IconArrowUp,
IconBell,
IconChartLine,
IconChartPie,
IconCirclePlus,
IconCode,
IconDotsVertical,
Expand Down Expand Up @@ -110,6 +111,7 @@ import ChartDisplaySettingsDrawer, {
ChartConfigDisplaySettings,
} from './ChartDisplaySettingsDrawer';
import DBNumberChart from './DBNumberChart';
import { DBPieChart } from './DBPieChart';
import DBSqlRowTableWithSideBar from './DBSqlRowTableWithSidebar';
import {
CheckBoxControlled,
Expand Down Expand Up @@ -656,6 +658,8 @@ export default function EditTimeChartForm({
return 'markdown';
case DisplayType.Table:
return 'table';
case DisplayType.Pie:
return 'pie';
case DisplayType.Number:
return 'number';
default:
Expand All @@ -669,7 +673,9 @@ export default function EditTimeChartForm({
}
}, [displayType, setValue]);

const showGeneratedSql = ['table', 'time', 'number'].includes(activeTab); // Whether to show the generated SQL preview
const showGeneratedSql = ['table', 'time', 'number', 'pie'].includes(
activeTab,
); // Whether to show the generated SQL preview
const showSampleEvents = tableSource?.kind !== SourceKind.Metric;

const [
Expand Down Expand Up @@ -1030,6 +1036,12 @@ export default function EditTimeChartForm({
>
Number
</Tabs.Tab>
<Tabs.Tab
value={DisplayType.Pie}
leftSection={<IconChartPie size={16} />}
>
Pie
</Tabs.Tab>
<Tabs.Tab
value={DisplayType.Search}
leftSection={<IconList size={16} />}
Expand Down Expand Up @@ -1494,6 +1506,14 @@ export default function EditTimeChartForm({
/>
</div>
)}
{queryReady && dbTimeChartConfig != null && activeTab === 'pie' && (
<div className="flex-grow-1 d-flex flex-column" style={{ height: 400 }}>
<DBPieChart
config={dbTimeChartConfig}
showMVOptimizationIndicator={false}
/>
</div>
)}
{queryReady && queriedConfig != null && activeTab === 'number' && (
<div className="flex-grow-1 d-flex flex-column" style={{ height: 400 }}>
<DBNumberChart
Expand Down
253 changes: 253 additions & 0 deletions packages/app/src/components/DBPieChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { useMemo } from 'react';
import randomUUID from 'crypto-randomuuid';
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import { ChartConfigWithOptTimestamp } from '@hyperdx/common-utils/dist/types';
import { Box, Code, Flex, Text } from '@mantine/core';

import {
buildMVDateRangeIndicator,
convertToPieChartConfig,
} from '@/ChartUtils';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
import { useMVOptimizationExplanation } from '@/hooks/useMVOptimizationExplanation';
import { useSource } from '@/source';
import { COLORS } from '@/utils';

import ChartContainer from './charts/ChartContainer';
import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator';
import { SQLPreview } from './ChartSQLPreview';

export const DBPieChart = ({
config,
title,
enabled = true,
queryKeyPrefix,
showMVOptimizationIndicator = true,
toolbarPrefix,
toolbarSuffix,
}: {
config: ChartConfigWithOptTimestamp;
title?: React.ReactNode;
enabled?: boolean;
queryKeyPrefix?: string;
showMVOptimizationIndicator?: boolean;
toolbarPrefix?: React.ReactNode[];
toolbarSuffix?: React.ReactNode[];
}) => {
const { data: source } = useSource({ id: config.source });

const queriedConfig = useMemo(() => {
return convertToPieChartConfig(config);
}, [config]);

const { data: mvOptimizationData } =
useMVOptimizationExplanation(queriedConfig);

const { data, isLoading, isError, error } = useQueriedChartConfig(
queriedConfig,
{
placeholderData: (prev: any) => prev,
queryKey: [queryKeyPrefix, queriedConfig],
enabled,
},
);

// Returns an array of aliases, so we can check if something is using an alias
const aliasMap = useMemo(() => {
// If the config.select is a string, we can't infer this.
// One day, we could potentially run this through chSqlToAliasMap but AST parsing
// doesn't work for most DBTableChart queries.
if (typeof config.select === 'string') {
return [];
}
return config.select.reduce((acc, select) => {
if (select.alias) {
acc.push(select.alias);
}
return acc;
}, [] as string[]);
}, [config.select]);

const toolbarItemsMemo = useMemo(() => {
const allToolbarItems = [];

if (toolbarPrefix && toolbarPrefix.length > 0) {
allToolbarItems.push(...toolbarPrefix);
}

if (source && showMVOptimizationIndicator) {
allToolbarItems.push(
<MVOptimizationIndicator
key="db-table-chart-mv-indicator"
config={queriedConfig}
source={source}
variant="icon"
/>,
);
}

const dateRangeIndicator = buildMVDateRangeIndicator({
mvOptimizationData,
originalDateRange: queriedConfig.dateRange,
});

if (dateRangeIndicator) {
allToolbarItems.push(dateRangeIndicator);
}

if (toolbarSuffix && toolbarSuffix.length > 0) {
allToolbarItems.push(...toolbarSuffix);
}

return allToolbarItems;
}, [
toolbarPrefix,
toolbarSuffix,
source,
showMVOptimizationIndicator,
mvOptimizationData,
queriedConfig,
]);

// Extract group column names from groupBy config
const groupByKeys = useMemo(() => {
if (!queriedConfig.groupBy) return [];

if (typeof queriedConfig.groupBy === 'string') {
return queriedConfig.groupBy.split(',').map(v => v.trim());
}

return queriedConfig.groupBy.map(g =>
typeof g === 'string' ? g : g.valueExpression,
);
}, [queriedConfig.groupBy]);

const pieChartData = useMemo(() => {
if (!data || data.data.length === 0) return [];

if (groupByKeys.length > 0 && data.data.length > 0) {
const groupColumnSet = new Set(groupByKeys);

return data.data.map((row, index) => {
const label =
groupByKeys.length === 1
? String(row[groupByKeys[0]])
: groupByKeys.map(key => row[key]).join(' - ');

let totalValue = 0;
for (const key in row) {
if (!groupColumnSet.has(key)) {
const numValue = parseFloat(row[key]);
if (!isNaN(numValue)) {
totalValue += numValue;
}
}
}

return {
label,
value: totalValue,
color:
index >= COLORS.length
? // Source - https://stackoverflow.com/a/5092872
'#000000'.replace(/0/g, () => {
return (~~(Math.random() * 16)).toString(16);
})
: COLORS[index],
};
});
}

if (data.data.length === 1) {
const queryData = data.data[0];

return Object.keys(queryData).map((key, index) => ({
// If it's an alias, wrap in quotes to support a variety of formats (ex "Time (ms)", "Req/s", etc)
label: aliasMap.includes(key) ? `${key}` : key,
value: parseFloat(queryData[key]),
color:
index >= COLORS.length
? // Source - https://stackoverflow.com/a/5092872
'#000000'.replace(/0/g, () => {
return (~~(Math.random() * 16)).toString(16);
})
: COLORS[index],
}));
}

return [];
}, [data, aliasMap, groupByKeys]);

return (
<ChartContainer title={title} toolbarItems={toolbarItemsMemo}>
{isLoading && !data ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError && error ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted overflow-scroll">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<Flex
data-testid="pie-chart-container"
align="center"
justify="center"
h="100%"
style={{ flexGrow: 1 }}
>
<ResponsiveContainer
height="100%"
width="100%"
className={isLoading ? 'effect-pulse' : ''}
>
<PieChart>
<Pie
cx="50%"
cy="50%"
data={pieChartData}
dataKey="value"
fill="#8884d8"
nameKey="label"
legendType="none"
>
{pieChartData.map(entry => (
<Cell key={entry.label} fill={entry.color} stroke="none" />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</Flex>
)}
</ChartContainer>
);
};
Loading
Loading