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
5 changes: 5 additions & 0 deletions .changeset/heatmap-tooltip-percentile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---

Show percentile context in the heatmap hover tooltip
42 changes: 42 additions & 0 deletions packages/app/src/components/DBHeatmapChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,36 @@ export function formatDataForHeatmap({
return [time, bucket, count];
}

/**
* Cumulative share (0-100) of events at or below each y-bucket, keyed by
* that bucket's y-value.
*/
export function computeBucketPercentiles(
data: Mode2DataArray,
): Map<number, number> {
const [, ys, counts] = data;

const bucketTotals = new Map<number, number>();
let total = 0;
for (let i = 0; i < ys.length; i++) {
bucketTotals.set(ys[i], (bucketTotals.get(ys[i]) ?? 0) + counts[i]);
total += counts[i];
}

const percentiles = new Map<number, number>();
if (total === 0) {
return percentiles;
}

let cumulative = 0;
for (const [y, bucketTotal] of [...bucketTotals].sort(([a], [b]) => a - b)) {
cumulative += bucketTotal;
percentiles.set(y, (cumulative / total) * 100);
}

return percentiles;
}

function HeatmapContainer({
config,
enabled = true,
Expand Down Expand Up @@ -1093,6 +1123,11 @@ function Heatmap({
[numberFormatKey, scaleType],
);

const bucketPercentiles = useMemo(
() => computeBucketPercentiles(data),
[data],
);

const options: uPlot.Options = useMemo(() => {
const themedSeries = buildSeriesForPalette(palette);
return {
Expand Down Expand Up @@ -1258,6 +1293,9 @@ function Heatmap({
};
}, [width, height, tickFormatter, scaleType, palette, hasFilter]);

const highlightedPercentile =
highlightedPoint && bucketPercentiles.get(highlightedPoint.yVal);

return (
<div
ref={ref}
Expand Down Expand Up @@ -1349,6 +1387,10 @@ function Heatmap({
</div>
<div>
<b>Y Value:</b> {tickFormatter(highlightedPoint.yVal)}
{highlightedPercentile != null &&
` (p${new Intl.NumberFormat('en-US', {
maximumFractionDigits: 1,
}).format(highlightedPercentile)})`}
</div>
<div>
<b>Count Value:</b>{' '}
Expand Down
55 changes: 54 additions & 1 deletion packages/app/src/components/__tests__/DBHeatmapChart.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { formatDataForHeatmap } from '@/components/DBHeatmapChart';
import {
computeBucketPercentiles,
formatDataForHeatmap,
} from '@/components/DBHeatmapChart';

// CH widthBucket returns buckets 0..nBuckets+1, so each time bucket
// produces nBuckets+2 grid cells.
Expand Down Expand Up @@ -94,3 +97,53 @@ describe('formatDataForHeatmap', () => {
]);
});
});

describe('computeBucketPercentiles', () => {
it('reports the share of events at or below each bucket, pooled across time buckets', () => {
const percentiles = computeBucketPercentiles(
formatDataForHeatmap({
...baseArgs,
data: [
row(T0, 1, '3'),
row(T0, 4, '1'),
row(T1, 1, '5'),
row(T1, 2, '1'),
],
}),
);

expect(percentiles).toEqual(
new Map([
[0, 0],
[1, 80], // 3 + 5 of the 10 events, both time buckets counted
[2, 90],
[3, 90], // empty bucket carries the share of the one below it
[4, 100],
[5, 100],
]),
);
});

it('returns no percentiles when the grid holds no events', () => {
// Every cell is 0, so there is no distribution to place a value in
// the tooltip omits the percentile rather than dividing by zero.
expect(
computeBucketPercentiles(formatDataForHeatmap({ ...baseArgs, data: [] })),
).toEqual(new Map());
});

it('orders buckets by y-value rather than by the order they arrive in', () => {
const percentiles = computeBucketPercentiles([
[0, 0], // times, unused
[5, 1], // y-values, highest first
[1, 3], // counts
]);

expect(percentiles).toEqual(
new Map([
[1, 75],
[5, 100],
]),
);
});
});
Loading