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