Skip to content
Draft
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
10 changes: 10 additions & 0 deletions .changeset/add-search-histogram-severity-legend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@hyperdx/app': minor
---

Add a legend below the search histogram showing each series' total across the
entire selected time range, so a breakdown like "how many errors in the last 45
minutes" reads as one number instead of bars to sum by eye. Severity-like groups
are colored semantically and ordered most-severe-first; any other grouping uses
the chart's palette colors ordered by total. Clicking an item narrows the search
to that series.
107 changes: 100 additions & 7 deletions packages/app/src/ChartUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,23 @@ function setLineColors(
});
}

/**
* Stack severity series in a consistent order: info at the bottom, then warn,
* then error on top. Series with no semantic color (any non-log-level grouping)
* all rank equally, so `sort` leaves them in the order the response produced.
*/
function sortLineDataByLogLevel(lineDataMap: {
[keyName: string]: LineDataWithOptionalColor;
}): LineDataWithOptionalColor[] {
const logLevelColorOrder = getLogLevelColorOrder();
return Object.values(lineDataMap).sort((a, b) => {
return (
logLevelColorOrder.findIndex(color => color === a.color) -
logLevelColorOrder.findIndex(color => color === b.color)
);
});
}

function firstGroupColumnIsLogLevel(
source: TSource | undefined,
groupColumns: ColumnMetaType[],
Expand Down Expand Up @@ -736,13 +753,7 @@ export function formatResponseForTimeChart({
});
}

const logLevelColorOrder = getLogLevelColorOrder();
const sortedLineData = Object.values(lineDataMap).sort((a, b) => {
return (
logLevelColorOrder.findIndex(color => color === a.color) -
logLevelColorOrder.findIndex(color => color === b.color)
);
});
const sortedLineData = sortLineDataByLogLevel(lineDataMap);

if (generateEmptyBuckets && granularity != null) {
const generatedTsBuckets = timeBucketByGranularity(
Expand Down Expand Up @@ -795,6 +806,88 @@ export function formatResponseForTimeChart({
};
}

/** One chart series collapsed to a single total over the whole date range. */
export type SeriesTotal = {
/** The series key, identical to the chart's `Bar`/`Area` dataKey. */
dataKey: string;
displayName: string;
/** The color the chart draws this series with. */
color: string;
total: number;
};

/**
* Collapse a time-series response into one total per series, spanning the
* entire date range rather than a single bucket.
*
* This deliberately runs the same pipeline as `formatResponseForTimeChart`
* (identical series keys, the same stacking order, and `setLineColors` for
* colors) so a totals view can never disagree with the chart it summarizes —
* semantic colors when a group value looks like a log level, palette colors
* otherwise. Re-deriving any of that separately would drift the moment either
* side changed.
*
* Only the current period is summed: a previous-period comparison covers a
* different window, so folding it into a "total for this range" would be wrong.
*/
export function formatResponseForSeriesTotals({
response,
source,
}: {
response: ResponseJSON<Record<string, any>>;
source?: TSource;
}): {
seriesTotals: SeriesTotal[];
groupColumns: string[];
isSingleValueColumn: boolean;
} {
const meta = response.meta;
if (meta == null) {
throw new Error('No meta data found in response');
}

const valueColumns = inferValueColumns(meta, new Set()) ?? [];
const groupColumns = inferGroupColumns(meta) ?? [];

const tsBucketMap: Map<number, Record<string, any>> = new Map();
const lineDataMap: { [keyName: string]: LineDataWithOptionalColor } = {};

addResponseToFormattedData({
response,
lineDataMap,
tsBucketMap,
source,
isPreviousPeriod: false,
previousPeriodOffsetSeconds: 0,
});

const lineData = setLineColors(sortLineDataByLogLevel(lineDataMap));

const totalByDataKey = new Map<string, number>();
for (const bucket of tsBucketMap.values()) {
for (const line of lineData) {
const value = bucket[line.dataKey];
if (typeof value === 'number' && Number.isFinite(value)) {
totalByDataKey.set(
line.dataKey,
(totalByDataKey.get(line.dataKey) ?? 0) + value,
);
}
}
}

return {
seriesTotals: lineData.map(line => ({
dataKey: line.dataKey,
displayName: line.displayName || line.dataKey,
color: line.color,
total: totalByDataKey.get(line.dataKey) ?? 0,
})),
groupColumns: groupColumns.map(g => g.name),
isSingleValueColumn: valueColumns.length === 1,
};
}

// Define a mapping from app AggFn to common-utils AggregateFunction
const mapV1AggFnToV2 = (aggFn?: AggFn): AggFnV2 | undefined => {
if (aggFn == null) {
Expand Down
18 changes: 18 additions & 0 deletions packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { ErrorBoundary } from '@/components/Error/ErrorBoundary';
import { FavoriteButton } from '@/components/FavoriteButton';
import { InputControlled } from '@/components/InputControlled';
import OnboardingModal from '@/components/OnboardingModal';
import SearchHistogramLegend from '@/components/SearchHistogramLegend';
import SearchWhereInput, {
getStoredLanguage,
} from '@/components/SearchInput/SearchWhereInput';
Expand Down Expand Up @@ -2433,6 +2434,14 @@ export function DBSearchPage() {
/>
</Box>
)}
{!hasQueryError && (
<SearchHistogramLegend
config={histogramTimeChartConfig}
queryKeyPrefix={QUERY_KEY_PREFIX}
sourceId={searchedConfig.source ?? undefined}
onFocusSeries={handleFocusSeries}
/>
)}
<Box flex="1" mih="0" px="sm">
<PatternTable
source={searchedSource}
Expand Down Expand Up @@ -2534,6 +2543,15 @@ export function DBSearchPage() {
/>
</Box>
)}
{!hasQueryError && (
<SearchHistogramLegend
config={histogramTimeChartConfig}
queryKeyPrefix={QUERY_KEY_PREFIX}
sourceId={searchedConfig.source ?? undefined}
enableParallelQueries
onFocusSeries={handleFocusSeries}
/>
)}
</>
)}
{hasQueryError && queryError ? (
Expand Down
Loading
Loading