Heatmap Widget

Heatmap Widget Overview

The Heatmap Widget renders a multi-unit peri-stimulus time heatmap showing the firing rate (or other scalar metric) of discrete events (DigitalEventSeries) across multiple units, aligned to a user-selected reference event or interval. Each row of the heatmap represents one unit’s rate profile over relative time, with color intensity encoding the magnitude. This provides a compact population-level view that complements the single-unit PSTHWidget and trial-level EventPlotWidget.

The heatmap is constructed by performing the same per-unit rate estimation as PSTHWidget — binning aligned spike times into uniform time bins and optionally smoothing — and then arranging each unit’s rate profile as a row of colored cells. The result is a units × time_bins matrix visualized with a configurable colormap.

Architecture

The widget follows the standard plot widget directory layout:

Plots/HeatmapWidget/
├── CMakeLists.txt
├── HeatmapWidgetRegistration.hpp    # EditorRegistry registration module
├── HeatmapWidgetRegistration.cpp
├── Core/
│   ├── HeatmapState.hpp             # QObject state class + serializable struct
│   ├── HeatmapState.cpp
│   ├── HeatmapDataPipeline.hpp      # Pure-data pipeline (testable without OpenGL)
│   └── HeatmapDataPipeline.cpp
├── Rendering/
│   ├── HeatmapOpenGLWidget.hpp      # OpenGL rendering + mouse interaction
│   └── HeatmapOpenGLWidget.cpp
└── UI/
    ├── HeatmapWidget.hpp            # Main composite widget (axes + OpenGL + layout)
    ├── HeatmapWidget.cpp
    ├── HeatmapWidget.ui
    ├── HeatmapPropertiesWidget.hpp  # Properties/settings panel
    ├── HeatmapPropertiesWidget.cpp
    └── HeatmapPropertiesWidget.ui

Component Overview

Component Responsibility
HeatmapWidget (UI) Top-level Qt widget — composes OpenGL canvas, relative time axis, vertical axis
HeatmapPropertiesWidget (UI) Properties panel — alignment settings, axis range controls (via collapsible sections)
HeatmapOpenGLWidget (Rendering) OpenGL canvas — heatmap scene building, rendering, mouse interaction
HeatmapState (Core) Serializable state — alignment, view state, background color
HeatmapDataPipeline (Core) Pure-data pipeline — gather, estimate, scale, convert (testable without OpenGL)
HeatmapWidgetRegistration Registers type with EditorRegistry (state, view, properties factories)

EditorState Pattern

The Heatmap Widget follows the project’s EditorState architecture:

  • HeatmapState inherits from EditorState
  • Registered with EditorRegistry via HeatmapWidgetModule::registerTypes()
  • HeatmapWidget is the view factory, HeatmapPropertiesWidget is the properties factory
  • Both share the same HeatmapState instance
  • Properties placed in Right zone, view placed in Center zone (default)
  • The create_editor_custom factory in HeatmapWidgetRegistration handles coupling the view and properties widgets (e.g., connecting range controls and forwarding timePositionSelected to EditorRegistry::setCurrentTime())

State Management

HeatmapStateData

The serializable state struct (defined in HeatmapState.hpp alongside HeatmapState) contains all persistent configuration:

struct HeatmapStateData {
    std::string instance_id;
    std::string display_name = "Heatmap Plot";
    PlotAlignmentData alignment;
    CorePlotting::ViewStateData view_state;
    RelativeTimeAxisStateData time_axis;
    VerticalAxisStateData vertical_axis;
    std::string background_color = "#FFFFFF";
    std::vector<std::string> unit_keys;              ///< Selected DigitalEventSeries keys
    WhiskerToolbox::Plots::ScalingMode scaling;      ///< Default: FiringRateHz
    WhiskerToolbox::Plots::EstimationParams estimation_params;  ///< Default: BinningParams{}
    HeatmapColorRangeConfig color_range;             ///< Auto/Manual/Symmetric
    CorePlotting::Colormaps::ColormapPreset colormap; ///< Default: Inferno
    HeatmapSortMode sort_mode = HeatmapSortMode::Manual;
    bool sort_ascending = true;
};

Note that HeatmapStateData is defined in the same header as HeatmapState rather than in a separate HeatmapStateData.hpp. This is the same pattern used by PSTHWidget.

Signal Architecture

HeatmapState emits category-level signals to minimize coupling complexity:

Signal Trigger
alignmentEventKeyChanged Alignment event/interval key changed
intervalAlignmentTypeChanged Interval alignment type changed
offsetChanged Alignment offset changed
windowSizeChanged Window size changed (also resets zoom/pan to fit)
viewStateChanged Zoom, pan, or bounds changed (view transform only — no scene rebuild)
backgroundColorChanged Background color changed
estimationParamsChanged EstimationParams variant changed (method or parameters)
scalingChanged Scaling mode changed
colorRangeChanged Color range mode or bounds changed

Performance-critical separation: Zoom and pan operations emit only viewStateChanged(), which updates the projection matrix without triggering an expensive scene rebuild via stateChanged().

Composition with PlotAlignmentState

HeatmapState owns a PlotAlignmentState instance that manages alignment settings (event key, interval alignment type, offset, window size). This is the same shared component used by EventPlotWidget and PSTHWidget, ensuring consistent alignment behavior across all trial-aligned plot types.

Composition with Axis States

HeatmapState owns both:

  • RelativeTimeAxisState — for the X-axis (relative time centered at t = 0). Synchronized bidirectionally with the view state using the “silent update” pattern described in the Plot Widget Guide.
  • VerticalAxisState — for the Y-axis (unit index). The Y bounds are auto-adjusted when unitCountChanged is emitted by the OpenGL widget, along with zoom/pan reset so all units are visible. Uses identityAxis(\"Unit\", 0) mapping for integer labels.

The window size change handler resets the X view state bounds (x_min, x_max) and zoom/pan to fit the new window, then propagates to both viewStateChanged and stateChanged.

Rendering Pipeline

Data Flow

The data pipeline has been extracted into a pure free function runHeatmapPipeline() in HeatmapDataPipeline.hpp/cpp. This enables comprehensive testing of the gather → estimate → scale → convert pipeline without any OpenGL context. HeatmapOpenGLWidget::rebuildScene() delegates to this function for data processing, then handles colormap selection and OpenGL scene upload.

HeatmapState (alignment + estimation_params + scaling + sort_mode)
        ↓
runHeatmapPipeline()  [HeatmapDataPipeline.cpp — testable free function]
  ├─ createUnitGatherContexts() — one UnitGatherContext per selected unit
  ├─ estimateRates(contexts, window_size, estimationParams())
  ├─ applyScaling(rate_estimate, scaling()) per unit
  └─ → HeatmapPipelineResult { rows, rate_estimates, success }
        ↓
computeSortOrder() + applySortOrder()  [if sort_mode != Manual]
        ↓
HeatmapMapper::buildScene() — colormap + color range → RenderableRectangleBatch
        ↓
SceneRenderer::uploadScene() + render()

HeatmapOpenGLWidget::rebuildScene() implements this pipeline:

  1. Guard: returns early if no units are selected or alignment state is missing
  2. Calls createUnitGatherContexts() to gather trial-aligned DigitalEventSeries for all selected units
  3. Calls estimateRates() passing _state->estimationParams() directly (no conversion needed)
  4. Calls applyScaling() in-place on each RateEstimate using _state->scaling()
  5. Converts RateEstimate objects to HeatmapRowData using times[] bin centers and metadata.sample_spacing
  6. Applies row sorting via computeSortOrder() and applySortOrder() if sort mode is not Manual
  7. Calls HeatmapMapper::buildScene() with the colormap and HeatmapColorRange from state
  8. Uploads via _scene_renderer.uploadScene() and emits unitCountChanged

Interaction

Zoom and Pan

The widget supports separated X/Y zoom for independent exploration of time and unit dimensions, using the shared PlotInteractionHelpers:

Input Action
Mouse wheel X-zoom only (time axis)
Shift + wheel Y-zoom only (unit axis)
Ctrl + wheel Uniform zoom (both axes)
Click + drag Pan in both axes

The interaction delegates to WhiskerToolbox::Plots::handlePanning() and WhiskerToolbox::Plots::handleZoom() from PlotInteractionHelpers, which are shared across all OpenGL plot widgets. A DRAG_THRESHOLD of 5 pixels prevents accidental pans during clicks.

Zoom and pan update only the view projection (no scene rebuild), providing smooth interactive performance.

Double Click

Double-clicking emits plotDoubleClicked(time_frame_index), which the parent HeatmapWidget converts to a TimePosition and forwards to EditorRegistry::setCurrentTime(). This navigates other time-synced widgets to the clicked time position.

Hover

Hovering over a heatmap row displays a tooltip showing the DigitalEventSeries key name corresponding to the Y-axis position under the cursor. The tooltip is managed by the shared PlotTooltipManager and correctly reflects the current sort order (the cached _display_unit_keys vector is updated after each sort).

Current implementation: The tooltip displays the unit key name as plain text.

Planned: The tooltip will be extended to show a miniature stacked PSTH histogram and raster plot for the hovered unit, rendered on-demand using cached GatherResult data. See roadmap Phase 8 for the full implementation plan, including rendering approach and layout.

Selection

Single-clicking a heatmap row identifies the DigitalEventSeries key corresponding to the Y-axis position. A signal is emitted carrying that key via SelectionContext, which companion widgets can respond to:

  • PSTHWidget — updates to display the selected unit’s PSTH histogram
  • EventPlotWidget — updates to display the selected unit’s trial-level raster plot

This enables rapid population-level exploration: clicking rows in the heatmap drives synchronized detail views without manual widget configuration. Widgets can be pinned to opt out of responding to selection changes, preserving a fixed view while the heatmap is browsed. See roadmap Phase 7 for the full SelectionContext integration design.

Axis System

Time Axis (X)

Uses the shared RelativeTimeAxisWidget with RelativeTimeAxisState. The axis is synchronized bidirectionally with the view state:

  • User changes axis range → view state updates → scene rebuilds
  • User zooms/pans → axis range updates silently (no feedback loop)

See Plot Widget Guide for full axis integration details.

Unit Axis (Y)

Uses a VerticalAxisWidget with VerticalAxisState. The Y bounds are automatically set when the OpenGL widget reports the unit count via unitCountChanged. The widget auto-fits by resetting Y zoom to 1.0 and Y pan to 0.0. Uses identityAxis("Unit", 0) for integer unit-index labels.

Properties Panel

The HeatmapPropertiesWidget provides the following collapsible sections:

  1. PlotAlignmentWidget — Shared alignment UI (event/interval key, alignment type, offset, window size).
  2. Unit SelectionFeature_Tree_Widget filtered to DigitalEventSeries, with prefix-based group toggling. Syncs with HeatmapState::unit_keys.
  3. Scaling & Estimation — Composable controls from the RateEstimationControls library:
    • EstimationMethodControls: method combo (Histogram Binning / Gaussian Kernel / Causal Exponential) with a QStackedWidget showing per-method parameter spinboxes. Emits paramsChanged(EstimationParams) → connected directly to HeatmapState::setEstimationParams(). Stub methods show a “Not yet implemented” page.
    • ScalingModeControls: scaling mode combo auto-populated from allScalingModes(). Emits scalingModeChanged(ScalingMode) → connected directly to HeatmapState::setScaling().
    • ColormapControls (from Plots/Common/ColormapControls): colormap preset combo (with painted gradient icon previews), color range mode combo (Auto / Manual / Symmetric) with vmin/vmax spinboxes (visible only in Manual mode). Emits colormapChanged(ColormapPreset)HeatmapState::setColormapPreset() and colorRangeChanged(ColorRangeConfig)HeatmapState::setColorRange().
  4. Sorting — Collapsible section with a sort mode combo box (Manual / Time to Peak / Peak Rate / Mean Rate / Alphabetical) and an ascending/descending checkbox. Sort mode changes emit sortChanged() → triggers scene rebuild. Time-to-Peak sorting produces the characteristic “diagonal stripe” pattern for temporal sequence visualization. The sorted order is reflected in the Y-axis row positions.
  5. Time Axis Range ControlsRelativeTimeAxisRangeControls in a collapsible Section.
  6. Y-Axis Range ControlsVerticalAxisRangeControls in a collapsible Section.

The EstimationParams value flows unchanged from UI → state → renderer: no std::visit or intermediate extraction is needed at the widget layer. The _updating_ui anti-recursion guard prevents feedback loops between state signals and the UI controls.

Visual Customization

Colormap

The mapping from scalar rate values to RGBA display colors is configurable via a colormap. Each cell’s value in the units × time_bins matrix passes through the colormap to produce its rendered color, with intensity encoding magnitude.

The default colormap is Inferno (perceptually uniform, dark-to-bright), which is accessible for color vision deficiency. Available presets:

Preset Characteristics Recommended For
Inferno Perceptually uniform, dark-to-bright Firing rate (default)
Viridis Perceptually uniform, blue-green-yellow General use
Magma Perceptually uniform, dark-to-pink Firing rate
Plasma Perceptually uniform, blue-to-yellow General use
Coolwarm Diverging blue-white-red Z-score scaling
Grayscale Simple grayscale Publication printing
Hot Black-red-yellow-white High-contrast display

The colormap is selected from the properties panel via a combo box with a visual gradient preview. When z-score scaling is active, the Coolwarm diverging colormap is automatically suggested. See roadmap Phase 2 for the shared colormap infrastructure design and roadmap Phase 9 for the selection UI.

Export

PNG Export

The heatmap can be exported as a PNG image capturing the current view. The basic export uses Qt’s QOpenGLWidget::grabFramebuffer() to capture exactly what is displayed, including the current zoom and pan state.

Planned export options (see roadmap Phase 10):

  • High-resolution export — render at a user-specified scale factor (2×, 4×) for publication quality
  • Composite export — include axis labels and colorbar alongside the heatmap in a single image
  • SVG export — resolution-independent output, to be adopted when shared SVG infrastructure is available from the PSTH Roadmap Phase 5

Export will be accessible via a button in the properties panel and a right-click context menu on the plot.

Relationship to Other Widgets

The Heatmap Widget is designed as the population-level companion to the single-unit PSTHWidget and trial-level EventPlotWidget:

Shared Component HeatmapWidget EventPlotWidget PSTHWidget
PlotAlignmentWidget Yes Yes Yes
PlotAlignmentState Yes Yes Yes
RelativeTimeAxisWidget Yes Yes Yes
GatherResult Yes (per-unit, aggregated) Yes (per-trial views) Yes (aggregated)
EventRateEstimation library Yes (per-unit) N/A Yes (single-unit)
EstimationParams / EstimationMethodControls Yes N/A Planned
ScalingMode / ScalingModeControls Yes N/A Planned
SelectionContext linking Planned (source) Planned Planned
OpenGL rendering SceneRenderer SceneRenderer SceneRenderer
Zoom/Pan interaction PlotInteractionHelpers PlotInteractionHelpers PlotInteractionHelpers

The intended workflow is:

  1. HeatmapWidget shows all units at a glance, colored by firing rate
  2. Row selection drives PSTHWidget and EventPlotWidget to show the selected unit’s detailed histogram and raster
  3. Row hover shows a miniature stacked PSTH + raster tooltip for that unit without changing the selection

This three-widget ensemble provides a complete neural population analysis view. See the roadmap for implementation plans.

Testing

Test Files

  • Properties widget observer tests follow the common pattern in tests/WhiskerToolbox/Plots/HeatmapWidget/
  • Common plot widget tests are instantiated via PlotWidgetCommonTests.hpp (see Plot Widget Guide)

See Also

  • Plot Widget Guide — Shared architecture, axis types, DataManager integration
  • PSTHWidget — Single-unit PSTH; shares rate estimation and histogram infrastructure
  • EventPlotWidget — Trial-level raster plot; shares alignment and GatherResult infrastructure
  • HeatmapWidget Roadmap — Planned features and development phases