Self-contained colormap module for managing VTK color transfer functions, colorbar rendering, and interactive preset controls in Trame apps.
pip install trame-colormapsOr with uv:
uv add trame-colormaps# Get code
git clone https://github.com/Kitware/trame-colormaps.git
cd trame-colormaps
# Create venv and install all dependencies
uv sync --all-extras --dev
# Activate environment
source .venv/bin/activate
# Install commit analysis
pre-commit install
pre-commit install --hook-type commit-msgRun tests:
pytestRun the example app:
python ./examples/wavelet.pyLint and format:
pre-commit run --all-files- Bump the version in
pyproject.toml - Commit and tag:
git tag v<version> - Push with tags:
git push --tags - GitHub Actions will build and publish to PyPI automatically
The wavelet example (examples/wavelet.py) shows four colorbars around a 3D view:
two horizontal bars (top and bottom) and two vertical bars (left and right).
Clicking any colorbar opens its control panel with preset picker, scale mode,
and discrete settings. Only one panel can be open at a time.
A production app using trame-colormaps for climate data visualization.
Each data variable gets its own horizontal colorbar at the bottom of the view,
with symlog tick marks that adapt to the data range.
Hover over any colorbar and the cursor changes to a context-menu icon:
Click the colorbar to open its control panel. Only one panel can be open at a time — opening one automatically closes any other.
The control panel has three sections, top to bottom:
-
Toolbar — A row of icon buttons across the top that control scale, diverging mode, preset category, colorblind filtering, inversion, discrete banding, and custom range. A search field and close button sit on the right.
-
Settings panels — Context-sensitive inputs that appear below the toolbar depending on which modes are active. These include discrete color count, diverging mode controls (|max| and ε tolerance), and custom range inputs (Min / Max).
-
Preset list — A scrollable list of colormap swatches filtered by the active category, colorblind setting, and search text. Click any swatch to apply it immediately.
The toolbar has three areas, left to right:
-
Icon buttons — Nine buttons separated into three groups by vertical dividers. Active toggles show a primary-colored square outline; the icon itself stays black. The NaN color button opens a dropdown instead of toggling. Details on each button below.
-
Search / preset name — Shows the name of the currently active preset. Click into it to type and filter the preset list by name. Use the clear button to reset the search. Switching categories clears the search.
-
Close button (✕) — Dismisses the control panel.
Empty rows in the table indicate the vertical separator dividers between button groups.
- Δ Difference is disabled when Scale is Log or when Custom Range is on. You can always turn Δ Difference off.
- Custom Range is disabled when Δ Difference is on.
- Cut Outside Range is disabled when neither Custom Range nor Δ Difference is active.
- Category dropdown is disabled when Δ Difference is on (presets forced to Diverging). When Δ is turned off, category resets to Sequential.
The NaN Color button ()
opens a scrollable dropdown with 19 preset options. Each row shows a color
swatch circle and a situation label. Options are grouped:
| Group | Options |
|---|---|
| Default | transparent, general |
| Colormap types | sequential maps, diverging maps, categorical maps, grayscale maps, bright maps, dark maps, hot maps, terrain final |
| Data quality | error, warning, suspect data, masked data, debugging |
| Background/context | light background, dark background, publication light, publication dark |
The default is transparent (rgba(0,0,0,0)), meaning NaN cells are
invisible. The selected color is applied via vtkColorTransferFunction.SetNanColorRGBA()
on both the main CTF and any render CTF (e.g. symlog).
The Scale button cycles through three modes. The colorbar and tick labels update to match the active scale:
- Linear — evenly spaced ticks, decimal labels. Colorbar image is a direct mapping of the preset. Discrete label reads "Colors per tick interval."
- Log — decade-based ticks, scientific notation labels. Colorbar image is resampled through the log transform so more visual space is given to lower magnitudes. Not available in Δ mode. Discrete label reads "Colors per order of magnitude."
- SymLog — symmetric log around zero, decade ticks adaptively thinned near zero, scientific notation labels. Colorbar image is resampled through the symlog transform. In Δ mode, the epsilon dead zone aligns with the symlog tick marks. Discrete label reads "Colors per order of magnitude."
When Δ Difference is active:
- Preset list is forced to diverging-only presets.
- Scale only toggles between Linear and SymLog — Log is not available.
- Settings panel replaces Min/Max with |max| (symmetric range centered at zero) and ε tolerance (dead zone around zero).
- Custom Range is disabled — range is driven by |max|.
- Category dropdown is disabled — presets locked to Diverging.
- Cut Outside Range is available — out-of-range values can use the NaN color instead of endpoint clamping.
The Cut button ()
toggles between two modes for values outside the color range:
- Clamp (default) — out-of-range values are assigned the nearest endpoint color. This is VTK's default behavior.
- Cut — out-of-range values are assigned the NaN color (set via the NaN Color dropdown). With the default transparent NaN color, out-of-range regions become invisible.
Cut mode is only available when Custom Range or Δ Difference is active — these are the modes where the color range may intentionally exclude part of the data. When neither is active, the button is disabled because the range covers the full data extent.
Internally, cut mode uses vtkColorTransferFunction.SetUseAboveRangeColor()
and SetUseBelowRangeColor() with the current NaN color RGB. Changing the
NaN color automatically updates the above/below range colors.
Import via the trame namespace:
from trame.dataclasses.colormaps import ColormapConfig
from trame.widgets.colormaps import HorizontalScalarBar, VerticalScalarBar, ColorMapEditor| Symbol | Module | Purpose |
|---|---|---|
ColormapConfig |
trame_colormaps.dataclasses |
Reactive state model — owns CTF, mapper, presets, range, ticks |
HorizontalScalarBar |
trame_colormaps.widgets |
Horizontal colorbar widget with built-in control panel |
VerticalScalarBar |
trame_colormaps.widgets |
Vertical colorbar widget with built-in control panel |
ColorMapEditor |
trame_colormaps.widgets |
Preset picker / control panel popup (used internally by scalar bars) |
buttons |
trame_colormaps.widgets |
Returns button config dicts for the control panel toolbar |
NAN_COLOR_OPTIONS |
trame_colormaps.widgets |
List of 19 NaN color presets with RGBA values and situation labels |
All colormap presets are stored as JSON files under src/trame_colormaps/presets/.
- Source: ParaView ColorMaps.json
- License: BSD-3-Clause
- 199 colormaps including Brewer, matplotlib, and other community-contributed presets
- Presets that originally used
IndexedColors(discrete/qualitative) have been converted toRGBPointswith evenly spaced control points for uniform handling
- Source: Fabio Crameri's Scientific Colour Maps
- License: MIT
- 60 colormaps — sequential, diverging, multi-sequential, cyclic, and categorical
- All are perceptually uniform and color-blind safe
- Downloaded from the cmcrameri GitHub repository
| Category | Use When | Data Character |
|---|---|---|
| Sequential | Magnitude — more/less of something | Temperature, pressure, density |
| Multi-Sequential | Structured subranges — regimes that differ | Terrain, threshold bands |
| Diverging | Deviation — Δ from a reference value | Anomaly, residual, balance |
| Cyclic | Periodic — values that wrap around | Phase, angle, time-of-day |
| Categorical | Discrete labels — no inherent order | Material ID, region, class |
Note: Categorical presets are excluded from
default_presets.jsonbecause trame-colormaps generates its own discrete/categorical colormaps from any preset via the discrete banding feature.
Sequential colormaps encode "more vs less" — data that is ordered and one-sided with no special reference value.
Use when:
- Data interpretation is monotonic: low → high
- There is no meaningful midpoint or zero crossing
Examples: temperature (no reference), density, probability, intensity, error magnitude (|Δ|).
Properties:
- Monotonic lightness — darker always means more (or less)
- No implied midpoint
- Easy to interpret quantitatively
Good defaults: Viridis, Plasma, batlow — perceptually uniform ramps.
Multi-sequential colormaps encode structured data — data with meaningful subranges where different regimes should look visually distinct.
Use when:
- Data has meaningful subranges
- Different regimes should look different
Examples:
- Terrain / elevation — deep ocean → shallow → land → mountains → snow; each region has different semantics.
- Threshold-based interpretation — low → acceptable → warning → critical.
- Nonlinear emphasis (without changing scale) — more color variation in important ranges, less in unimportant ones.
Properties:
- Does not imply "above vs below" — just adds internal structure
- Requires care to avoid false edges and visual discontinuities
Diverging colormaps encode "above vs below reference" — data with a meaningful center value where you care about the direction of deviation.
Use when:
- There is a meaningful center (usually 0, but not always)
- You care about direction: below reference ← neutral → above reference
Examples: Δ = A − B, anomalies (value − mean), residuals, signed errors.
Properties:
- Two symmetric color branches around a neutral center (white/light gray)
- Encodes both sign and magnitude
- Must be centered correctly to avoid misinterpretation
- Should be perceptually balanced on both sides
Common derived difference fields:
-
Absolute difference —
Δ = A − BYour primary case (simulation vs observation). Symmetric, interpretable. -
Relative / percent difference —
Δ = (A − B) / BorΔ% = 100 × (A − B) / BUseful when scale matters. Still centered at 0 → diverging applies. -
Deviation from a baseline —
Δ = value − reference_valueExamples: temperature − freezing point, measurement − target threshold, field − spatial mean. -
Standardized anomaly —
Δ = (value − mean) / stdNow Δ is in "number of standard deviations." Very common in climate and statistics. -
Log-ratio (for multiplicative differences) —
Δ = log(A / B)Symmetric around 0. Handles ratios cleanly and plays nicely with wide dynamic ranges.
Diverging workflow:
- Derive Δ field — compute the difference quantity
- Choose scale — linear or symlog (symlog for wide dynamic ranges near zero)
- Apply diverging colormap centered at 0 — toggle diverging mode
- Optional tolerance band (epsilon) — suppress a dead zone around zero
Cyclic colormaps encode "wrap-around / periodic" — data where start and end represent the same value (0° ≡ 360°).
Use when:
- Data is periodic with no true endpoints
- There must be no visual discontinuity at the boundary
Examples: angle, phase, orientation, wind direction, time of day (circular).
Properties:
- Ends match seamlessly — color at min == color at max
- No discontinuity at boundaries
- Not suitable for ordered or magnitude data
Categorical colormaps encode "different kinds, not ordered" — discrete labels with no inherent ranking.
Use when:
- Data represents discrete labels with no meaningful ordering
- You need maximum visual distinction between classes
Examples: material IDs, cluster labels, classes, region tags.
Properties:
- Distinct, maximally separated colors
- No gradient or implied ordering between colors
- Not suitable for continuous or magnitude data
Note: In trame-colormaps, any preset can be turned into a categorical colormap via the discrete banding feature. This also serves as a way to apply color-based contours to continuous data — discrete bands act as visual iso-surfaces that segment the color range into distinct regions.
A JSON array of colormap names that controls which presets are active by default.
Includes all non-Brewer presets, only the highest-count Brewer variants, and the
core 15 Crameri colormaps. Each preset entry includes a "ColorBlindSafe"
boolean field for filtering.
On import, the active preset list is loaded from default_presets.json. Use
set_active_presets() to override it at runtime:
from trame_colormaps.core.presets import get_active_presets, set_active_presets
# Get the current active list
presets = get_active_presets()
# Set from a Python list
set_active_presets(["Cool to Warm", "batlow", "vik", "Viridis (matplotlib)"])
# Set from a JSON file
set_active_presets("/path/to/my_presets.json")from trame.app import TrameApp
from trame.dataclasses.colormaps import ColormapConfig
from trame.widgets.colormaps import HorizontalScalarBar
class MyApp(TrameApp):
def __init__(self, server=None):
super().__init__(server)
# ... set up VTK pipeline, mapper, etc. ...
self.colormap = ColormapConfig(
self.server,
mapper=self.mapper,
data_array_fn=self.get_data_array,
).set_data_array("Temperature", self.get_data_array, "point")
# Re-render when the colormap updates the mapper
self.colormap.watch(["mapper_change"], self.render)
self._build_ui()
def get_data_array(self):
ds = self.source.GetOutput()
return ds.GetPointData().GetScalars() if ds else None
def render(self, *_):
self.ctx.view.update()
def _build_ui(self):
with SinglePageLayout(self.server) as self.ui:
with self.ui.content:
# ... 3D view ...
with self.colormap.provide_as("bar"):
HorizontalScalarBar("bar", popup_location="top")Each ColormapConfig instance is independent. When one control panel opens,
all others close automatically:
self.top = ColormapConfig(server, mapper=mapper, data_array_fn=get_data)
self.top.set_data_array("RTData", get_data, "point")
self.left = ColormapConfig(server, mapper=mapper, data_array_fn=get_data)
self.left.set_data_array("RTData", get_data, "point")
# In UI:
with self.top.provide_as("top"):
HorizontalScalarBar("top", popup_location="bottom")
with self.left.provide_as("left"):
VerticalScalarBar("left", popup_location="right")When the underlying data changes (e.g. new data loaded, pipeline update),
call update_color_range() to recompute the range, re-apply transforms,
and regenerate ticks:
self.colormap.update_color_range()To color by a different variable without creating a new ColormapConfig:
self.colormap.set_data_array(
"Pressure",
data_array_fn=lambda: get_pressure_array(),
scalar_mode="point", # "cell" (default), "point", or "default"
)ColormapConfig in dataclasses.py is a trame.app.dataclass.StateDataModel
subclass. Fields fall into three groups:
| Field | Type | Default | Role |
|---|---|---|---|
| User-settable (bound to UI, triggers reactive updates) | |||
active_presets |
list[str] |
default_presets.json |
Preset names available in the picker |
preset |
str |
"BuGnYl" |
Active color preset name |
invert |
bool |
False |
Invert the transfer function |
color_blind |
bool |
False |
Filter preset list to color-blind safe |
use_log_scale |
str |
"linear" |
Scale mode: "linear", "log", "symlog" |
discrete_log |
bool |
False |
Enable discrete banding |
n_discrete_colors |
int |
4 |
Color bands between ticks (linear) or per decade (log/symlog) |
n_ticks |
int |
5 |
Number of tick marks on the colorbar |
color_value_min |
str |
"0" |
Manual range min (string for text field) |
color_value_max |
str |
"1" |
Manual range max (string for text field) |
override_range |
bool |
False |
Use manual range instead of data range |
cut_outside_range |
bool |
False |
Cut mode: out-of-range values use NaN color instead of endpoint color |
nan_color |
list[float] |
[0,0,0,0] |
RGBA color for NaN/missing values (transparent by default) |
| Derived (computed internally, read by UI) | |||
color_range |
tuple[float, float] |
(0, 1) |
Active min/max color range |
color_value_min_valid |
bool |
True |
Whether color_value_min parses as a valid float |
color_value_max_valid |
bool |
True |
Whether color_value_max parses as a valid float |
n_colors |
int |
255 |
Number of LUT samples |
lut_img_h |
str |
"" |
Base64 PNG data URI of the horizontal colorbar image |
lut_img_v |
str |
"" |
Base64 PNG data URI of the vertical colorbar image |
color_ticks |
list |
[] |
Tick marks: [{position, label, color}, ...] |
effective_color_range |
tuple[float, float] |
(0, 1) |
Actual CTF range after transforms |
luts_normal |
list |
[] |
Sorted preset picker entries (normal) |
luts_inverted |
list |
[] |
Sorted preset picker entries (inverted) |
| UI widget state | |||
menu |
bool |
False |
Whether the control panel popup is open |
search |
str | None |
None |
Preset search filter text |
orientation |
str |
"horizontal" |
Colorbar orientation |
show_nan_menu |
bool |
False |
Whether the NaN color dropdown is open |
mapper_change |
int |
0 |
Server-only counter incremented on each mapper update |
| Method | Purpose |
|---|---|
set_data_array(name, fn, scalar_mode) |
Configure the mapper's scalar mode and color array, recompute range, re-apply preset |
update_color_range() |
Recompute range from data (or validate manual range), re-apply transforms, regenerate ticks |
update_color_preset(name, invert, log_scale, ...) |
Apply a preset with scale/discrete settings — also called automatically by reactive watchers |
| Parameter | Default | Description |
|---|---|---|
server |
required | Trame server instance (first positional arg) |
mapper |
None |
VTK mapper — the CTF is set as its lookup table |
data_array_fn |
None |
Callable returning the VTK data array for range computation |
| Parameter | Default | Function | Description |
|---|---|---|---|
samples |
255 |
generate_colormaps() |
Horizontal pixels in each colorbar image |
preset_list |
default_presets.json |
set_active_presets() |
List of names or path to JSON file |
| Parameter | Default | Function | Description |
|---|---|---|---|
n |
5 |
get_nice_ticks() |
Desired number of ticks |
scale |
"linear" |
get_nice_ticks() |
Scale mode: "linear", "log", or "symlog" |
linthresh |
1.0 |
get_nice_ticks() |
Linear threshold for log/symlog scales |
Tick marks are computed identically for discrete and continuous modes:
- Linear: evenly spaced (e.g. 20%, 40%, 60%, 80% for
n_ticks=4) - Log: only powers of 10 (decade marks) that fall within the data range
- Symlog: powers of 10 filtered by visual position spacing — ticks far from zero (where the symlog transform expands the scale) are shown at every decade, while ticks near zero (where the transform compresses values) are adaptively thinned to prevent overlap. Zero is always shown when it falls within the data range and away from edges.
The adaptive spacing uses the symlog-transformed position of each candidate tick: only ticks that are at least 100/n percentage points apart in visual space are kept. This naturally produces a nonlinear stride — larger gaps near zero, smaller gaps at extremes.
| Parameter | Default | Function | Description |
|---|---|---|---|
n_sub |
1 |
All apply_discrete_*() functions |
Number of color bands per gap (linear) or per decade (log/symlog) |
n_samples |
256 |
apply_log(), apply_symlog(), apply_discrete_symlog() |
Resampling resolution for building continuous CTFs |
| Package | Used in | Purpose |
|---|---|---|
vtk (vtkmodules) |
core/presets.py, core/transforms.py, dataclasses.py |
vtkColorTransferFunction for color sampling, vtkPNGWriter/vtkImageData for colorbar image generation, mapper wiring |
| numpy | core/ticks.py, core/transforms.py, dataclasses.py |
Tick computation, LUT transforms, linthresh calculation |
| trame | dataclasses.py, widgets.py |
StateDataModel for reactive config, Vuetify 3 widgets for UI |
| trame-dataclass | dataclasses.py |
StateDataModel base class, Sync/ServerOnly field types, provide_as scoped slot |
src/trame_colormaps/
├── __init__.py # Package version
├── dataclasses.py # ColormapConfig(StateDataModel) — reactive state, CTF, mapper wiring
├── widgets.py # ColorMapEditor, HorizontalScalarBar, VerticalScalarBar
├── module.py # No-op trame module stub for enable_module
├── core/
│ ├── __init__.py # "Pure VTK/numpy, no trame dependency"
│ ├── presets.py # Preset discovery, COLORBAR_CACHE, lut_to_img()
│ ├── ticks.py # Tick computation (linear, log, symlog)
│ └── transforms.py # LUT transforms (linear, log, symlog, discrete variants)
└── presets/
├── __init__.py # Bundled preset JSON shipping
├── paraview_colormaps.json # 199 ParaView built-in presets
├── crameri_colormaps.json # 60 Crameri scientific colour maps
└── default_presets.json # Active preset list with color-blind-safe flags
src/trame/
├── dataclasses/
│ └── colormaps.py # Re-exports ColormapConfig
└── widgets/
└── colormaps.py # Re-exports widgets, initialize(server)
| Layer | Modules | Dependencies |
|---|---|---|
| Core (pure VTK/numpy) | core/presets.py, core/ticks.py, core/transforms.py |
VTK, numpy |
| State + Logic (Trame reactive model) | dataclasses.py |
Core + trame |
| Widgets (UI) | widgets.py |
trame (Vuetify 3, HTML) |
The core layer has zero Trame dependency and can be used independently for headless colormap operations.
HorizontalScalarBar / VerticalScalarBar produce the following DOM tree:
html.Div (top-level — flexbox row/column, bg-blue-grey-darken-2)
├── VMenu (activator="parent" — click anywhere on the bar to open)
│ └── ColorMapEditor → VCard (360px popup)
│ ├── VCardItem: toggle buttons (color-blind, invert, scale, range, discrete)
│ ├── VCardItem: discrete color count input (v-show when discrete)
│ ├── VCardItem: min/max text fields (v-show when override_range)
│ ├── VDivider
│ └── VList: searchable preset list with thumbnail images
├── html.Div (min range label)
├── html.Div (colorbar image container, position:relative)
│ ├── html.Img (LUT image — horizontal or vertical)
│ └── html.Div (tick overlay, position:absolute, pointer-events:none)
│ └── html.Div v-for="tick in <name>.color_ticks"
│ ├── html.Div (tick line)
│ └── html.Span (tick label)
└── html.Div (max range label)
Template bindings use <name>.* via config.provide_as("<name>").
When one control panel opens, all others close automatically.
The popup panel position is controlled by popup_location:
"top" → above bar, "bottom" → below, "left"/"right" for vertical bars.
| File | Description |
|---|---|
examples/wavelet.py |
4-region layout with horizontal + vertical colorbars around a 3D wavelet visualization |



