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:
HeatmapStateinherits fromEditorState- Registered with
EditorRegistryviaHeatmapWidgetModule::registerTypes() HeatmapWidgetis the view factory,HeatmapPropertiesWidgetis the properties factory- Both share the same
HeatmapStateinstance - Properties placed in Right zone, view placed in Center zone (default)
- The
create_editor_customfactory inHeatmapWidgetRegistrationhandles coupling the view and properties widgets (e.g., connecting range controls and forwardingtimePositionSelectedtoEditorRegistry::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 whenunitCountChangedis emitted by the OpenGL widget, along with zoom/pan reset so all units are visible. UsesidentityAxis(\"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:
- Guard: returns early if no units are selected or alignment state is missing
- Calls
createUnitGatherContexts()to gather trial-alignedDigitalEventSeriesfor all selected units - Calls
estimateRates()passing_state->estimationParams()directly (no conversion needed) - Calls
applyScaling()in-place on eachRateEstimateusing_state->scaling() - Converts
RateEstimateobjects toHeatmapRowDatausingtimes[]bin centers andmetadata.sample_spacing - Applies row sorting via
computeSortOrder()andapplySortOrder()if sort mode is not Manual - Calls
HeatmapMapper::buildScene()with the colormap andHeatmapColorRangefrom state - Uploads via
_scene_renderer.uploadScene()and emitsunitCountChanged
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:
- PlotAlignmentWidget — Shared alignment UI (event/interval key, alignment type, offset, window size).
- Unit Selection —
Feature_Tree_Widgetfiltered toDigitalEventSeries, with prefix-based group toggling. Syncs withHeatmapState::unit_keys. - Scaling & Estimation — Composable controls from the
RateEstimationControlslibrary:EstimationMethodControls: method combo (Histogram Binning / Gaussian Kernel / Causal Exponential) with aQStackedWidgetshowing per-method parameter spinboxes. EmitsparamsChanged(EstimationParams)→ connected directly toHeatmapState::setEstimationParams(). Stub methods show a “Not yet implemented” page.ScalingModeControls: scaling mode combo auto-populated fromallScalingModes(). EmitsscalingModeChanged(ScalingMode)→ connected directly toHeatmapState::setScaling().ColormapControls(fromPlots/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). EmitscolormapChanged(ColormapPreset)→HeatmapState::setColormapPreset()andcolorRangeChanged(ColorRangeConfig)→HeatmapState::setColorRange().
- 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. - Time Axis Range Controls —
RelativeTimeAxisRangeControlsin a collapsibleSection. - Y-Axis Range Controls —
VerticalAxisRangeControlsin a collapsibleSection.
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:
- HeatmapWidget shows all units at a glance, colored by firing rate
- Row selection drives PSTHWidget and EventPlotWidget to show the selected unit’s detailed histogram and raster
- 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
GatherResultinfrastructure - HeatmapWidget Roadmap — Planned features and development phases