Skip to content

feat: Wire heatmap chart into dashboard editor and tile rendering#2107

Open
alex-fedotyev wants to merge 33 commits intomainfrom
cursor/heatmap-chart-editor-dashboard-5801
Open

feat: Wire heatmap chart into dashboard editor and tile rendering#2107
alex-fedotyev wants to merge 33 commits intomainfrom
cursor/heatmap-chart-editor-dashboard-5801

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented Apr 11, 2026

What

Wires the existing DBHeatmapChart component and DisplayType.Heatmap enum into the chart editor, dashboard tile rendering, and unifies display settings across all heatmap contexts (search Event Deltas, chart editor, dashboards).

Why

Users should be able to create and view heatmap charts from the chart editor and dashboards, alongside existing chart types. Heatmaps visualize distributions over time (e.g., latency distributions) and were previously only available in the search Event Deltas view.

Changes

Shared Display Settings Drawer (HeatmapSettingsDrawer.tsx):

  • Extracted from the inline drawer in DBSearchHeatmapChart.tsx into a reusable component
  • Contains Scale (Log/Linear), Value expression, Count expression
  • Used by search Event Deltas (gear icon), chart editor, and dashboards
  • defaultValues memoized at call sites to prevent unnecessary form resets

Chart Editor (EditTimeChartForm, ChartEditorControls, HeatmapSeriesEditor):

  • Added Heatmap tab with IconGrid3x3 icon
  • Heatmap editor shows Where clause + Display Settings button (opens shared drawer)
  • Single series constraint enforced; validation requires value expression
  • Auto-populates getDurationMsExpression(source) and count() when a Trace source is selected (on tab switch or source change)
  • Sets numberFormat: { output: 'duration', factor: 0.001 } for proper Y-axis labels
  • Generated SQL section hidden for heatmap (queries are built internally by HeatmapContainer)

Dashboard Tiles (DBDashboardPage):

  • HeatmapTile component renders DBHeatmapChart via toHeatmapChartConfig() helper
  • autoRun passed to EditTimeChartForm in dashboard edit modal so saved charts preview immediately
  • numberFormat and scaleType passed through from saved config

Chart Preview (ChartPreviewPanel):

  • HeatmapPreview component renders heatmap in the chart editor preview area

Heatmap UX improvements (DBHeatmapChart.tsx):

  • Dynamic Y-axis sizing via ctx.measureText() — measures actual formatted tick labels
  • Compact tick formatter (formatDurationMsCompact): m instead of min, values under 2min stay as seconds, fewer decimals
  • X-axis: gap: 10 + space: 60 prevents label overlap in narrow containers
  • padding: [8, 8, 0, 4] prevents label clipping at edges
  • Drag-to-select and "Click & Drag" prompt disabled when onFilter is not provided (editor/dashboard); hover tooltip still shows for inspection
  • Full interactivity preserved in search Event Deltas where onFilter is wired

Config helper (toHeatmapChartConfig):

  • Shared function converts builder config to HeatmapChartConfig format
  • Eliminates duplicated config construction and unsafe casts
  • Exported alongside HeatmapChartConfig type from DBHeatmapChart.tsx
Open in Web Open in Cursor 

- Add Heatmap tab to chart editor form with IconGrid3x3 icon
- Create HeatmapSeriesEditor with value expression (Y-axis) and
  count expression (intensity) inputs
- Add heatmap display type to displayTypeToActiveTab mapping
- Add heatmap preview rendering in ChartPreviewPanel
- Add heatmap tile rendering in DBDashboardPage
- Add heatmap validation: single series, required value expression
- Constrain heatmap to single series (no Add Series / ratio)

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 11, 2026

🦋 Changeset detected

Latest commit: 7509050

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/app Minor
@hyperdx/common-utils Patch
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Apr 23, 2026 1:58am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 11, 2026

E2E Test Results

All tests passed • 146 passed • 3 skipped • 1144s

Status Count
✅ Passed 146
❌ Failed 0
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

…ormat, reduce chart whitespace

- When switching to Heatmap with a trace source, auto-fill value
  expression with getDurationMsExpression and set numberFormat to
  duration with factor 0.001 for proper Y-axis labels (4ms, 100ms, 1.2s)
- Pass numberFormat from config through to DBHeatmapChart in both
  ChartPreviewPanel and DBDashboardPage for correct tick formatting
- Reduce Stack gap in ChartContainer from default to 4px to minimize
  whitespace above the chart area

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
…ctivity in editor/dashboard

- Add effect to populate duration defaults when source changes while
  already in heatmap mode (not just when switching to heatmap tab)
- Disable drag-to-select and hide 'Click & Drag' prompt in Heatmap
  when onFilter is not provided (chart editor and dashboard contexts)
- Keep hover tooltip with Y/Count values for read-only inspection

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
…ngs button for heatmap

- Fill Value field with getDurationMsExpression and Count field with
  count() when switching to Heatmap with a trace source (visible text,
  not just behind the scenes)
- Add Display Settings button below heatmap editor, same pattern as
  other chart types, opening ChartDisplaySettingsDrawer for number
  format configuration

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
…hboards

- Extract HeatmapSettingsDrawer into shared component used by both
  search Event Deltas and chart editor/dashboard contexts
- Remove Value/Count fields from the main heatmap editor form — move
  them into the Heatmap Settings drawer (Scale, Value, Count)
- Replace 'Display Settings' button with 'Heatmap Settings' button
  that opens the same drawer as search Event Deltas gear icon
- Pass scaleType (log/linear) from form state through to DBHeatmapChart
  in preview panel and dashboard tiles
- Pre-populate Value with getDurationMsExpression and Count with count()
  from data source config, editable via Heatmap Settings

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
…artContainer gap

- Add padding: [8,0,0,0] to uPlot options to reduce whitespace above
  the topmost Y-axis tick in heatmap charts
- Set lockScroll=false on HeatmapSettingsDrawer to prevent layout shift
  when opening settings inside a dashboard edit modal
- Restore ChartContainer gap to 'xs' (was incorrectly set to 4px
  affecting all chart types)

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
…ChartContainer changes

- Wrap HeatmapSettingsDrawer in Portal so it doesn't participate in
  the flex layout of the editor form, preventing content shift when
  opening/closing settings in dashboard edit modal
- Revert ChartContainer gap/margin changes to preserve existing
  spacing for all chart types

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
cursoragent and others added 2 commits April 13, 2026 16:46
Pass autoRun to EditTimeChartForm in the dashboard edit modal so
saved charts (including heatmaps) render their preview immediately
when opened for editing, matching the Chart Explorer behavior.

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
… layout shift

Move the HeatmapSettingsDrawer out of HeatmapSeriesEditor and render
it at the EditTimeChartForm top level (same pattern as
ChartDisplaySettingsDrawer). This prevents any DOM insertion from
affecting the flex layout between the settings button and run button
when the drawer opens/closes.

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev alex-fedotyev marked this pull request as ready for review April 13, 2026 17:30
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Apr 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 13, 2026

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Cross-layer change: touches frontend (packages/app) + shared utils (packages/common-utils)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 12
  • Production lines changed: 980 (+ 150 in test files, excluded from tier calculation)
  • Branch: cursor/heatmap-chart-editor-dashboard-5801
  • Author: alex-fedotyev

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 13, 2026

PR Review

  • ⚠️ formatDurationMsCompact boundary overflow: Math.round(999.9) produces "1000µs" and "1000ms" at unit boundaries instead of crossing to the next unit ("1ms" / "1s"). These will appear as Y-axis tick labels. Fix: use Math.floor or apply toPrecision consistently at boundaries (e.g., treat ms >= 999.5 as "1s").

  • ⚠️ Hardcoded zIndex: 199 backdrop in HeatmapTile: The manual Portal-based backdrop in DBDashboardPage uses a fixed z-index that may conflict with Mantine's modal/drawer overlay stack. Prefer Popover's built-in closeOnClickOutside to avoid managing backdrop z-index manually.

  • ⚠️ applyHeatmapDefaults hard-codes { output: 'duration', factor: 0.001 } instead of using the already-imported getTraceDurationNumberFormat(tableSource) helper. If the helper's output ever diverges from this literal (e.g., for nanosecond-precision sources), dashboard heatmaps will silently mis-format. Use the helper or remove the unused import.

  • ℹ️ applyHeatmapDefaults sets select and series to the same array reference then mutates via a path-based setValue. This works today with react-hook-form but is fragile — if select and series schemas diverge, silent bugs appear. Consider constructing them separately.

…le form, revert ChartContainer

- Extract toHeatmapChartConfig() helper into DBHeatmapChart.tsx,
  eliminating duplicated config construction and unsafe casts in
  DBDashboardPage and ChartPreviewPanel
- Export HeatmapChartConfig and HeatmapSelectExtras types for typed
  access to countExpression and heatmapScaleType on select items
- Add useEffect to sync HeatmapSettingsDrawer form when defaultValues
  change (prevents stale closure when source changes while drawer open)
- Fully revert ChartContainer to original (no gap change)

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev alex-fedotyev requested review from a team and pulpdrew and removed request for a team April 14, 2026 03:39
Copy link
Copy Markdown
Contributor

@pulpdrew pulpdrew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool stuff, but I have some suggestions.

Also, do we have a followup ticket to update the external API implementation+docs+tests to cover this new visualization type?

Comment on lines +420 to +424
const traceSource =
tableSource?.kind === SourceKind.Trace &&
tableSource.durationExpression
? tableSource
: undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need some sort of messaging or improved error messages here, or some way to make this work by default for non-trace sources.

I was very confused why this just errors out by default for non-Trace sources:

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by restricting the source picker to trace sources only (commit a587ca2). Non-trace sources no longer appear in the dropdown, so the error state isn't reachable. Defaults for trace sources (Duration / 1e6 + count()) auto-populate on tab/source change.

Comment on lines +784 to +793
<HeatmapSettingsDrawer
opened={heatmapSettingsOpened}
onClose={closeHeatmapSettings}
connection={tableConnection}
parentRef={parentRef}
defaultValues={heatmapSettingsDefaults}
scaleType={heatmapScaleType}
onScaleTypeChange={handleHeatmapScaleTypeChange}
onSubmit={handleUpdateHeatmapSettings}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that we're probably doing it this way because the Event Deltas heatmap uses a drawer for the settings. And that makes sense because it is restricted to Trace sources and a particular use case, which have good defaults.

In the context of the chart editor, the point is to be able to edit the chart, and I think it makes less sense to hide the heatmap settings in a drawer. Particularly because for metric and log sources, the defaults error out and it's not at all clear where I would set a 'value expression' if I'm not already aware that the settings are hidden in display settings.

I'd also suggest that the value and count expressions are not display settings but are just as central to this chart as the series are a part of the line chart or table chart - they should be front and center IMO

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping as "advanced settings" in the drawer for now — heatmap is now restricted to trace sources (see the first thread), so value/count auto-populate from the source's duration expression and count(). The original pain (metric/log sources erroring out with no clear place to fix them) is gone.

Agreed this is a design call rather than a hard answer. We'll revisit based on user feedback; if the drawer turns out to be too buried we'll promote them into the main panel.

}

// Validate number and pie charts only have one series
// Validate number, pie, and heatmap charts only have one series
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the unit tests to cover the changes in this file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests in packages/app/src/components/ChartEditor/__tests__/utils.test.ts:

  • validateChartForm — valid heatmap with trace source, multi-series rejection, missing valueExpression rejection
  • Round-trip: countExpression and heatmapScaleType survive convertFormStateToSavedChartConfigconvertSavedChartConfigToFormState

chartConfigForExplanations?: ChartConfigWithOptTimestamp;
onSubmit: (suppressErrorNotification?: boolean) => void;
openDisplaySettings: () => void;
openHeatmapSettings?: () => void;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be optional? Looks like it is always provided

Suggested change
openHeatmapSettings?: () => void;
openHeatmapSettings: () => void;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — (no longer optional).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Apologies, previous reply lost the code snippet to shell escaping.)

Done — openHeatmapSettings: () => void (no ?). Fallback to openDisplaySettings also removed per the next comment.

setValue={setValue}
tableSource={tableSource}
onSubmit={onSubmit}
onOpenDisplaySettings={openHeatmapSettings ?? openDisplaySettings}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we ever want to open the display settings here?

Suggested change
onOpenDisplaySettings={openHeatmapSettings ?? openDisplaySettings}
onOpenHeatmapSettings={openHeatmapSettings}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — onOpenDisplaySettings={openHeatmapSettings} (no fallback).

import { SQLPreview } from './ChartSQLPreview';

/** Compact duration labels for axis ticks — fewer decimals, shorter units. */
function formatDurationMsCompact(ms: number): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on making this a shared util alongside formatDurationMs in packages/app/src/utils.ts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be great if we could add some unit tests here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — moved to packages/app/src/utils.ts alongside formatDurationMs and exported. DBHeatmapChart.tsx now imports it from @/utils.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 8 tests in packages/app/src/__tests__/utils.test.ts covering: zero, negative, ns (< 0.001ms), µs (< 1ms), ms (< 1000ms), seconds (< 2min), minutes (< 1h), and hours.

];
setValue('select', heatmapSeries);
setValue('series', heatmapSeries);
setValue('series.0.countExpression' as any, 'count()');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add countExpression to the SelectItem schema/type so that we don't need a cast here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — see the reply to the HeatmapSelectExtras thread above. countExpression is now part of DerivedColumnSchema and the cast is gone.

setValue('series.0.countExpression' as any, 'count()');
if (traceSource) {
setValue('numberFormat', {
output: 'duration' as any,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed. Casting as any will hide any future type errors, and should be avoided when possible.

Suggested change
output: 'duration' as any,
output: 'duration',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the cast wasn't needed since 'duration' is a valid value in NumberFormatSchema. Now just output: 'duration'.

setValue('select', heatmapSeries);
setValue('series', heatmapSeries);
setValue('series.0.countExpression' as any, 'count()');
setValue('numberFormat', { output: 'duration' as any, factor: 0.001 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cast is not needed

Suggested change
setValue('numberFormat', { output: 'duration' as any, factor: 0.001 });
setValue('numberFormat', { output: 'duration', factor: 0.001 });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — no cast, just setValue('numberFormat', { output: 'duration', factor: 0.001 }).

defaultValues: HeatmapSettingsValues;
scaleType: HeatmapScaleType;
onScaleTypeChange: (v: HeatmapScaleType) => void;
onSubmit: (v: HeatmapSettingsValues) => void;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to combine scaleType into HeatmapSettingsValues instead of treating them separately here and having to handle both handleHeatmapScaleTypeChange and handleUpdateHeatmapSettings in the parent component?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — done. scaleType is now part of HeatmapSettingsSchema:

export const HeatmapSettingsSchema = z.object({
  value: z.string().trim().min(1),
  count: z.string().trim().optional(),
  scaleType: z.enum(['log', 'linear']).default('log'),
});

The drawer manages scaleType via useWatch/setValue like the other fields, and onSubmit now receives all three values together. The parent no longer needs separate scaleType / onScaleTypeChange props or a second handler.

Heatmap charts require trace data (duration expression) to work properly.
Non-trace sources would error out with no guidance. Instead of adding
complex error messaging, filter the source dropdown to only show trace
sources when heatmap display type is selected.
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Pushed a587ca2 — restricts the data source picker to trace sources only when heatmap display type is selected.

This addresses:

  • Non-trace source error UX: the option to pick a non-trace source simply won't appear, so no confusing error state
  • Simplified heatmap defaults: since the source is always a trace, duration formatting is applied unconditionally

The SourceSelectControlled component already supported allowedSourceKinds filtering (used on Sessions, Search pages) — just wired it up for heatmap.

Still working through the remaining review items (removing as any casts, unit tests, schema updates, etc.).

…editor-dashboard-5801

# Conflicts:
#	packages/app/src/components/ChartEditor/utils.ts
… formatted tick labels

The static opt.axes[1].size function was measuring raw values from a
prior uPlot cycle, not the actual formatted labels produced by the
values callback (tickFormatter). This caused the Y-axis to be too
narrow for longer labels like '1.67min'. Moving size into the options
useMemo alongside the values callback ensures it always measures the
same formatted strings that are actually rendered.
…ent Deltas

Drag-select is intercepted by react-grid-layout when the heatmap is
inside a dashboard tile, making the existing drag-based drill-down
unreachable. Replace it with a click-popover pattern matching the line
chart (DBTimeChart): clicking anywhere on the heatmap opens a small
popover with a 'View in Event Deltas' link that preserves source,
where clause, filters, and time range via buildEventsSearchUrl + mode=delta.
.claude/settings.json contains user-local tool permission preferences
and should not be in the repo.
- Deduplicate heatmap defaults setup (previously inlined in two useEffects)
  into applyHeatmapDefaults()
- Add changeset describing the feature for the next release
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Review round-up

Replied to each thread individually. Quick summary:

Drew's comments

  • All 12 threads addressed. as any casts gone, countExpression/heatmapScaleType in DerivedColumnSchema, formatDurationMsCompact moved to utils.ts with 8 unit tests, scaleType combined into HeatmapSettingsValues, openHeatmapSettings non-optional, heatmap validation tests added.
  • Value/count in drawer (thread at line 781): Keeping them in the Display Settings drawer as "advanced settings". Rationale: heatmap is now trace-only, so Duration / 1e6 and count() auto-populate on source selection — the original "not clear where to set value expression" pain is gone. Log + count are the standard shape for span heatmaps in the industry. If user feedback shows otherwise we'll promote them into the main panel.

Claude Code review bot

  • as any casts — fixed via schema extension.
  • handleHeatmapScaleTypeChange not submitting — handler removed; scale type now flows through the unified handleUpdateHeatmapSettings which always calls onSubmit().
  • Duplicate heatmap series init in two useEffects — extracted to applyHeatmapDefaults() helper.
  • exhaustive-deps suppression in the drawer — kept. form.reset(defaultValues) pattern where form is stable from useForm; suppressing is the idiomatic fix.
  • buildChartConfigForExplanations heatmap fallthrough — safe today (SQL preview is hidden for heatmap); if that guard ever moves, this path can be hardened.

New in this round

  • Dashboard drill-down: dashboard heatmap tiles can't use drag-select (react-grid-layout intercepts the drag). Now a click on the tile opens a popover with a "View in Event Deltas" link, matching DBTimeChart's click-popover pattern. Preserves source, where clause, filters, and time range via buildEventsSearchUrl + mode=delta.
  • Y-axis label clipping fix: the size callback was sitting on the static opt object and measuring raw values from a prior uPlot cycle, not the formatted strings produced by the values callback. Moved it into the options useMemo so it measures the same labels (e.g. 1.67min) that actually render.
  • Changeset added.

…n' clicks

The options useMemo depended on the onFilter function reference. Since
DBSearchHeatmapChart passes an inline arrow to onFilter, the reference
changed on every parent render — so after clicking 'Filter by Selection'
(which calls setFields() and triggers a re-render), the options object
recomputed, uPlot re-initialized, and its internal u.select state (the
rectangle) was wiped.

Depend on a boolean (`hasFilter = !!onFilter`) instead of the function
itself. The drag-enabled state is what the options actually care about,
and the boolean only flips when onFilter goes from undefined to defined
(which is a context switch, not a per-render change).
Before: drag → rectangle + 'Filter by Selection' button → click button → filter applies.
After: drag → filter applies immediately; click anywhere on chart clears selection + exits comparison mode.

Rationale: the button was an extra step for the only action that made sense
after a drag. Applying on drag-end matches how selection works in most
analytics tools and removes the 'how do I commit this?' discoverability gap.

- Remove selectingInfo state and the 'Filter by Selection' floating button
- setSelect hook now fires onFilter directly (with log→linear y conversion
  and time-unit scaling that the button did previously)
- Add onClearFilter prop; container's onClick calls u.setSelect(zero) and
  onClearFilter, guarded by a justDraggedAtRef timestamp to ignore the
  synthetic click that follows mouseup after a drag
- Restore uPlot instance capture via onCreate/onDelete for programmatic
  u.select clearing on outside-click
- DBSearchHeatmapChart passes onClearFilter that resets xMin/xMax/yMin/yMax
  URL params (exits comparison mode)
- Tooltip hint: 'Click & Drag to Select Data' → 'Drag to Compare · Click to Clear'
Two related cases where the selection rectangle went away but the URL
xMin/xMax/yMin/yMax params lingered, leaving the delta chart stuck in
comparison mode against stale coordinates:

1. Time range change (time picker) — data refetches, uPlot re-initializes
   (rectangle gone), but the xMin/xMax/yMin/yMax URL params remained and
   the delta chart kept running its comparison against the new time range.
   Added a useEffect that tracks dateRange and clears selection on change.

2. Display settings change (value/count/scaleType via drawer) — a
   rectangle drawn against the old y-axis semantics doesn't map cleanly to
   the new one. The drawer's onSubmit now clears selection alongside the
   new values.
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Hi @pulpdrew — while fixing a regression where the heatmap selection rectangle disappeared after clicking "Filter by Selection" (turned out to be a dep-array issue I introduced in an earlier commit), we went a step further and rethought the drag interaction. Would appreciate a second pair of eyes on this UX change before we call the PR done.

What changed

Before: drag a rectangle → rectangle + floating "Filter by Selection" button appears → click the button → delta comparison runs.

After: drag a rectangle → delta comparison runs immediately; click anywhere on the chart to clear + exit comparison mode.

Why

  • The button was the only sensible action after a drag — making it an extra click didn't add value.
  • It was the main discoverability gap (commit the selection? close it? click away?).
  • Applying on drag-end is the common pattern in analytics tools (Grafana, Datadog).
  • uPlot's dist: 5 threshold already filters accidental drags.

Also cleaned up

  • Time range change — data refetches and the rectangle disappears (uPlot re-inits), but the xMin/xMax/yMin/yMax URL params used to linger, so the delta chart stayed in comparison mode against the new time range. Now cleared.
  • Display settings change — changing value / count / scaleType via the drawer also clears the selection. A rectangle drawn against old axis semantics doesn't map cleanly to new ones.

Open question for you

Concern I'd flag: "drag to immediately run a query" means a slightly-off drag triggers work. The dist: 5 pixel threshold on cursor.drag covers stray clicks, but someone experimenting with a selection no longer has a "commit when ready" moment. If you'd rather keep the button for that reason, happy to revert — let me know.

Commits for reference: 1f2b4fed (the UX change) and 75090506 (time/settings-change clears).

@kodiakhq kodiakhq Bot removed the automerge label Apr 23, 2026
@kodiakhq
Copy link
Copy Markdown
Contributor

kodiakhq Bot commented Apr 23, 2026

This PR currently has a merge conflict. Please resolve this and then re-add the automerge label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants