Skip to content

Add Scalar Bar Range Dialog with clim preservation across time steps#6

Merged
eskandarih merged 2 commits intomainfrom
feature/scalar-bar-range-dialog
Apr 7, 2026
Merged

Add Scalar Bar Range Dialog with clim preservation across time steps#6
eskandarih merged 2 commits intomainfrom
feature/scalar-bar-range-dialog

Conversation

@eskandarih
Copy link
Copy Markdown
Collaborator

@eskandarih eskandarih commented Apr 7, 2026

Summary

Adds a new Scalar Bar Range dialog for customizing scalar bar color ranges, with manual min/max editing and automatic range computation across all time steps. Also fixes a critical issue where changing time steps would reset the scalar bar range, and fixes vector glyph rendering.

New Features

Scalar Bar Range Dialog (scalar_bar_range_dialog.py)

  • Non-modal dialog accessible from a new "Scalar Bar Range" toolbar action (adjacent to existing "Scalar Bars" action)
  • Displays one group per active scalar bar with:
    • Read-only scalar bar name label
    • Min / Max QDoubleSpinBox editors for manual range control
    • "Auto From All Time Steps" button that scans all time points to find the global min/max
  • Auto button shows a confirmation warning before scanning (potentially long operation)
  • OK applies and closes; Apply applies without closing; Cancel restores original ranges
image

clim Preservation Across Time Steps

  • _plot_scalar_field(): Before re-plotting, reads the existing scalar bar's lookup table range and passes it as clim to add_mesh(). On first render (no scalar bar yet), clim=None lets PyVista auto-compute.
  • _plot_vector_field(): Same pattern — reads existing vector actor's mapper.scalar_range and injects as clim.
  • This means manually-set or dialog-set ranges survive time step changes.

Bug Fixes

Vector glyph scalar_bar_args error

  • Before: scalar_bar_args was incorrectly passed to block.glyph(), which doesn't accept it → TypeError: DataSetFilters.glyph() got an unexpected keyword argument 'scalar_bar_args'
  • After: Moved scalar_bar_args to plotter.add_mesh() where it belongs

Vector glyph scalar bar source registration

  • _plot_vector_field() now registers the glyph's active scalars name (e.g., "GlyphScale") as a scalar bar source mapped back to the original vector array. This allows the Scalar Bar Range dialog to resolve vector plot scalar bars.

Duplicate method removal

  • Removed duplicate _open_scalar_bar_settings_dialog() definition in qt_window.py

Plotter Helpers Added (plotter.py)

Method Purpose
_register_scalar_bar_source() Tracks how a rendered scalar bar maps to a mesh array + association
_range_from_array() Computes finite scalar range for scalar or vector arrays
_infer_scalar_bar_source() Fallback inference from _scalar_props, _contour_props, _vector_props
_resolve_scalar_bar_source() Returns registered or inferred source, raises if not found
_current_scalar_range() Computes visible min/max for current time step
get_scalar_bar_ranges() Reads live lookup table ranges from plotter.scalar_bars
compute_scalar_bar_data_range() Sweeps all time points to find global min/max, restores original time
apply_scalar_bar_range() Calls update_scalar_bar_range AND GetLookupTable().SetRange() explicitly
_iter_visible_blocks() Yields only visibility-enabled blocks

New Toolbar Icons

  • Blocks.svg, EditScalarBar.svg, EditScalarRange.svg, ScalarBar.svg

Tests

test_scalar_bar_range_dialog.py (6 tests)

  • Apply range updates lookup table and plotter call
  • Compute range scans all time steps and restores active time
  • Dialog applies manual ranges
  • Auto button updates fields (with monkeypatched Yes confirmation)
  • Auto button stops when user declines (monkeypatched No)
  • Cancel restores initial ranges

test_clim_preservation.py (5 tests)

  • First render auto-computes clim (no existing actors)
  • Manual range adjustment survives re-render with different mesh data
  • Switching scalar name resets clim (no matching bar → auto-compute)
  • Full render() path preserves clim across time step change
  • Vector field preserves clim across re-render

All 11 tests pass.

- New ScalarBarRangeDialog with per-scalar-bar manual min/max editors and
  'Auto From All Time Steps' button that scans all time points for global range
- Auto button shows confirmation warning before potentially long scan
- Plotter helpers: compute_scalar_bar_data_range, apply_scalar_bar_range,
  get_scalar_bar_ranges, _register_scalar_bar_source, and supporting methods
- Preserve clim across re-renders (time step changes) in _plot_scalar_field
  and _plot_vector_field by reading existing scalar bar / actor ranges
- Fix vector glyph scalar_bar_args: moved from glyph() to add_mesh()
- Register glyph scalar bar source (GlyphScale) for vector plot range dialog
- New toolbar action 'Scalar Bar Range' adjacent to 'Scalar Bars'
- Remove duplicate _open_scalar_bar_settings_dialog in qt_window.py
- New toolbar icons for display toolbar actions
- 11 focused tests (6 dialog, 5 clim preservation)
@eskandarih
Copy link
Copy Markdown
Collaborator Author

eskandarih commented Apr 7, 2026

Architecture & Workflow Documentation


_scalar_bar_sources Registry

_scalar_bar_sources is a dict[str, dict[str, str]] initialized in Plotter.__init__(). It acts as a lookup table that maps the name of a rendered scalar bar (as it appears in the PyVista plotter) back to the original mesh data source that produced it.

Structure:

_scalar_bar_sources = {
    "Temperature": {"array_name": "Temperature", "association": "point"},
    "GlyphScale":  {"array_name": "B",           "association": "point"},
}

This registry exists because scalar bar names don't always match the source array. For example, PyVista's glyph() filter renames the active scalars to "GlyphScale" — without this mapping, the dialog would not know which mesh array to scan when computing auto-range for a vector plot's scalar bar.


Source Resolution Chain

When the dialog needs to find the data source behind a scalar bar, it follows this resolution chain:

flowchart TB
    A["_resolve_scalar_bar_source(name)"]
    A --> B{"name in<br/>_scalar_bar_sources?"}
    B -- Yes --> C["Return cached source<br/>{array_name, association}"]
    B -- No --> D["_infer_scalar_bar_source(name)"]
    D --> E{"Check _scalar_props<br/>_contour_props<br/>_vector_props"}
    E -- "Match found in<br/>visible blocks" --> F["Cache result &<br/>return source"]
    E -- "No match" --> G["Raise ValueError"]
Loading

Scalar Bar Range Dialog — Manual Edit Workflow

sequenceDiagram
    actor User
    participant Dialog as ScalarBarRangeDialog
    participant Plotter
    participant VTK as ScalarBar<br/>LookupTable

    User ->> Dialog: Open from toolbar
    Dialog ->> Plotter: get_scalar_bar_ranges()
    Plotter ->> VTK: scalar_bars[name]<br/>.GetLookupTable()<br/>.GetRange()
    VTK -->> Dialog: {name: (min, max)}
    Dialog ->> Dialog: Populate Min/Max spinboxes
    User ->> Dialog: Edit Min/Max & click Apply
    Dialog ->> Plotter: apply_scalar_bar_range(name, min, max)
    Plotter ->> VTK: update_scalar_bar_range(clim, name)
    Plotter ->> VTK: GetLookupTable().SetRange(min, max)
Loading

Scalar Bar Range Dialog — Auto Range Workflow

sequenceDiagram
    actor User
    participant Dialog as ScalarBarRangeDialog
    participant Plotter

    User ->> Dialog: Click "Auto From<br/>All Time Steps"
    Dialog ->> Dialog: Show QMessageBox<br/>confirmation warning
    User ->> Dialog: Confirm Yes
    Dialog ->> Plotter: compute_scalar_bar_data_range(name)
    Plotter ->> Plotter: _resolve_scalar_bar_source(name)
    loop Each time step
        Plotter ->> Plotter: reader.set_active_time_point(t)
        Plotter ->> Plotter: _current_scalar_range(array, assoc)
    end
    Plotter ->> Plotter: Restore original time point
    Plotter -->> Dialog: (global_min, global_max)
    Dialog ->> Dialog: Update spinbox values
Loading

Scalar Bar Range Dialog — Cancel / Restore Workflow

sequenceDiagram
    actor User
    participant Dialog as ScalarBarRangeDialog
    participant Plotter

    Note over Dialog: Initial ranges saved at open time
    User ->> Dialog: Click Cancel
    Dialog ->> Plotter: apply_scalar_bar_range(<br/>name, orig_min, orig_max)
    Note over Plotter: Original ranges restored
Loading

Clim Preservation — Scalar Field

When the user changes the active time step, render() re-calls _plot_scalar_field(). Without clim preservation, the method would auto-compute new ranges — discarding any manual adjustment.

flowchart TB
    S1["_plot_scalar_field() called"]
    S1 --> S2{"name in<br/>plotter.scalar_bars?"}
    S2 -- Yes --> S3["existing_clim =<br/>scalar_bar<br/>.GetLookupTable()<br/>.GetRange()"]
    S2 -- No --> S4["existing_clim = None<br/>(first render)"]
    S3 --> S5["Loop over visible blocks"]
    S4 --> S5
    S5 --> S6["add_mesh(block,<br/>clim=existing_clim)"]
    S6 --> S7["_register_scalar_bar_source(<br/>name, name, assoc)"]
Loading

Clim Preservation — Vector Field

Vector fields follow the same pattern but read ranges from the actor's mapper instead of the scalar bar lookup table.

flowchart TB
    V1["_plot_vector_field() called"]
    V1 --> V2["Scan renderer.actors<br/>for keys starting with<br/>'vector_field'"]
    V2 --> V3{"Actor found?"}
    V3 -- Yes --> V4["clim =<br/>actor.mapper<br/>.scalar_range"]
    V3 -- No --> V5["clim not set<br/>(first render,<br/>auto-compute)"]
    V4 --> V6["block.glyph(...)<br/>to create glyphs"]
    V5 --> V6
    V6 --> V7["add_mesh(glyphs,<br/>clim=clim,<br/>scalar_bar_args=...)"]
    V7 --> V8["glyph_scalars_name =<br/>glyphs.active_scalars_name"]
    V8 --> V9["_register_scalar_bar_source(<br/>glyph_scalars_name,<br/>original_name, assoc)"]
Loading

Method Reference

Method Purpose
_register_scalar_bar_source(bar_name, array_name, assoc) Stores the mapping from a rendered scalar bar name to its mesh array source
_range_from_array(values) Computes finite (min, max) from scalar or vector (magnitude) arrays
_infer_scalar_bar_source(bar_name) Fallback inference — searches _scalar_props, _contour_props, _vector_props against visible blocks
_resolve_scalar_bar_source(bar_name) Returns cached source or falls back to inference; raises if neither works
_current_scalar_range(array_name, assoc) Computes visible (min, max) across all visible blocks for the current time step
get_scalar_bar_ranges() Reads live lookup table ranges from all active scalar bars
compute_scalar_bar_data_range(bar_name) Iterates all time steps, calls _current_scalar_range at each, returns global (min, max)
apply_scalar_bar_range(bar_name, min, max) Updates both the plotter scalar bar range and the VTK lookup table
_iter_visible_blocks(skip_empty) Yields blocks that are currently set to visible (used by range computation)

@eskandarih eskandarih self-assigned this Apr 7, 2026
@eskandarih eskandarih added the enhancement New feature or request label Apr 7, 2026
@eskandarih eskandarih merged commit ef6c5fa into main Apr 7, 2026
@eskandarih eskandarih deleted the feature/scalar-bar-range-dialog branch April 7, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant