Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ec48095
feat: wire heatmap chart into dashboard editor and rendering
cursoragent Apr 11, 2026
1ee7f0f
feat: auto-populate duration defaults for trace sources, pass numberF…
cursoragent Apr 11, 2026
21dfd1a
fix: add tableSource to useEffect dependency array
cursoragent Apr 11, 2026
f21af1f
feat: auto-populate heatmap defaults on source change, disable intera…
cursoragent Apr 11, 2026
9a15708
feat: fill Value/Count fields with actual defaults, add Display Setti…
cursoragent Apr 11, 2026
58011b3
refactor: unify Heatmap Settings across search, chart editor, and das…
cursoragent Apr 11, 2026
a7d7646
fix: reduce uPlot top padding, prevent drawer scroll lock, restore Ch…
cursoragent Apr 13, 2026
aa64f1a
fix: prevent layout shift by portaling HeatmapSettingsDrawer, revert …
cursoragent Apr 13, 2026
1f5b425
fix: auto-run chart preview when editing dashboard tiles
cursoragent Apr 13, 2026
09854eb
rename: Heatmap Settings → Display Settings for consistency
cursoragent Apr 13, 2026
9b4dc70
fix: lift HeatmapSettingsDrawer to EditTimeChartForm level to prevent…
cursoragent Apr 13, 2026
1895cee
refactor: address code review — extract toHeatmapChartConfig, fix sta…
cursoragent Apr 13, 2026
0160d84
merge: resolve conflict with main (raw SQL alert validation)
cursoragent Apr 13, 2026
af06542
fix: remove unused HeatmapSelectExtras export, memoize defaultValues
cursoragent Apr 13, 2026
b392516
fix: widen Y-axis and add right padding to prevent label clipping
cursoragent Apr 13, 2026
78c26de
fix: dynamic Y-axis sizing and compact tick labels
cursoragent Apr 13, 2026
d68bb1a
fix: prettier formatting in formatDurationMsCompact
cursoragent Apr 13, 2026
9772d1f
fix: X-axis label overlap, replace IIFEs with components
cursoragent Apr 13, 2026
2721fb6
fix: increase X-axis min tick spacing to prevent cramped labels in sm…
cursoragent Apr 13, 2026
badd8d5
fix: add 4px left padding to prevent Y-axis label clipping
cursoragent Apr 13, 2026
58fd368
fix: add left padding to search heatmap container to prevent Y-axis c…
cursoragent Apr 14, 2026
7180587
fix: add right padding to search heatmap container to match left
cursoragent Apr 14, 2026
59b2476
fix: hide Generated SQL for heatmap, clean up useEffect deps
cursoragent Apr 14, 2026
a587ca2
fix: restrict heatmap data source picker to trace sources only
alex-fedotyev Apr 15, 2026
f730d48
Merge remote-tracking branch 'origin/main' into cursor/heatmap-chart-…
alex-fedotyev Apr 21, 2026
be51b97
fix: move Y-axis size calculation into options useMemo so it measures…
alex-fedotyev Apr 22, 2026
435663b
feat: add click-popover drill-down from dashboard heatmap tiles to Ev…
alex-fedotyev Apr 23, 2026
a285969
chore: remove accidentally-committed local Claude Code settings
alex-fedotyev Apr 23, 2026
e676713
Merge remote-tracking branch 'origin/main' into cursor/heatmap-chart-…
alex-fedotyev Apr 23, 2026
e89d581
refactor: extract heatmap default-series helper, add changeset
alex-fedotyev Apr 23, 2026
c8970d6
fix: persist heatmap drag-select rectangle across 'Filter by Selectio…
alex-fedotyev Apr 23, 2026
1f2b4fe
refactor: heatmap drag-to-compare UX — drop 'Filter by Selection' button
alex-fedotyev Apr 23, 2026
7509050
fix: clear heatmap selection when time range or display settings change
alex-fedotyev Apr 23, 2026
41fff1c
Merge remote-tracking branch 'origin/main' into cursor/heatmap-chart-…
alex-fedotyev Apr 23, 2026
4e1d46d
fix: address heatmap review feedback round 2
alex-fedotyev Apr 27, 2026
7ec8cad
Merge remote-tracking branch 'origin/main' into cursor/heatmap-chart-…
alex-fedotyev Apr 27, 2026
51bb761
feat(heatmap): show bounds + bucket SQL preview, hide MV indicator
alex-fedotyev Apr 28, 2026
6fb0c0d
fix(heatmap): remove unused DisplayType and formatDurationMs imports
alex-fedotyev Apr 28, 2026
0ee9fa5
Merge branch 'main' into cursor/heatmap-chart-editor-dashboard-5801
kodiakhq[bot] Apr 28, 2026
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
13 changes: 13 additions & 0 deletions .changeset/heatmap-chart-editor-dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@hyperdx/app': minor
'@hyperdx/common-utils': patch
---

feat: heatmap charts in chart editor and dashboards

- Heatmap is now a selectable display type in the chart editor tabs
- Dashboard tiles render heatmaps via the shared `DBHeatmapChart` component
- Heatmap source picker restricted to trace sources; value/count expressions auto-populate from the source's duration expression
- Display Settings drawer (scale, value, count) shared across search Event Deltas, chart editor, and dashboards
- Click a dashboard heatmap tile to open Event Deltas with source, where clause, filters, and time range preserved
- Dynamic Y-axis sizing measures formatted tick labels so long labels (e.g. "1.67min") are not clipped
156 changes: 155 additions & 1 deletion packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from '@hyperdx/common-utils/dist/guards';
import {
AlertState,
BuilderChartConfigWithDateRange,
ChartConfigWithDateRange,
DashboardContainer as DashboardContainerSchema,
DashboardFilter,
Expand All @@ -44,6 +45,7 @@ import {
SearchConditionLanguage,
SourceKind,
SQLInterval,
TSource,
} from '@hyperdx/common-utils/dist/types';
import {
ActionIcon,
Expand All @@ -57,6 +59,8 @@ import {
Menu,
Modal,
Paper,
Popover,
Portal,
Stack,
Text,
Tooltip,
Expand All @@ -77,6 +81,7 @@ import {
IconPlayerPlay,
IconPlus,
IconRefresh,
IconSearch,
IconSquaresDiagonal,
IconTags,
IconTrash,
Expand Down Expand Up @@ -114,6 +119,9 @@ import useDashboardContainers, {
import { calculateNextTilePosition, makeId } from '@/utils/tilePositioning';

import ChartContainer from './components/charts/ChartContainer';
import DBHeatmapChart, {
toHeatmapChartConfig,
} from './components/DBHeatmapChart';
import { DBPieChart } from './components/DBPieChart';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import OnboardingModal from './components/OnboardingModal';
Expand All @@ -126,7 +134,11 @@ import { useDashboardRefresh } from './hooks/useDashboardRefresh';
import useTileSelection from './hooks/useTileSelection';
import { useBrandDisplayName } from './theme/ThemeProvider';
import { parseAsJsonEncoded, parseAsStringEncoded } from './utils/queryParsers';
import { buildTableRowSearchUrl, DEFAULT_CHART_CONFIG } from './ChartUtils';
import {
buildEventsSearchUrl,
buildTableRowSearchUrl,
DEFAULT_CHART_CONFIG,
} from './ChartUtils';
import { useConnections } from './connection';
import { useDashboard } from './dashboard';
import DashboardFilters from './DashboardFilters';
Expand All @@ -149,6 +161,135 @@ import { useZIndex, ZIndexContext } from './zIndex';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';

function HeatmapTile({
keyPrefix,
chartId,
title,
toolbar,
queriedConfig,
source,
dateRange,
}: {
keyPrefix: string;
chartId: string;
title: React.ReactNode;
toolbar: React.ReactNode[];
queriedConfig: BuilderChartConfigWithDateRange;
source: TSource | undefined;
dateRange: [Date, Date];
}) {
const { heatmapConfig, scaleType } = toHeatmapChartConfig(queriedConfig);

const [clickPos, setClickPos] = useState<{ x: number; y: number } | null>(
null,
);
const containerRef = useRef<HTMLDivElement>(null);

const eventDeltasUrl = useMemo(() => {
if (!source) return null;
const url = buildEventsSearchUrl({
source,
config: queriedConfig,
dateRange,
});
if (!url) return null;
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}mode=delta`;
}, [source, queriedConfig, dateRange]);

const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!eventDeltasUrl) return;
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
setClickPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
},
[eventDeltasUrl],
);

const dismiss = useCallback(() => setClickPos(null), []);

return (
<div
ref={containerRef}
style={{ position: 'relative', width: '100%', height: '100%' }}
onClick={handleClick}
>
<DBHeatmapChart
key={`${keyPrefix}-${chartId}`}
title={title}
toolbarPrefix={toolbar}
config={heatmapConfig}
scaleType={scaleType}
showLegend
/>
{clickPos != null && eventDeltasUrl != null && (
<>
<Portal>
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 199,
}}
onClick={e => {
e.stopPropagation();
e.preventDefault();
dismiss();
}}
onMouseDown={e => e.stopPropagation()}
/>
</Portal>
<Popover
opened
onChange={opened => {
if (!opened) dismiss();
}}
position="bottom-start"
offset={4}
withinPortal
closeOnEscape
withArrow
shadow="md"
>
<Popover.Target>
<div
style={{
position: 'absolute',
left: clickPos.x,
top: clickPos.y,
width: 1,
height: 1,
pointerEvents: 'none',
}}
/>
</Popover.Target>
<Popover.Dropdown
p="xs"
onClick={e => e.stopPropagation()}
onMouseDown={e => e.stopPropagation()}
>
<Link
data-testid="heatmap-view-event-deltas-link"
href={eventDeltasUrl}
onClick={dismiss}
>
<Group gap="xs">
<IconSearch size={16} />
View in Event Deltas
</Group>
</Link>
</Popover.Dropdown>
</Popover>
</>
)}
</div>
);
}

const ReactGridLayout = WidthProvider(RGL);

type MoveTarget = {
Expand Down Expand Up @@ -658,6 +799,18 @@ const Tile = forwardRef(
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Heatmap &&
isBuilderChartConfig(queriedConfig) && (
<HeatmapTile
keyPrefix={keyPrefix}
chartId={chart.id}
title={title}
toolbar={toolbar}
queriedConfig={queriedConfig}
source={source}
dateRange={dateRange}
/>
)}
{effectiveMarkdownConfig?.displayType ===
DisplayType.Markdown &&
'markdown' in effectiveMarkdownConfig && (
Expand Down Expand Up @@ -873,6 +1026,7 @@ const EditTileModal = ({
onClose={handleClose}
onDirtyChange={setHasUnsavedChanges}
isDashboardForm
autoRun
/>
</ZIndexContext.Provider>
)}
Expand Down
47 changes: 47 additions & 0 deletions packages/app/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as utils from '../utils';
import {
formatAttributeClause,
formatDurationMs,
formatDurationMsCompact,
formatNumber,
getAllMetricTables,
getMetricTableName,
Expand Down Expand Up @@ -1123,3 +1124,49 @@ describe('mapKeyBy', () => {
expect(result.get('a')).toBe(data.at(1));
});
});

describe('formatDurationMsCompact', () => {
it('returns 0 for zero', () => {
expect(formatDurationMsCompact(0)).toBe('0');
});

it('formats negative values', () => {
expect(formatDurationMsCompact(-5)).toBe('-5ms');
});

it('formats nanoseconds (< 0.001 ms)', () => {
expect(formatDurationMsCompact(0.0005)).toBe('500ns');
expect(formatDurationMsCompact(0.00012)).toBe('120ns');
});

it('formats microseconds (< 1 ms)', () => {
expect(formatDurationMsCompact(0.005)).toBe('5µs');
expect(formatDurationMsCompact(0.5)).toBe('500µs');
expect(formatDurationMsCompact(0.123)).toBe('123µs');
});

it('formats milliseconds (< 1000 ms)', () => {
expect(formatDurationMsCompact(5)).toBe('5ms');
expect(formatDurationMsCompact(5.67)).toBe('5.7ms');
expect(formatDurationMsCompact(100)).toBe('100ms');
expect(formatDurationMsCompact(999)).toBe('999ms');
});

it('formats seconds (< 2 min)', () => {
expect(formatDurationMsCompact(1000)).toBe('1s');
expect(formatDurationMsCompact(5432)).toBe('5.43s');
expect(formatDurationMsCompact(60_000)).toBe('60s');
expect(formatDurationMsCompact(119_999)).toBe('120s');
});

it('formats minutes (< 1 hour)', () => {
expect(formatDurationMsCompact(120_000)).toBe('2m');
expect(formatDurationMsCompact(300_000)).toBe('5m');
expect(formatDurationMsCompact(3_599_999)).toBe('60m');
});

it('formats hours (>= 1 hour)', () => {
expect(formatDurationMsCompact(3_600_000)).toBe('1h');
expect(formatDurationMsCompact(7_200_000)).toBe('2h');
});
});
Loading
Loading