|
| 1 | +# colormaps |
| 2 | + |
| 3 | +Self-contained colormap module for managing ParaView/VTK color transfer |
| 4 | +functions, colorbar rendering, and interactive preset controls in Trame apps. |
| 5 | + |
| 6 | +Designed to be extracted as a standalone `trame-colormap` package in the future. |
| 7 | + |
| 8 | +## Public API |
| 9 | + |
| 10 | +Three symbols are exported from `colormaps/__init__.py`: |
| 11 | + |
| 12 | +| Symbol | Source | Purpose | |
| 13 | +|--------|--------|---------| |
| 14 | +| `ColormapController` | `controller.py` | Per-view LUT manager: creates LUT, wires mapper, manages presets/range/ticks | |
| 15 | +| `ColormapConfig` | `state.py` | Reactive state model with all colormap fields (standalone use) | |
| 16 | +| `ColormapInitialize` | `server.py` | One-time server setup: populates `luts_normal`/`luts_inverted` on server state | |
| 17 | + |
| 18 | +Additionally, `widgets/` provides: |
| 19 | + |
| 20 | +| Symbol | Source | Purpose | |
| 21 | +|--------|--------|---------| |
| 22 | +| `ColormapColorbar` | `widgets/colorbar.py` | Colorbar strip + popup control panel (imported as alias in `view_manager.py`) | |
| 23 | + |
| 24 | +## Dependencies |
| 25 | + |
| 26 | +| Package | Used in | Purpose | |
| 27 | +|---------|---------|---------| |
| 28 | +| **paraview** | `core/presets.py`, `controller.py` | LUT proxy creation, preset discovery/application, `servermanager` access | |
| 29 | +| **vtk** (`vtkmodules`) | `core/presets.py`, `core/transforms.py`, `controller.py` | `vtkColorTransferFunction` for color sampling, `vtkPNGWriter`/`vtkImageData` for colorbar image generation, `vtk_to_numpy` for data array conversion | |
| 30 | +| **numpy** | `core/ticks.py`, `core/transforms.py` | See below | |
| 31 | +| **trame** | `state.py`, `widgets/` | `StateDataModel` for reactive config, Vuetify 3 widgets for UI | |
| 32 | + |
| 33 | +### Why numpy? |
| 34 | + |
| 35 | +Numpy is used in two core modules for operations that would be verbose, |
| 36 | +slower, or less correct with pure Python: |
| 37 | + |
| 38 | +**`core/ticks.py`** — tick computation: |
| 39 | +- `np.nanmin`/`np.nanmax` with `where=` for masked reduction without copies |
| 40 | + (2–3 orders of magnitude faster than Python loops on large arrays) |
| 41 | +- `np.finfo` for dtype-aware floating-point limits |
| 42 | +- `np.linspace`/`np.geomspace` for uniform tick spacing |
| 43 | +- `np.log10`, `np.sign`, `np.abs` for vectorized log/symlog math |
| 44 | +- `np.isclose` for robust floating-point comparison |
| 45 | +- `np.unique`/`np.sort` for deduplication |
| 46 | + |
| 47 | +**`core/transforms.py`** — LUT transforms: |
| 48 | +- `np.log10`, `np.sign`, `np.abs`, `np.asarray` for symlog transform math |
| 49 | +- `np.linspace` for uniform sampling in log/symlog space |
| 50 | +- `np.floor`/`np.ceil` for decade boundary computation |
| 51 | + |
| 52 | +Numpy is already a transitive dependency of both ParaView and VTK, so it |
| 53 | +adds no new install burden. |
| 54 | + |
| 55 | +## Module Structure |
| 56 | + |
| 57 | +``` |
| 58 | +colormaps/ |
| 59 | +├── __init__.py # Re-exports: ColormapController, ColormapConfig, ColormapInitialize |
| 60 | +├── README.md # This file |
| 61 | +├── server.py # ColormapInitialize() — Trame server setup (depends on server.state) |
| 62 | +├── state.py # ColormapConfig(StateDataModel) — reactive color state |
| 63 | +├── controller.py # ColormapController — owns LUT, wires mapper, manages presets/range/ticks |
| 64 | +├── core/ |
| 65 | +│ ├── presets.py # Preset discovery, COLORBAR_CACHE, lut_to_img() |
| 66 | +│ ├── ticks.py # Tick computation (linear, log, symlog) |
| 67 | +│ └── transforms.py # LUT transforms (linear, log, symlog, discrete variants) |
| 68 | +└── widgets/ |
| 69 | + ├── __init__.py # Re-exports: create_colorbar, create_control_panel |
| 70 | + ├── colorbar.py # Colorbar strip with tick marks and range labels |
| 71 | + └── control_panel.py # Preset picker, scale mode, range, discrete settings |
| 72 | +``` |
| 73 | + |
| 74 | +## Layer Separation |
| 75 | + |
| 76 | +| Layer | Modules | Dependencies | |
| 77 | +|-------|---------|-------------| |
| 78 | +| **Core** (pure VTK/numpy) | `core/presets.py`, `core/ticks.py`, `core/transforms.py` | ParaView, VTK, numpy | |
| 79 | +| **Server** (Trame init) | `server.py` | Core + Trame server.state | |
| 80 | +| **State** (Trame reactive model) | `state.py` | trame | |
| 81 | +| **Controller** (orchestration) | `controller.py` | Core + State + ParaView | |
| 82 | +| **Widgets** (UI) | `widgets/colorbar.py`, `widgets/control_panel.py` | trame (Vuetify 3) | |
| 83 | + |
| 84 | +The core layer has zero Trame dependency and can be used independently |
| 85 | +for headless colormap operations. `server.py` is the only file outside |
| 86 | +`widgets/` and `state.py` that touches Trame's server API. |
| 87 | + |
| 88 | +## Widget Structure |
| 89 | + |
| 90 | +`ColormapColorbar` (`create_colorbar`) produces the following DOM tree: |
| 91 | + |
| 92 | +``` |
| 93 | +html.Div (top-level — flexbox row, bg-blue-grey-darken-2, 1rem tall) |
| 94 | +├── v3.VMenu (activator="parent" — click anywhere on the bar to open) |
| 95 | +│ └── create_control_panel → v3.VCard (360px popup) |
| 96 | +│ ├── VCardItem: toggle buttons (color-blind, invert, scale, range, discrete) |
| 97 | +│ ├── VCardItem: discrete color count input (v-show when discrete) |
| 98 | +│ ├── VCardItem: min/max text fields (v-show when override_range) |
| 99 | +│ ├── VDivider |
| 100 | +│ └── VList: searchable preset list with thumbnail images |
| 101 | +├── html.Div (min range label) |
| 102 | +├── html.Div (colorbar image container, position:relative) |
| 103 | +│ ├── html.Img (LUT image, full width) |
| 104 | +│ └── html.Div (tick overlay, position:absolute, pointer-events:none) |
| 105 | +│ └── html.Div v-for="tick in color_ticks" |
| 106 | +│ ├── html.Div (top tick line) |
| 107 | +│ ├── html.Span (tick label) |
| 108 | +│ └── html.Div (bottom tick line) |
| 109 | +└── html.Div (max range label) |
| 110 | +``` |
| 111 | + |
| 112 | +The control panel reads `luts_normal` / `luts_inverted` from server |
| 113 | +state (populated by `ColormapInitialize`). All template bindings |
| 114 | +reference `config.*` via `config.provide_as("config")` called in |
| 115 | +`create_colorbar`. |
| 116 | + |
| 117 | +## Config Fields |
| 118 | + |
| 119 | +The `config` object passed to `ColormapController` and `ColormapColorbar` |
| 120 | +must be a `trame.app.dataclass.StateDataModel` (or subclass) with the |
| 121 | +following fields. All fields are required. `ColormapConfig` in `state.py` |
| 122 | +provides the canonical definition with defaults. |
| 123 | + |
| 124 | +| Field | Type | Default | Role | |
| 125 | +|-------|------|---------|------| |
| 126 | +| **User-settable (read by controller, bound to UI)** |||| |
| 127 | +| `preset` | `str` | `"BuGnYl"` | Active color preset name | |
| 128 | +| `invert` | `bool` | `False` | Invert the transfer function | |
| 129 | +| `color_blind` | `bool` | `False` | Filter preset list to color-blind safe | |
| 130 | +| `use_log_scale` | `str` | `"linear"` | Scale mode: `"linear"`, `"log"`, `"symlog"` | |
| 131 | +| `discrete_log` | `bool` | `False` | Enable discrete banding | |
| 132 | +| `n_discrete_colors` | `int` | `4` | Number of discrete sub-bands (1–20) | |
| 133 | +| `color_value_min` | `str` | `"0"` | Manual range min (string for text field) | |
| 134 | +| `color_value_max` | `str` | `"1"` | Manual range max (string for text field) | |
| 135 | +| `override_range` | `bool` | `False` | Use manual range instead of data range | |
| 136 | +| **Derived (written by controller, read by widgets)** |||| |
| 137 | +| `color_range` | `tuple[float, float]` | `(0, 1)` | Active min/max color range | |
| 138 | +| `color_value_min_valid` | `bool` | `True` | Whether `color_value_min` parses as a valid float | |
| 139 | +| `color_value_max_valid` | `bool` | `True` | Whether `color_value_max` parses as a valid float | |
| 140 | +| `n_colors` | `int` | `255` | Number of LUT samples | |
| 141 | +| `lut_img` | `str` | `""` | Base64 PNG data URI of the colorbar image | |
| 142 | +| `color_ticks` | `list` | `[]` | Tick marks: `[{position, label, color}, ...]` | |
| 143 | +| `effective_color_range` | `tuple[float, float]` | `(0, 1)` | Actual CTF range after transforms | |
| 144 | +| **UI widget state (used by control panel / colorbar)** |||| |
| 145 | +| `menu` | `bool` | `False` | Whether the control panel popup is open | |
| 146 | +| `search` | `str \| None` | `None` | Preset search filter text | |
| 147 | + |
| 148 | +When composing into a larger config (like `ViewConfiguration`), include |
| 149 | +all fields above alongside your app-specific fields. The controller reads |
| 150 | +and writes them by name — no inheritance required. |
| 151 | + |
| 152 | +## Usage |
| 153 | + |
| 154 | +### Integrated (current — within QuickView) |
| 155 | + |
| 156 | +The controller operates on an existing `ViewConfiguration` state model: |
| 157 | + |
| 158 | +```python |
| 159 | +from e3sm_quickview.colormaps import ColormapController, ColormapInitialize |
| 160 | +from e3sm_quickview.colormaps.widgets import create_colorbar as ColormapColorbar |
| 161 | + |
| 162 | +# One-time server initialization (populates preset lists) |
| 163 | +ColormapInitialize(server) |
| 164 | + |
| 165 | +# Per-view: controller creates LUT, wires mapper, sets up reactive watchers |
| 166 | +colormap = ColormapController( |
| 167 | + server=server, |
| 168 | + variable_name="Temperature", |
| 169 | + mapper=my_mapper, |
| 170 | + data_array_fn=lambda: get_data_array(), |
| 171 | + render_fn=render, |
| 172 | + config=existing_config, # uses fields from a larger config object |
| 173 | +) |
| 174 | + |
| 175 | +# In UI building: |
| 176 | +ColormapColorbar(config, colormap.update_color_preset) |
| 177 | +``` |
| 178 | + |
| 179 | +### Updating color range after data changes |
| 180 | + |
| 181 | +When the underlying data changes (e.g. slice selection, pipeline update), |
| 182 | +call `update_color_range()` on each view's controller to recompute the |
| 183 | +range, re-apply transforms, and regenerate ticks: |
| 184 | + |
| 185 | +```python |
| 186 | +for view in views: |
| 187 | + view.colormap.update_color_range() |
| 188 | +``` |
| 189 | + |
| 190 | +This is done in `ViewManager.update_color_range()`, called from `app.py` |
| 191 | +after slicing or downstream pipeline changes. |
| 192 | + |
| 193 | +### State export / import |
| 194 | + |
| 195 | +Colormap fields live on the same config as layout fields, so they are |
| 196 | +serialized alongside them in `download_state()` / `_import_state()`. |
| 197 | +One caveat: JSON deserializes tuples as lists, so `color_range` must |
| 198 | +be converted back: |
| 199 | + |
| 200 | +```python |
| 201 | +if isinstance(cfg["color_range"], list): |
| 202 | + cfg["color_range"] = tuple(cfg["color_range"]) |
| 203 | +``` |
| 204 | + |
| 205 | +### Standalone (future trame-colormap package) |
| 206 | + |
| 207 | +When no external config is provided, the controller creates its own |
| 208 | +`ColormapConfig`: |
| 209 | + |
| 210 | +```python |
| 211 | +from colormaps import ColormapController, ColormapInitialize |
| 212 | + |
| 213 | +ColormapInitialize(server) |
| 214 | + |
| 215 | +colormap = ColormapController( |
| 216 | + server=server, |
| 217 | + variable_name="Pressure", |
| 218 | + mapper=my_mapper, |
| 219 | + data_array_fn=lambda: get_data_array(), |
| 220 | + render_fn=render, |
| 221 | + # config=None → creates a ColormapConfig automatically |
| 222 | +) |
| 223 | +``` |
0 commit comments