From 7569ba1e22a78e514bdc7b6eb9ff1af4455bde8c Mon Sep 17 00:00:00 2001 From: Alex Fedotyev <61838744+alex-fedotyev@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:08:50 -0700 Subject: [PATCH 1/4] feat: always-on attribute distribution mode for Event Deltas - Show attribute value distribution charts immediately on load without heatmap selection - Blue 'All spans' distribution bars shown by default; selecting heatmap area switches to red/green comparison mode - Add legend UI showing current mode and interaction hints Closes #1824 --- .changeset/always-on-distribution.md | 5 + packages/app/src/components/DBDeltaChart.tsx | 187 +++++++++++++++--- .../components/PropertyComparisonChart.tsx | 65 +++--- .../Search/DBSearchHeatmapChart.tsx | 46 ++--- .../app/src/components/deltaChartUtils.ts | 5 +- 5 files changed, 218 insertions(+), 90 deletions(-) create mode 100644 .changeset/always-on-distribution.md diff --git a/.changeset/always-on-distribution.md b/.changeset/always-on-distribution.md new file mode 100644 index 0000000000..0f6bfe277f --- /dev/null +++ b/.changeset/always-on-distribution.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +feat: always-on attribute distribution mode for Event Deltas \ No newline at end of file diff --git a/packages/app/src/components/DBDeltaChart.tsx b/packages/app/src/components/DBDeltaChart.tsx index a64154a525..41c412b9bf 100644 --- a/packages/app/src/components/DBDeltaChart.tsx +++ b/packages/app/src/components/DBDeltaChart.tsx @@ -18,10 +18,12 @@ import { useElementSize } from '@mantine/hooks'; import { isAggregateFunction } from '@/ChartUtils'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; import { getFirstTimestampValueExpression } from '@/source'; +import { getChartColorError, getChartColorSuccess } from '@/utils'; import { SQLPreview } from './ChartSQLPreview'; import type { AddFilterFn } from './deltaChartUtils'; import { + ALL_SPANS_COLOR, computeComparisonScore, flattenedKeyToFilterKey, getPropertyStatistics, @@ -45,22 +47,32 @@ export type { AddFilterFn } from './deltaChartUtils'; export default function DBDeltaChart({ config, valueExpr, - xMin, - xMax, - yMin, - yMax, + xMin: rawXMin, + xMax: rawXMax, + yMin: rawYMin, + yMax: rawYMax, onAddFilter, spanIdExpression, }: { config: BuilderChartConfigWithDateRange; valueExpr: string; - xMin: number; - xMax: number; - yMin: number; - yMax: number; + xMin?: number | null; + xMax?: number | null; + yMin?: number | null; + yMax?: number | null; onAddFilter?: AddFilterFn; spanIdExpression?: string; }) { + // Derive whether a heatmap selection exists from nullable props + const hasSelection = + rawXMin != null && rawXMax != null && rawYMin != null && rawYMax != null; + // Safe numeric defaults so query builders always get valid values + // (outlier/inlier queries are gated by enabled:hasSelection) + const xMin = rawXMin ?? 0; + const xMax = rawXMax ?? 0; + const yMin = rawYMin ?? 0; + const yMax = rawYMax ?? 0; + // Determine if the value expression uses aggregate functions const isAggregate = isAggregateFunction(valueExpr); @@ -200,28 +212,60 @@ export default function DBDeltaChart({ ]; }; - const { data: outlierData, error } = useQueriedChartConfig({ - ...config, - with: buildWithClauses(true), - select: '*', - filters: buildFilters(true), - orderBy: [{ ordering: 'DESC', valueExpression: stableSampleExpr }], - limit: { limit: SAMPLE_SIZE }, - }); - - const { data: inlierData } = useQueriedChartConfig({ - ...config, - with: buildWithClauses(false), - select: '*', - filters: buildFilters(false), - orderBy: [{ ordering: 'DESC', valueExpression: stableSampleExpr }], - limit: { limit: SAMPLE_SIZE }, - }); + const { + data: outlierData, + error: outlierError, + isLoading: isOutlierLoading, + } = useQueriedChartConfig( + { + ...config, + with: buildWithClauses(true), + select: '*', + filters: buildFilters(true), + orderBy: [{ ordering: 'DESC', valueExpression: stableSampleExpr }], + limit: { limit: SAMPLE_SIZE }, + }, + { enabled: hasSelection }, + ); + + const { data: inlierData, isLoading: isInlierLoading } = + useQueriedChartConfig( + { + ...config, + with: buildWithClauses(false), + select: '*', + filters: buildFilters(false), + orderBy: [{ ordering: 'DESC', valueExpression: stableSampleExpr }], + limit: { limit: SAMPLE_SIZE }, + }, + { enabled: hasSelection }, + ); + + // When no selection exists, fetch all spans without any range filter + const { + data: allSpansData, + error: allSpansError, + isLoading: isAllSpansLoading, + } = useQueriedChartConfig( + { + ...config, + select: '*', + orderBy: [{ ordering: 'DESC', valueExpression: stableSampleExpr }], + limit: { limit: SAMPLE_SIZE }, + }, + { enabled: !hasSelection }, + ); + + const isLoading = hasSelection + ? isOutlierLoading || isInlierLoading + : isAllSpansLoading; + + const error = outlierError ?? allSpansError; // Column metadata for field classification (from ClickHouse response) const columnMeta = useMemo<{ name: string; type: string }[]>( - () => outlierData?.meta ?? inlierData?.meta ?? [], - [outlierData?.meta, inlierData?.meta], + () => outlierData?.meta ?? inlierData?.meta ?? allSpansData?.meta ?? [], + [outlierData?.meta, inlierData?.meta, allSpansData?.meta], ); // Wrap onAddFilter to convert flattened dot-notation keys into ClickHouse bracket notation @@ -233,24 +277,30 @@ export default function DBDeltaChart({ [onAddFilter, columnMeta], ); - // TODO: Is loading state const { visibleProperties, hiddenProperties, outlierValueOccurences, inlierValueOccurences, } = useMemo(() => { + // When no selection: use allSpans as "outlier" data and empty for inliers. + // The sort will rank by frequency (delta = count - 0 = count). + const actualOutlierData = hasSelection + ? (outlierData?.data ?? []) + : (allSpansData?.data ?? []); + const actualInlierData = hasSelection ? (inlierData?.data ?? []) : []; + const { percentageOccurences: outlierValueOccurences, propertyOccurences: outlierPropertyOccurences, valueOccurences: outlierRawValueOccurences, - } = getPropertyStatistics(outlierData?.data ?? []); + } = getPropertyStatistics(actualOutlierData); const { percentageOccurences: inlierValueOccurences, propertyOccurences: inlierPropertyOccurences, valueOccurences: inlierRawValueOccurences, - } = getPropertyStatistics(inlierData?.data ?? []); + } = getPropertyStatistics(actualInlierData); // Get all the unique keys from the outliers let uniqueKeys = new Set([...outlierValueOccurences.keys()]); @@ -308,7 +358,13 @@ export default function DBDeltaChart({ outlierValueOccurences, inlierValueOccurences, }; - }, [outlierData?.data, inlierData?.data, columnMeta]); + }, [ + outlierData?.data, + inlierData?.data, + allSpansData?.data, + hasSelection, + columnMeta, + ]); const [activePage, setPage] = useState(1); @@ -411,6 +467,71 @@ export default function DBDeltaChart({ flexDirection: 'column', }} > + {/* Legend */} + + {hasSelection ? ( + <> + + + + Selection + + + + + + Background + + + + ) : ( + <> + + + + All spans + + + + {isLoading + ? 'Loading...' + : 'Select an area on the chart above to enable comparisons'} + + + )} + + {/* Loading state */} + {isLoading && visibleOnPage.length === 0 && hiddenOnPage.length === 0 && ( + + + Loading attribute distributions... + + + )} {/* Primary fields */} {visibleOnPage.length > 0 && (
))}
@@ -468,6 +590,7 @@ export default function DBDeltaChart({ } onAddFilter={onAddFilter ? handleAddFilter : undefined} key={key} + hasSelection={hasSelection} /> ))} @@ -489,4 +612,4 @@ export default function DBDeltaChart({ ); -} +} \ No newline at end of file diff --git a/packages/app/src/components/PropertyComparisonChart.tsx b/packages/app/src/components/PropertyComparisonChart.tsx index 2cad624304..f1fc0b3b87 100644 --- a/packages/app/src/components/PropertyComparisonChart.tsx +++ b/packages/app/src/components/PropertyComparisonChart.tsx @@ -23,6 +23,7 @@ import { import { DBRowTableIconButton } from './DBTable/DBRowTableIconButton'; import type { AddFilterFn } from './deltaChartUtils'; import { + ALL_SPANS_COLOR, applyTopNAggregation, mergeValueStatisticsMaps, OTHER_BUCKET_COLOR, @@ -103,7 +104,7 @@ function TruncatedTick({ x = 0, y = 0, payload }: TickProps) { const value = String(payload?.value ?? ''); const MAX_CHARS = 12; const displayValue = - value.length > MAX_CHARS ? value.slice(0, MAX_CHARS) + '\u2026' : value; + value.length > MAX_CHARS ? value.slice(0, MAX_CHARS) + '...' : value; return ( {value} @@ -127,11 +128,13 @@ export function PropertyComparisonChart({ outlierValueOccurences, inlierValueOccurences, onAddFilter, + hasSelection = true, }: { name: string; outlierValueOccurences: Map; inlierValueOccurences: Map; onAddFilter?: AddFilterFn; + hasSelection?: boolean; }) { const mergedValueStatistics = mergeValueStatisticsMaps( outlierValueOccurences, @@ -206,41 +209,47 @@ export function PropertyComparisonChart({ /> } - allowEscapeViewBox={{ x: false, y: true }} + allowEscapeViewBox={{ x: true, y: true }} wrapperStyle={{ zIndex: 1000 }} /> {chartData.map((entry, index) => ( - ))} - - - {chartData.map((entry, index) => ( - ))} + {hasSelection && ( + + {chartData.map((entry, index) => ( + + ))} + + )} @@ -262,14 +271,16 @@ export function PropertyComparisonChart({ {clickedValue.length === 0 ? Empty String : clickedValue} - - Selection:{' '} + + {hasSelection ? 'Selection' : 'All spans'}:{' '} {(outlierValueOccurences.get(clickedValue) ?? 0).toFixed(1)}% - - Background:{' '} - {(inlierValueOccurences.get(clickedValue) ?? 0).toFixed(1)}% - + {hasSelection && ( + + Background:{' '} + {(inlierValueOccurences.get(clickedValue) ?? 0).toFixed(1)}% + + )} {onAddFilter && ( @@ -310,4 +321,4 @@ export function PropertyComparisonChart({ ); -} +} \ No newline at end of file diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 7fb8792846..ae2647844a 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -14,8 +14,6 @@ import { } from '@hyperdx/common-utils/dist/types'; import { Box, Flex } from '@mantine/core'; import { Button } from '@mantine/core'; -import { Center } from '@mantine/core'; -import { Text } from '@mantine/core'; import { IconPlayerPlay } from '@tabler/icons-react'; import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor'; @@ -120,33 +118,21 @@ export function DBSearchHeatmapChart({ }} /> - {fields.xMin != null && - fields.xMax != null && - fields.yMin != null && - fields.yMax != null ? ( - - ) : ( -
- - Please highlight an outlier range in the heatmap to view the delta - chart. - -
- )} +
); } @@ -214,4 +200,4 @@ function DBSearchHeatmapForm({ ); -} +} \ No newline at end of file diff --git a/packages/app/src/components/deltaChartUtils.ts b/packages/app/src/components/deltaChartUtils.ts index 21250cf021..de1cbb60fc 100644 --- a/packages/app/src/components/deltaChartUtils.ts +++ b/packages/app/src/components/deltaChartUtils.ts @@ -94,6 +94,9 @@ export const MAX_CHART_VALUES_UPPER = 8; // Color for the "Other (N)" aggregated bucket — neutral gray. export const OTHER_BUCKET_COLOR = 'var(--mantine-color-gray-5)'; +// Color for the "All spans" distribution bar (no selection / comparison mode off). +export const ALL_SPANS_COLOR = 'var(--mantine-color-blue-6)'; + export function mergeValueStatisticsMaps( outlierValues: Map, // value -> count inlierValues: Map, @@ -513,4 +516,4 @@ export function semanticBoost(key: string): number { if (lowerKey.endsWith('.' + suffix) || lowerKey === suffix) return 1; } return 0; -} +} \ No newline at end of file From 19207e95f8ffb9d926342ac5366fc679bc899626 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev <61838744+alex-fedotyev@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:16:45 -0700 Subject: [PATCH 2/4] fix: add missing trailing newlines --- .changeset/always-on-distribution.md | 2 +- packages/app/src/components/DBDeltaChart.tsx | 8 ++++---- packages/app/src/components/PropertyComparisonChart.tsx | 4 ++-- .../app/src/components/Search/DBSearchHeatmapChart.tsx | 2 +- packages/app/src/components/deltaChartUtils.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.changeset/always-on-distribution.md b/.changeset/always-on-distribution.md index 0f6bfe277f..e6a06ec54d 100644 --- a/.changeset/always-on-distribution.md +++ b/.changeset/always-on-distribution.md @@ -2,4 +2,4 @@ "@hyperdx/app": patch --- -feat: always-on attribute distribution mode for Event Deltas \ No newline at end of file +feat: always-on attribute distribution mode for Event Deltas diff --git a/packages/app/src/components/DBDeltaChart.tsx b/packages/app/src/components/DBDeltaChart.tsx index 41c412b9bf..341db2bc82 100644 --- a/packages/app/src/components/DBDeltaChart.tsx +++ b/packages/app/src/components/DBDeltaChart.tsx @@ -518,7 +518,7 @@ export default function DBDeltaChart({ {isLoading - ? 'Loading...' + ? 'Loading\u2026' : 'Select an area on the chart above to enable comparisons'} @@ -528,7 +528,7 @@ export default function DBDeltaChart({ {isLoading && visibleOnPage.length === 0 && hiddenOnPage.length === 0 && ( - Loading attribute distributions... + Loading attribute distributions\u2026 )} @@ -570,7 +570,7 @@ export default function DBDeltaChart({ labelPosition="left" /> )} - {/* Lower-priority fields — separate grid so rows align independently */} + {/* Lower-priority fields - separate grid so rows align independently */} {hiddenOnPage.length > 0 && (
); -} \ No newline at end of file +} diff --git a/packages/app/src/components/PropertyComparisonChart.tsx b/packages/app/src/components/PropertyComparisonChart.tsx index f1fc0b3b87..799f306c34 100644 --- a/packages/app/src/components/PropertyComparisonChart.tsx +++ b/packages/app/src/components/PropertyComparisonChart.tsx @@ -104,7 +104,7 @@ function TruncatedTick({ x = 0, y = 0, payload }: TickProps) { const value = String(payload?.value ?? ''); const MAX_CHARS = 12; const displayValue = - value.length > MAX_CHARS ? value.slice(0, MAX_CHARS) + '...' : value; + value.length > MAX_CHARS ? value.slice(0, MAX_CHARS) + '\u2026' : value; return ( {value} @@ -321,4 +321,4 @@ export function PropertyComparisonChart({ ); -} \ No newline at end of file +} diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index ae2647844a..f0c8893c77 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -200,4 +200,4 @@ function DBSearchHeatmapForm({ ); -} \ No newline at end of file +} diff --git a/packages/app/src/components/deltaChartUtils.ts b/packages/app/src/components/deltaChartUtils.ts index de1cbb60fc..d9c20f57f3 100644 --- a/packages/app/src/components/deltaChartUtils.ts +++ b/packages/app/src/components/deltaChartUtils.ts @@ -516,4 +516,4 @@ export function semanticBoost(key: string): number { if (lowerKey.endsWith('.' + suffix) || lowerKey === suffix) return 1; } return 0; -} \ No newline at end of file +} From 25ff115cf73808e322b56d111dc483ef5c1c33b2 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev Date: Thu, 12 Mar 2026 15:39:59 -0700 Subject: [PATCH 3/4] fix: prettier formatting in PropertyComparisonChart and DBSearchHeatmapChart Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app/src/components/PropertyComparisonChart.tsx | 5 ++++- packages/app/src/components/Search/DBSearchHeatmapChart.tsx | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/PropertyComparisonChart.tsx b/packages/app/src/components/PropertyComparisonChart.tsx index 799f306c34..69c5919f18 100644 --- a/packages/app/src/components/PropertyComparisonChart.tsx +++ b/packages/app/src/components/PropertyComparisonChart.tsx @@ -271,7 +271,10 @@ export function PropertyComparisonChart({ {clickedValue.length === 0 ? Empty String : clickedValue} - + {hasSelection ? 'Selection' : 'All spans'}:{' '} {(outlierValueOccurences.get(clickedValue) ?? 0).toFixed(1)}% diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index f0c8893c77..8b3fedd186 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -128,9 +128,7 @@ export function DBSearchHeatmapChart({ xMax={fields.xMax} yMin={fields.yMin} yMax={fields.yMax} - onAddFilter={ - onAddFilter ? handleAddFilterAndClearSelection : undefined - } + onAddFilter={onAddFilter ? handleAddFilterAndClearSelection : undefined} spanIdExpression={source.spanIdExpression} /> From 94eeff02b3bfba46b49aa10db4dd6684389b8384 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev Date: Thu, 12 Mar 2026 19:15:56 -0700 Subject: [PATCH 4/4] chore: trigger Vercel redeploy