Skip to content

Commit 25451a1

Browse files
Merge pull request #1 from Kitware/fix/colormaps-and-ticks
fix: log colorbar shows linear space with log ticks
2 parents e6eef2f + ad75931 commit 25451a1

8 files changed

Lines changed: 640 additions & 532 deletions

File tree

README.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,8 @@ All parameters have sensible defaults and are backward-compatible keyword argume
138138
| Field | Default | Description |
139139
|-------|---------|-------------|
140140
| `active_presets` | `default_presets.json` | List of preset names available in the picker |
141-
| `n_intervals` | `4` | Number of equal intervals for discrete linear mode |
142-
| `n_ticks` | `5` | Desired number of tick marks on the colorbar |
143-
| `n_discrete_colors` | `4` | Number of sub-bands per interval (1–20) |
141+
| `n_ticks` | `5` | Number of tick marks on the colorbar |
142+
| `n_discrete_colors` | `4` | Color bands between ticks (linear) or per decade (log/symlog) |
144143
| `n_colors` | `255` | Number of LUT samples |
145144

146145
These are synced to the Trame client and trigger reactive updates via the controller.
@@ -156,16 +155,15 @@ These are synced to the Trame client and trigger reactive updates via the contro
156155

157156
| Parameter | Default | Function | Description |
158157
|-----------|---------|----------|-------------|
159-
| `n` | `5` | `compute_color_ticks()` | Desired number of tick marks |
160-
| `min_gap` | `7` | `compute_color_ticks()` | Minimum gap between ticks (percentage points) |
161-
| `edge_margin` | `3` | `compute_color_ticks()` | Minimum distance from edges (percentage points) |
162-
| `linthresh` | `1.0` (symlog), `1e-15` (log) | `get_nice_ticks()`, `compute_color_ticks()` | Linear threshold for log/symlog scales |
158+
| `n` | `5` | `get_nice_ticks()` | Desired number of ticks |
159+
| `scale` | `"linear"` | `get_nice_ticks()` | Scale mode: `"linear"`, `"log"`, or `"symlog"` |
160+
| `linthresh` | `1.0` | `get_nice_ticks()` | Linear threshold for log/symlog scales |
163161

164162
### Tick mark behavior
165163

166164
Tick marks are computed identically for discrete and continuous modes:
167165

168-
- **Linear**: evenly spaced interval boundaries (e.g. 25%, 50%, 75% for `n_intervals=4`)
166+
- **Linear**: evenly spaced (e.g. 20%, 40%, 60%, 80% for `n_ticks=4`)
169167
- **Log**: only powers of 10 (decade marks) that fall within the data range
170168
- **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.
171169

@@ -175,8 +173,8 @@ The adaptive spacing uses the symlog-transformed position of each candidate tick
175173

176174
| Parameter | Default | Function | Description |
177175
|-----------|---------|----------|-------------|
178-
| `n_intervals` | `4` | `apply_discrete_linear()` | Number of equal intervals to divide the data range |
179-
| `n_sub` | `1` | All `apply_discrete_*()` functions | Number of sub-bands per interval/decade |
176+
| `n_ticks` | `4` | `apply_discrete_linear()` | Number of tick marks (creates `n_ticks + 1` equal gaps) |
177+
| `n_sub` | `1` | All `apply_discrete_*()` functions | Number of color bands per gap (linear) or per decade (log/symlog) |
180178
| `n_samples` | `256` | `apply_discrete_log()`, `apply_symlog()`, `apply_discrete_symlog()` | Resampling resolution for building continuous CTFs |
181179

182180
## Dependencies
@@ -266,9 +264,8 @@ provides the canonical definition with defaults.
266264
| `color_blind` | `bool` | `False` | Filter preset list to color-blind safe |
267265
| `use_log_scale` | `str` | `"linear"` | Scale mode: `"linear"`, `"log"`, `"symlog"` |
268266
| `discrete_log` | `bool` | `False` | Enable discrete banding |
269-
| `n_discrete_colors` | `int` | `4` | Number of discrete sub-bands (1–20) |
270-
| `n_intervals` | `int` | `4` | Equal intervals for discrete linear mode |
271-
| `n_ticks` | `int` | `5` | Desired tick marks on the colorbar |
267+
| `n_discrete_colors` | `int` | `4` | Color bands between ticks (linear) or per decade (log/symlog) |
268+
| `n_ticks` | `int` | `5` | Number of tick marks on the colorbar |
272269
| `color_value_min` | `str` | `"0"` | Manual range min (string for text field) |
273270
| `color_value_max` | `str` | `"1"` | Manual range max (string for text field) |
274271
| `override_range` | `bool` | `False` | Use manual range instead of data range |

src/trame_colormaps/controller.py

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import math
88

9+
import numpy as np
910
from vtkmodules.util.numpy_support import vtk_to_numpy
1011
from vtkmodules.vtkRenderingCore import vtkColorTransferFunction
1112

@@ -18,8 +19,9 @@
1819
rescale_ctf,
1920
)
2021
from trame_colormaps.core.ticks import (
21-
compute_color_ticks,
22+
format_log_tick,
2223
format_tick,
24+
get_nice_ticks,
2325
tick_contrast_color,
2426
)
2527
from trame_colormaps.core.transforms import (
@@ -103,7 +105,6 @@ def __init__(
103105
"use_log_scale",
104106
"discrete_log",
105107
"n_discrete_colors",
106-
"n_intervals",
107108
"n_ticks",
108109
],
109110
self.update_color_preset,
@@ -144,7 +145,6 @@ def set_data_array(self, variable_name, data_array_fn, scalar_mode="cell"):
144145
self.config.use_log_scale,
145146
self.config.discrete_log,
146147
self.config.n_discrete_colors,
147-
self.config.n_intervals,
148148
self.config.n_ticks,
149149
)
150150

@@ -186,7 +186,6 @@ def update_color_range(self):
186186
self.config.use_log_scale,
187187
self.config.discrete_log,
188188
self.config.n_discrete_colors,
189-
self.config.n_intervals,
190189
self.config.n_ticks,
191190
)
192191

@@ -197,7 +196,6 @@ def update_color_preset(
197196
log_scale,
198197
discrete_log=False,
199198
n_discrete_colors=4,
200-
n_intervals=4,
201199
n_ticks=5,
202200
):
203201
"""Apply a color preset with the specified scale and discrete settings.
@@ -207,8 +205,8 @@ def update_color_preset(
207205
invert: Whether to invert the transfer function.
208206
log_scale: Scale mode — ``"linear"``, ``"log"``, or ``"symlog"``.
209207
discrete_log: Enable discrete (stepped) color banding.
210-
n_discrete_colors: Number of sub-bands per interval (1–20).
211-
n_intervals: Number of equal intervals for discrete linear mode.
208+
n_discrete_colors: Number of color bands between ticks (linear)
209+
or per decade (log/symlog).
212210
n_ticks: Desired number of tick marks on the colorbar.
213211
"""
214212
self.config.preset = name
@@ -239,26 +237,44 @@ def update_color_preset(
239237
n_sub = max(1, min(20, int(n_discrete_colors)))
240238

241239
if log_scale == "linear" and discrete_log:
240+
vmin, vmax = self.config.color_range
241+
tick_vals = get_nice_ticks(vmin, vmax, n_ticks, scale="linear")
242242
result = apply_discrete_linear(
243-
self._ctf, linear_rgb_points, n_sub, n_intervals=n_intervals
243+
self._ctf, linear_rgb_points, n_sub, tick_vals=tick_vals
244244
)
245245
if result[0] is not None:
246246
linear_rgb_points = result[0]
247247
self.config.lut_img_h = result[2]
248248
self.config.lut_img_v = result[3]
249249
elif log_scale == "log":
250250
if discrete_log:
251+
# Compute major ticks (powers of 10) for discrete band boundaries
252+
vmin, vmax = self.config.color_range
253+
log_major_ticks = get_nice_ticks(
254+
vmin, vmax, n_ticks, scale="log", linthresh=linthresh
255+
)
256+
# Keep only powers of 10 as boundaries
257+
major_only = [
258+
v
259+
for v in log_major_ticks
260+
if v > 0 and np.isclose(np.log10(v) % 1, 0, atol=1e-9)
261+
]
251262
result = apply_discrete_log(
252-
self._ctf, linthresh, linear_rgb_points, n_sub
263+
self._ctf,
264+
linthresh,
265+
linear_rgb_points,
266+
n_sub,
267+
tick_vals=major_only,
253268
)
254269
if result[0] is not None:
255270
linear_rgb_points = result[0]
256271
self.config.lut_img_h = result[2]
257272
self.config.lut_img_v = result[3]
258273
else:
259-
apply_log(self._ctf, linthresh)
260-
self.config.lut_img_h = lut_to_img_h(self._ctf)
261-
self.config.lut_img_v = lut_to_img_v(self._ctf)
274+
result = apply_log(self._ctf, linthresh, linear_rgb_points)
275+
if result:
276+
self.config.lut_img_h = result[0]
277+
self.config.lut_img_v = result[1]
262278
elif log_scale == "symlog":
263279
if discrete_log:
264280
result = apply_discrete_symlog(
@@ -277,13 +293,12 @@ def update_color_preset(
277293
self._compute_ticks(
278294
linthresh=linthresh,
279295
linear_rgb_points=linear_rgb_points,
280-
n_intervals=n_intervals,
281296
n_ticks=n_ticks,
282297
)
283298

284-
# For symlog (or any discrete mode), rebuild a separate CTF
299+
# For log, symlog (or any discrete mode), rebuild a separate CTF
285300
# so the mapper gets the correct points.
286-
if log_scale == "symlog" or (discrete_log and log_scale in ("log", "linear")):
301+
if log_scale in ("symlog", "log") or (discrete_log and log_scale == "linear"):
287302
pts = get_rgb_points(self._ctf)
288303
render_ctf = vtkColorTransferFunction()
289304
for i in range(0, len(pts), 4):
@@ -346,37 +361,49 @@ def _build_lut_lists(self, active_presets):
346361
self.config.luts_normal = luts_normal
347362
self.config.luts_inverted = luts_inverted
348363

349-
def _compute_ticks(
350-
self, linthresh=None, linear_rgb_points=None, n_intervals=4, n_ticks=5
351-
):
364+
def _compute_ticks(self, linthresh=None, linear_rgb_points=None, n_ticks=5):
352365
"""Compute tick positions, labels, and contrast colors for the colorbar.
353366
354367
Args:
355368
linthresh: Linear threshold for log/symlog scale (None for linear).
356369
linear_rgb_points: RGB control points from the linear CTF, used to
357370
sample contrast colors. Falls back to current CTF points.
358-
n_intervals: Number of equal intervals for linear tick placement.
359-
n_ticks: Desired number of tick marks for log/symlog scales.
371+
n_ticks: Desired number of tick marks (all scale modes).
360372
"""
361373
vmin, vmax = self.config.color_range
362374

363-
# Ticks are computed the same regardless of discrete mode.
364-
if self.config.use_log_scale == "linear":
365-
data_range = vmax - vmin
366-
ticks = []
367-
if data_range > 0:
368-
for i in range(1, n_intervals):
369-
val = vmin + data_range * i / n_intervals
370-
pos = i / n_intervals * 100
375+
data_range = vmax - vmin
376+
ticks = []
377+
if data_range > 0:
378+
if self.config.use_log_scale == "linear":
379+
tick_vals = get_nice_ticks(vmin, vmax, n_ticks, scale="linear")
380+
for val in tick_vals:
381+
pos = (val - vmin) / data_range * 100
371382
ticks.append({"position": round(pos, 2), "label": format_tick(val)})
372-
else:
373-
ticks = compute_color_ticks(
374-
vmin,
375-
vmax,
376-
scale=self.config.use_log_scale,
377-
n=n_ticks,
378-
linthresh=linthresh,
379-
)
383+
elif self.config.use_log_scale in ("log", "symlog"):
384+
lt = linthresh if linthresh is not None else 1.0
385+
386+
def _sl(v):
387+
return np.sign(v) * np.log10(1.0 + np.abs(v) / lt)
388+
389+
sl_min = float(_sl(vmin))
390+
sl_max = float(_sl(vmax))
391+
sl_range = sl_max - sl_min
392+
tick_vals = get_nice_ticks(
393+
vmin,
394+
vmax,
395+
n_ticks,
396+
scale=self.config.use_log_scale,
397+
linthresh=linthresh,
398+
)
399+
for val in tick_vals:
400+
if sl_range > 0:
401+
pos = (float(_sl(val)) - sl_min) / sl_range * 100
402+
else:
403+
pos = (val - vmin) / data_range * 100
404+
ticks.append(
405+
{"position": round(pos, 2), "label": format_log_tick(val)}
406+
)
380407

381408
# Sample colors from the *linear* CTF so tick contrast matches the
382409
# displayed colorbar image, not the log/symlog-remapped rendering CTF.

0 commit comments

Comments
 (0)