Skip to content

Commit f4b6514

Browse files
Merge pull request #4 from Kitware/feat/divergent-colormaps
feat(diverging): diverging colormap mode with symmetric range and epsilon dead zone
2 parents be63dd0 + e2aca30 commit f4b6514

8 files changed

Lines changed: 1055 additions & 242 deletions

File tree

README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,123 @@ All colormap presets are stored as JSON files under `src/trame_colormaps/presets
106106
- All are perceptually uniform and color-blind safe
107107
- Downloaded from the [cmcrameri GitHub repository](https://github.com/callumrollo/cmcrameri)
108108

109+
### Colormap Usage Guide
110+
111+
| Category | Use When | Data Character |
112+
|----------|----------|----------------|
113+
| **Sequential** | Magnitude — more/less of something | Temperature, pressure, density |
114+
| **Diverging** | Deviation — Δ from a reference value | Anomaly, residual, balance |
115+
| **Cyclic** | Periodic — values that wrap around | Phase, angle, time-of-day |
116+
| **Categorical** | Discrete labels — no inherent order | Material ID, region, class |
117+
118+
> **Note:** Categorical presets are excluded from `default_presets.json` because
119+
> trame-colormaps generates its own discrete/categorical colormaps from any preset
120+
> via the discrete banding feature.
121+
122+
#### Using Sequential Colormaps
123+
124+
Sequential colormaps encode **"more vs less"** — data that is ordered and one-sided
125+
with no special reference value.
126+
127+
**Use when:**
128+
129+
- Data interpretation is monotonic: low → high
130+
- There is no meaningful midpoint or zero crossing
131+
132+
**Examples:** temperature (no reference), density, probability, intensity, error magnitude (`|Δ|`).
133+
134+
**Properties:**
135+
136+
- Monotonic lightness — darker always means more (or less)
137+
- No implied midpoint
138+
- Easy to interpret quantitatively
139+
140+
**Good defaults:** Viridis, Plasma, batlow — perceptually uniform ramps.
141+
142+
#### Using Diverging Colormaps
143+
144+
Diverging colormaps encode **"above vs below reference"** — data with a meaningful
145+
center value where you care about the direction of deviation.
146+
147+
**Use when:**
148+
149+
- There is a meaningful center (usually 0, but not always)
150+
- You care about direction: below reference ← neutral → above reference
151+
152+
**Examples:** Δ = A − B, anomalies (value − mean), residuals, signed errors.
153+
154+
**Properties:**
155+
156+
- Two symmetric color branches around a neutral center (white/light gray)
157+
- Encodes both sign and magnitude
158+
- Must be centered correctly to avoid misinterpretation
159+
- Should be perceptually balanced on both sides
160+
161+
**Common derived difference fields:**
162+
163+
1. **Absolute difference**`Δ = A − B`
164+
Your primary case (simulation vs observation). Symmetric, interpretable.
165+
166+
2. **Relative / percent difference**`Δ = (A − B) / B` or `Δ% = 100 × (A − B) / B`
167+
Useful when scale matters. Still centered at 0 → diverging applies.
168+
169+
3. **Deviation from a baseline**`Δ = value − reference_value`
170+
Examples: temperature − freezing point, measurement − target threshold, field − spatial mean.
171+
172+
4. **Standardized anomaly**`Δ = (value − mean) / std`
173+
Now Δ is in "number of standard deviations." Very common in climate and statistics.
174+
175+
5. **Log-ratio (for multiplicative differences)**`Δ = log(A / B)`
176+
Symmetric around 0. Handles ratios cleanly and plays nicely with wide dynamic ranges.
177+
178+
**Diverging workflow:**
179+
180+
1. **Derive Δ field** — compute the difference quantity
181+
2. **Choose scale** — linear or symlog (symlog for wide dynamic ranges near zero)
182+
3. **Apply diverging colormap centered at 0** — toggle diverging mode
183+
4. **Optional tolerance band (epsilon)** — suppress a dead zone around zero
184+
185+
#### Using Cyclic Colormaps
186+
187+
Cyclic colormaps encode **"wrap-around / periodic"** — data where start and end
188+
represent the same value (0° ≡ 360°).
189+
190+
**Use when:**
191+
192+
- Data is periodic with no true endpoints
193+
- There must be no visual discontinuity at the boundary
194+
195+
**Examples:** angle, phase, orientation, wind direction, time of day (circular).
196+
197+
**Properties:**
198+
199+
- Ends match seamlessly — color at min == color at max
200+
- No discontinuity at boundaries
201+
- Not suitable for ordered or magnitude data
202+
203+
#### Using Categorical Colormaps
204+
205+
Categorical colormaps encode **"different kinds, not ordered"** — discrete labels
206+
with no inherent ranking.
207+
208+
**Use when:**
209+
210+
- Data represents discrete labels with no meaningful ordering
211+
- You need maximum visual distinction between classes
212+
213+
**Examples:** material IDs, cluster labels, classes, region tags.
214+
215+
**Properties:**
216+
217+
- Distinct, maximally separated colors
218+
- No gradient or implied ordering between colors
219+
- Not suitable for continuous or magnitude data
220+
221+
> **Note:** In trame-colormaps, any preset can be turned into a categorical
222+
> colormap via the discrete banding feature. This also serves as a way to apply
223+
> color-based contours to continuous data — discrete bands act as visual
224+
> iso-surfaces that segment the color range into distinct regions.
225+
109226
### `default_presets.json` — Active Preset List
110227

111228
A JSON array of colormap names that controls which presets are active by default.

src/trame_colormaps/core/presets.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ def _init_registry():
2727
"""Load all presets and build the module-level registry constants.
2828
2929
Returns:
30-
Tuple of (registry, all_names, color_blind_safe, default_presets).
30+
Tuple of (registry, all_names, color_blind_safe, default_presets,
31+
diverging, sequential, cyclic, categorical).
3132
"""
3233

3334
def load(filename):
@@ -48,10 +49,45 @@ def load(filename):
4849
if defaults_path.exists()
4950
else sorted(all_names)
5051
)
51-
return registry, all_names, color_blind, defaults
52+
53+
# Category-based preset sets
54+
diverging = {
55+
n for n, p in registry.items() if p.get("Category", "").lower() == "diverging"
56+
}
57+
sequential = {
58+
n for n, p in registry.items() if p.get("Category", "").lower() == "sequential"
59+
}
60+
cyclic = {
61+
n for n, p in registry.items() if p.get("Category", "").lower() == "cyclic"
62+
}
63+
categorical = {
64+
n
65+
for n, p in registry.items()
66+
if p.get("Category", "").lower() in ("categorical", "multi-sequential")
67+
}
68+
69+
return (
70+
registry,
71+
all_names,
72+
color_blind,
73+
defaults,
74+
diverging,
75+
sequential,
76+
cyclic,
77+
categorical,
78+
)
5279

5380

54-
PRESET_REGISTRY, ALL_PRESETS, COLOR_BLIND_SAFE, DEFAULT_PRESETS = _init_registry()
81+
(
82+
PRESET_REGISTRY,
83+
ALL_PRESETS,
84+
COLOR_BLIND_SAFE,
85+
DEFAULT_PRESETS,
86+
DIVERGING_PRESETS,
87+
SEQUENTIAL_PRESETS,
88+
CYCLIC_PRESETS,
89+
CATEGORICAL_PRESETS,
90+
) = _init_registry()
5591

5692
#: Module-level active preset list. Modified via set_active_presets().
5793
_active_presets = [n for n in DEFAULT_PRESETS if n in PRESET_REGISTRY]

0 commit comments

Comments
 (0)