Temporal Projection View Widget

Temporal Projection View Widget Overview

The Temporal Projection View Widget renders a 2D spatial overlay of PointData and LineData collapsed across all time frames into a single view. Unlike trial-aligned plots (LinePlotWidget, EventPlotWidget, PSTHWidget, HeatmapWidget), this widget does not use PlotAlignmentState or GatherResult. Instead, it flattens all temporal samples via SpatialMapper_AllTimes and displays them in a single spatial coordinate frame.

Users can add multiple PointData and LineData keys, configure rendering appearance (point glyph shape, size, color, alpha, and line width), and interact via zoom, pan, and selection. Points and lines carry EntityID associations, enabling click-to-navigate, selection-to-group workflows, and group-based coloring.

Architecture

The widget follows the standard plot widget directory layout:

Plots/TemporalProjectionViewWidget/
├── CMakeLists.txt
├── TemporalProjectionViewWidgetRegistration.hpp   # EditorRegistry registration module
├── TemporalProjectionViewWidgetRegistration.cpp
├── Core/
│   ├── TemporalProjectionViewState.hpp            # QObject state class (EditorState subclass)
│   └── TemporalProjectionViewState.cpp
├── Rendering/
│   ├── TemporalProjectionOpenGLWidget.hpp          # OpenGL rendering + mouse interaction
│   └── TemporalProjectionOpenGLWidget.cpp
└── UI/
    ├── TemporalProjectionViewWidget.hpp            # Main composite widget (axes + OpenGL + layout)
    ├── TemporalProjectionViewWidget.cpp
    ├── TemporalProjectionViewWidget.ui
    ├── TemporalProjectionViewPropertiesWidget.hpp  # Properties/settings panel
    ├── TemporalProjectionViewPropertiesWidget.cpp
    └── TemporalProjectionViewPropertiesWidget.ui

Component Overview

Component Responsibility
TemporalProjectionViewWidget (UI) Top-level Qt widget — composes OpenGL canvas, horizontal axis, vertical axis
TemporalProjectionViewPropertiesWidget (UI) Properties panel — data key management (point + line), rendering controls, selection mode, axis range controls
TemporalProjectionOpenGLWidget (Rendering) OpenGL canvas — point rendering (via SceneRenderer), line rendering (via BatchLineRenderer), mouse interaction, selection
TemporalProjectionViewState (Core) Serializable state — view state, axis ranges, data keys, rendering parameters, selection mode
TemporalProjectionViewWidgetRegistration Registers type with EditorRegistry (state, view, properties factories)

EditorState Pattern

The Temporal Projection View Widget follows the project’s EditorState architecture:

  • TemporalProjectionViewState inherits from EditorState
  • Registered with EditorRegistry via TemporalProjectionViewWidgetModule::registerTypes()
  • TemporalProjectionViewWidget is the view factory, TemporalProjectionViewPropertiesWidget is the properties factory
  • Both share the same TemporalProjectionViewState instance
  • Properties placed in Right zone, view placed in Center zone (default)
  • The create_editor_custom factory in TemporalProjectionViewWidgetRegistration handles coupling the view and properties widgets, connecting range controls and forwarding timePositionSelected to EditorRegistry::setCurrentTime()

State Management

TemporalProjectionViewStateData

The serializable state struct contains all persistent configuration:

struct TemporalProjectionViewStateData {
    std::string instance_id;
    std::string display_name = "Spatial Overlay";
    CorePlotting::ViewStateData view_state;
    HorizontalAxisStateData horizontal_axis;
    VerticalAxisStateData vertical_axis;

    // Data keys
    std::vector<std::string> point_data_keys;
    std::vector<std::string> line_data_keys;

    // Rendering
    CorePlotting::GlyphStyleData point_glyph_style;  // shape, size, color, alpha
    float line_width = 2.0f;

    // Selection
    std::string selection_mode = "none"; // "none", "point", "line", "polygon"
    
    // Group integration
    bool color_by_group = true;  // Toggle for group-based coloring
};

Signal Architecture

TemporalProjectionViewState emits category-level signals to minimize coupling complexity:

Signal Trigger
viewStateChanged Zoom, pan, or bounds changed (view transform only — no scene rebuild)
pointDataKeyAdded / pointDataKeyRemoved / pointDataKeysCleared Point data key changes
lineDataKeyAdded / lineDataKeyRemoved / lineDataKeysCleared Line data key changes
glyphStyleChanged Any point glyph style property (shape, size, color, alpha) changed
lineWidthChanged Line rendering width changed
selectionModeChanged Selection mode changed
selectionChanged Selection set changed (entities added/removed)
colorByGroupChanged Group-based coloring toggled on/off
stateChanged (inherited) Any change requiring scene rebuild

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

Axis States

TemporalProjectionViewState owns both axis states:

  • HorizontalAxisState — X-axis (generic value axis). See Plot Widget Guide for full axis documentation.
  • VerticalAxisState — Y-axis (generic value axis). Supports the “silent update” pattern to prevent feedback loops during zoom/pan.

Both axis states synchronize bidirectionally with the view state:

  • User changes axis range via spinboxes → view state zoom/pan updated → projection refreshed
  • User zoom/pans in OpenGL → axis ranges updated silently via setRangeSilent() (no feedback loop)

This is the same axis pattern used by ScatterPlotWidget and ACFWidget.

Data Sources

Overview

The widget supports two spatial data types, each added as multiple keys from the DataManager:

Data Type Rendering Backend Selection Method EntityID Source
PointData SceneRenderer via SceneBuilder::addGlyphs() Click-to-select (point mode) Per-point EntityID from PointData
LineData BatchLineStore / BatchLineRenderer Ctrl+Drag stroke intersection (line mode) Per-line EntityID from LineData

PointData

Each PointData key in point_data_keys is loaded via DataManager::getData<PointData>(key). All time frames are flattened into a single set of (x, y) positions using CorePlotting::SpatialMapper::mapAllPoints(), which returns MappedElement objects containing world-space coordinates and associated EntityIDs.

Points are rendered as circle glyphs via SceneBuilder::addGlyphs()SceneRenderer. This is the same glyph rendering infrastructure used by EventPlotWidget for raster tick marks.

LineData

Each LineData key in line_data_keys is loaded via DataManager::getData<LineData>(key). Lines are iterated across all time frames and built into LineBatchData via CorePlotting::buildLineBatchFromLineData().

Lines are rendered via BatchLineStore / BatchLineRenderer, the same SSBO-based batch line rendering pipeline shared with LinePlotWidget. This infrastructure provides:

  • Efficient rendering of large numbers of line segments
  • Per-line selection state (normal / selected visual styles)
  • GPU compute shader intersection for selection strokes (with CPU fallback)

DataManager Observer

The properties widget registers a DataManager observer to keep combo boxes current when data is added/removed:

// Constructor
_dm_observer_id = _data_manager->addObserver([this]() {
    _populatePointComboBox();
    _populateLineComboBox();
});

// Destructor — critical to avoid dangling references
if (_data_manager && _dm_observer_id != -1) {
    _data_manager->removeObserver(_dm_observer_id);
}

See Plot Widget Guide — DataManager Integration for the full observer contract.

Rendering Pipeline

Data Flow

TemporalProjectionViewState (data keys + rendering params)
        ↓
DataManager::getData<PointData>(key) / getData<LineData>(key)
        ↓
SpatialMapper::mapAllPoints() → MappedElement[] (points)
LineData::elementsView() → line vertices (lines)
        ↓
Bounding box computation (auto-fit from data, 2% margin)
        ↓
setXBounds() / setYBounds() → view state + axis sync
        ↓
SceneBuilder::addGlyphs() → RenderableScene (points)
buildLineBatchFromLineData() → LineBatchData (lines)
        ↓
SceneRenderer::uploadScene() + BatchLineStore::upload()
        ↓
SceneRenderer::render() + BatchLineRenderer::render()

Dual Rendering Backend

The widget uses two separate rendering pipelines to handle points and lines with their distinct requirements:

Points — Rendered via SceneRenderer / SceneBuilder, which uses the GlyphRenderer for instanced glyph drawing. Selected points are highlighted by overwriting their color in the RenderableGlyphBatch::colors array (yellow highlight for selected entities).

Lines — Rendered via BatchLineStore / BatchLineRenderer, which uses SSBO-based segment storage with per-line selection masks. This enables cheap selection updates (mask-only GPU upload, no full scene rebuild). Selected lines are rendered in a distinct color (bright yellow) versus normal lines (semi-transparent red).

This dual-backend approach avoids double-rendering: lines are not added to the SceneBuilder scene.

Auto-Fit Bounds

After gathering all point and line data, the widget computes a tight bounding box with 2% padding:

  1. Scan all mapped point positions for min/max X and Y
  2. Scan all line vertices for min/max X and Y
  3. Fall back to ImageSize from data if no actual data points exist
  4. Fall back to 100×100 default if no data and no ImageSize
  5. Apply 2% margin to prevent edge clipping
  6. Reset zoom and pan to fit when bounds change

Line Rendering Colors

The BatchLineRenderer supports visual states per line:

State Color Purpose
Normal Semi-transparent red (0.8, 0.2, 0.2, 0.6) Unselected lines
Selected Bright yellow (1.0, 0.8, 0.0, 1.0) Lines selected by stroke intersection

Visual Customization

The widget uses the project-wide CorePlotting::GlyphStyleData / GlyphStyleState / GlyphStyleControls stack to give users control over point glyph appearance. The same stack is used by ScatterPlotWidget and EventPlotWidget.

Points

Serializable Style Data

CorePlotting::GlyphStyleData (in CorePlotting/DataTypes/GlyphStyleData.hpp) is a plain struct with no Qt dependencies, suitable for embedding directly in state data structs and serializing with rfl::json:

struct GlyphStyleData {
    GlyphType   glyph_type  = GlyphType::Circle;
    float       size        = 5.0f;
    std::string hex_color   = "#FF4444";
    float       alpha       = 0.8f;
};

TemporalProjectionViewStateData embeds this as:

CorePlotting::GlyphStyleData point_glyph_style;  // default: red circle, size 5, alpha 0.8

Available GlyphType values: Circle, Square, Tick, Cross, Diamond, Triangle. (Diamond and Triangle fall back to Circle in the current renderer.)

QObject State Wrapper

TemporalProjectionViewState owns a std::unique_ptr<GlyphStyleState> (Plots/Common/GlyphStyleWidget/Core/GlyphStyleState.hpp). GlyphStyleState is a QObject that wraps GlyphStyleData and emits:

Signal When Emitted Purpose
styleChanged() User edits a property Triggers scene rebuild via stateChanged()
styleUpdated() setStyleSilent() (e.g., on fromJson) Refreshes UI without rebuild

The state class wires these up in its constructor:

// Initialize GlyphStyleState from serialized data
_glyph_style_state->setStyleSilent(_data.point_glyph_style);

// Propagate user changes back to serializable struct and notify renderer
connect(_glyph_style_state.get(), &GlyphStyleState::styleChanged, this, [this]() {
    _data.point_glyph_style = _glyph_style_state->data();
    emit glyphStyleChanged();
    emit stateChanged();
});

The glyphStyleState() accessor returns the raw pointer for binding to GlyphStyleControls.

Properties Panel Controls

A “Point Appearance” collapsible Section in TemporalProjectionViewPropertiesWidget embeds GlyphStyleControls, which provides four controls laid out as a QFormLayout:

Control Type What It Sets
Shape QComboBox GlyphType (Circle / Square / Tick / Cross / Diamond / Triangle)
Size QDoubleSpinBox Radius in pixels (0.5 – 50 px, step 0.5)
Color QPushButton Opens QColorDialog; stores as hex string
Alpha QDoubleSpinBox Transparency (0.0 – 1.0, step 0.05)

The controls use the anti-recursion _updating_ui guard pattern to avoid feedback loops when the state updates the UI.

Rendering Conversion

TemporalProjectionOpenGLWidget::rebuildScene() reads the glyph style at scene build time via _state->glyphStyleState()->data() and converts it to rendering types using two free functions from CorePlotting/DataTypes/GlyphStyleConversion.hpp:

auto const & glyph_data = _state->glyphStyleState()->data();

CorePlotting::GlyphStyle point_style;
point_style.glyph_type = CorePlotting::toRenderableGlyphType(glyph_data.glyph_type);
point_style.size       = glyph_data.size;
point_style.color      = CorePlotting::hexColorToVec4(glyph_data.hex_color, glyph_data.alpha);

builder.addGlyphs("spatial_points", elements, point_style);

toRenderableGlyphType() maps the serializable GlyphType enum to the renderer-level RenderableGlyphBatch::GlyphType. hexColorToVec4() parses a "#RRGGBB" string and applies the separate alpha value, returning a glm::vec4 for the shader.

The glyphStyleChanged signal from TemporalProjectionViewState is connected in TemporalProjectionOpenGLWidget::setState() to set _scene_dirty = true and call update(), ensuring the next paint picks up the new style without any extra bookkeeping.

Shared infrastructure used (see Plots/Common/GlyphStyleWidget/):

Component Location
CorePlotting::GlyphType enum CorePlotting/DataTypes/GlyphStyleData.hpp
CorePlotting::GlyphStyleData struct CorePlotting/DataTypes/GlyphStyleData.hpp
CorePlotting::toRenderableGlyphType() CorePlotting/DataTypes/GlyphStyleConversion.hpp
CorePlotting::hexColorToVec4() CorePlotting/DataTypes/GlyphStyleConversion.hpp
GlyphStyleState Plots/Common/GlyphStyleWidget/Core/GlyphStyleState.hpp
GlyphStyleControls Plots/Common/GlyphStyleWidget/GlyphStyleControls.hpp

Interaction

Zoom and Pan

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

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

These are the same shared interaction helpers used across all plot widgets — see Plot Widget Guide.

Selection Modes

The widget supports four selection modes, controlled by a combo box in the properties panel:

Point Selection (mode: “point”)

  • Ctrl + Click on a point: Toggle selection (add or remove entity)
  • Ctrl + Shift + Click: Remove mode (deselect only)
  • Uses SceneHitTester::queryQuadTree() for spatial point lookup with 10-pixel tolerance
  • Selected points are highlighted orange in the glyph batch

This shares the SceneHitTester infrastructure from CorePlotting/Interaction/ also used by EventPlotWidget for raster click hit testing.

Line Selection (mode: “line”)

  • Ctrl + Click + Drag: Draw intersection stroke across lines
  • Ctrl + Shift + Click + Drag: Remove mode (deselect intersected lines)
  • Escape: Cancel in-progress selection
  • Uses the shared LineSelectionHelpers from Plots/Common/, which delegates to either GPU compute shader or CPU intersector
  • Selection updates are mask-only (no full scene rebuild)

This shares the line selection infrastructure with LinePlotWidget via LineSelectionHelpers:

Component Location Purpose
LineSelectionHelpers Plots/Common/ Shared stroke intersection + preview utilities
ILineBatchIntersector CorePlotting/LineBatch/ Intersector interface
CpuLineBatchIntersector CorePlotting/LineBatch/ CPU fallback intersector
ComputeShaderIntersector PlottingOpenGL/LineBatch/ GPU compute shader (GL 4.3+)

Polygon Selection (mode: “polygon”)

  • Ctrl + Click: Begin polygon / add vertex
  • Mouse move (while active): Update preview line from last vertex to cursor
  • Enter: Close polygon and select all points inside
  • Escape: Cancel polygon drawing
  • Backspace: Undo last vertex
  • Uses the shared PolygonInteractionController from CorePlotting/Interaction/
  • Point containment test via selectPointsInPolygon() from CorePlotting/Selection/ (ray-casting algorithm)
  • Polygon preview rendered via PreviewRenderer
  • Additive selection: Polygon selection adds to existing selection rather than replacing

This shares the polygon interaction infrastructure with ScatterPlotWidget:

Component Location Purpose
PolygonInteractionController CorePlotting/Interaction/ Click-based polygon creation, cursor preview, auto-close
selectPointsInPolygon CorePlotting/Selection/ Ray-casting point-in-polygon test
PreviewRenderer CorePlotting/Rendering/ Renders polygon preview during construction

The selection mode instructions are provided via the shared SelectionInstructions helpers from Plots/Common/SelectionInstructions.hpp, which provide context-sensitive help text for each mode.

Selection State

Selected entity IDs are tracked in an std::unordered_set<EntityId>. When the selection changes, the entitiesSelected signal is emitted, allowing other components (e.g., group management) to react.

Selection is preserved across scene rebuilds: after rebuilding, both point glyph colors and line selection masks are restored from the cached _selected_entity_ids set.

Group Integration

The widget integrates with the project’s Entity Group system to enable organizing selected points and lines into groups for downstream analysis. A GroupManager is passed during widget registration and wired to the OpenGL renderer.

Right-Click Context Menu

When entities are selected, right-clicking displays a context menu powered by GroupContextMenuHandler with actions:

  • Add to Group — submenu lists existing entity groups
  • Create New Group — creates a new group and adds selected entities
  • Remove from Group — lists groups the selected entities belong to
  • Clear Selection — clears the current selection

The context menu integrates with EntityRegistry for group operations. The handler receives callbacks for:

  • getSelectedEntities() — returns the current _selected_entity_ids set
  • clearSelection() — clears the selection
  • hasSelection() — checks if any entities are selected
  • onGroupOperationCompleted() — called after group operations to refresh the scene

Group-Based Coloring

Points can be colored by their group membership, allowing visual identification of subgroups. A “Color by Group” checkbox in the properties panel toggles the feature (stored in TemporalProjectionViewStateData::color_by_group).

When enabled, applyGroupColorsToScene() applies group colors with the following priority:

  1. Selected points — orange (1.0, 0.6, 0.0, 0.9) for visual feedback
  2. Group colors — each group has an assigned color from GroupManager::getEntityColor()
  3. Default style — falls back to the user-configured glyph style

The GroupManager emits signals (groupCreated, groupRemoved, groupModified) that trigger scene rebuilds to reflect group changes made in other widgets.

Note: Line group coloring is deferred due to the separate BatchLineRenderer pipeline. Only point glyph coloring is currently implemented.

Properties Panel

The TemporalProjectionViewPropertiesWidget provides the following controls:

Data Key Management

  • Point data key combo box + Add/Remove buttons — lists all PointData keys from DataManager
  • Point data table — shows currently added point data keys
  • Line data key combo box + Add/Remove buttons — lists all LineData keys from DataManager
  • Line data table — shows currently added line data keys

Rendering Controls

  • Point Appearance sectionGlyphStyleControls with shape, size, color, and alpha controls. See Visual Customization — Points for full details.
  • Line width spinbox — adjusts line thickness (default: 2.0)

Selection and Grouping Controls

  • Selection mode combo — None / Point / Line / Polygon
  • Selection instructions label — context-sensitive help text (via shared SelectionInstructions helpers)
  • Color by Group checkbox — toggles group-based coloring for points
  • Clear selection button — clears all selected entities

Axis Range Controls

When setPlotWidget() is called (during registration wiring), the properties panel creates:

  • X-Axis Range ControlsHorizontalAxisRangeControls in a collapsible Section
  • Y-Axis Range ControlsVerticalAxisRangeControls in a collapsible Section

See Also