Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,8 @@ All parameters have sensible defaults and are backward-compatible keyword argume
| Field | Default | Description |
|-------|---------|-------------|
| `active_presets` | `default_presets.json` | List of preset names available in the picker |
| `n_intervals` | `4` | Number of equal intervals for discrete linear mode |
| `n_ticks` | `5` | Desired number of tick marks on the colorbar |
| `n_discrete_colors` | `4` | Number of sub-bands per interval (1–20) |
| `n_ticks` | `5` | Number of tick marks on the colorbar |
| `n_discrete_colors` | `4` | Color bands between ticks (linear) or per decade (log/symlog) |
| `n_colors` | `255` | Number of LUT samples |

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

| Parameter | Default | Function | Description |
|-----------|---------|----------|-------------|
| `n` | `5` | `compute_color_ticks()` | Desired number of tick marks |
| `min_gap` | `7` | `compute_color_ticks()` | Minimum gap between ticks (percentage points) |
| `edge_margin` | `3` | `compute_color_ticks()` | Minimum distance from edges (percentage points) |
| `linthresh` | `1.0` (symlog), `1e-15` (log) | `get_nice_ticks()`, `compute_color_ticks()` | Linear threshold for log/symlog scales |
| `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 mark behavior

Tick marks are computed identically for discrete and continuous modes:

- **Linear**: evenly spaced interval boundaries (e.g. 25%, 50%, 75% for `n_intervals=4`)
- **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.

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

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

## Dependencies
Expand Down Expand Up @@ -266,9 +264,8 @@ provides the canonical definition with defaults.
| `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` | Number of discrete sub-bands (1–20) |
| `n_intervals` | `int` | `4` | Equal intervals for discrete linear mode |
| `n_ticks` | `int` | `5` | Desired tick marks on the colorbar |
| `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 |
Expand Down
99 changes: 63 additions & 36 deletions src/trame_colormaps/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import math

import numpy as np
from vtkmodules.util.numpy_support import vtk_to_numpy
from vtkmodules.vtkRenderingCore import vtkColorTransferFunction

Expand All @@ -18,8 +19,9 @@
rescale_ctf,
)
from trame_colormaps.core.ticks import (
compute_color_ticks,
format_log_tick,
format_tick,
get_nice_ticks,
tick_contrast_color,
)
from trame_colormaps.core.transforms import (
Expand Down Expand Up @@ -103,7 +105,6 @@ def __init__(
"use_log_scale",
"discrete_log",
"n_discrete_colors",
"n_intervals",
"n_ticks",
],
self.update_color_preset,
Expand Down Expand Up @@ -144,7 +145,6 @@ def set_data_array(self, variable_name, data_array_fn, scalar_mode="cell"):
self.config.use_log_scale,
self.config.discrete_log,
self.config.n_discrete_colors,
self.config.n_intervals,
self.config.n_ticks,
)

Expand Down Expand Up @@ -186,7 +186,6 @@ def update_color_range(self):
self.config.use_log_scale,
self.config.discrete_log,
self.config.n_discrete_colors,
self.config.n_intervals,
self.config.n_ticks,
)

Expand All @@ -197,7 +196,6 @@ def update_color_preset(
log_scale,
discrete_log=False,
n_discrete_colors=4,
n_intervals=4,
n_ticks=5,
):
"""Apply a color preset with the specified scale and discrete settings.
Expand All @@ -207,8 +205,8 @@ def update_color_preset(
invert: Whether to invert the transfer function.
log_scale: Scale mode — ``"linear"``, ``"log"``, or ``"symlog"``.
discrete_log: Enable discrete (stepped) color banding.
n_discrete_colors: Number of sub-bands per interval (1–20).
n_intervals: Number of equal intervals for discrete linear mode.
n_discrete_colors: Number of color bands between ticks (linear)
or per decade (log/symlog).
n_ticks: Desired number of tick marks on the colorbar.
"""
self.config.preset = name
Expand Down Expand Up @@ -239,26 +237,44 @@ def update_color_preset(
n_sub = max(1, min(20, int(n_discrete_colors)))

if log_scale == "linear" and discrete_log:
vmin, vmax = self.config.color_range
tick_vals = get_nice_ticks(vmin, vmax, n_ticks, scale="linear")
result = apply_discrete_linear(
self._ctf, linear_rgb_points, n_sub, n_intervals=n_intervals
self._ctf, linear_rgb_points, n_sub, tick_vals=tick_vals
)
if result[0] is not None:
linear_rgb_points = result[0]
self.config.lut_img_h = result[2]
self.config.lut_img_v = result[3]
elif log_scale == "log":
if discrete_log:
# Compute major ticks (powers of 10) for discrete band boundaries
vmin, vmax = self.config.color_range
log_major_ticks = get_nice_ticks(
vmin, vmax, n_ticks, scale="log", linthresh=linthresh
)
# Keep only powers of 10 as boundaries
major_only = [
v
for v in log_major_ticks
if v > 0 and np.isclose(np.log10(v) % 1, 0, atol=1e-9)
]
result = apply_discrete_log(
self._ctf, linthresh, linear_rgb_points, n_sub
self._ctf,
linthresh,
linear_rgb_points,
n_sub,
tick_vals=major_only,
)
if result[0] is not None:
linear_rgb_points = result[0]
self.config.lut_img_h = result[2]
self.config.lut_img_v = result[3]
else:
apply_log(self._ctf, linthresh)
self.config.lut_img_h = lut_to_img_h(self._ctf)
self.config.lut_img_v = lut_to_img_v(self._ctf)
result = apply_log(self._ctf, linthresh, linear_rgb_points)
if result:
self.config.lut_img_h = result[0]
self.config.lut_img_v = result[1]
elif log_scale == "symlog":
if discrete_log:
result = apply_discrete_symlog(
Expand All @@ -277,13 +293,12 @@ def update_color_preset(
self._compute_ticks(
linthresh=linthresh,
linear_rgb_points=linear_rgb_points,
n_intervals=n_intervals,
n_ticks=n_ticks,
)

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

def _compute_ticks(
self, linthresh=None, linear_rgb_points=None, n_intervals=4, n_ticks=5
):
def _compute_ticks(self, linthresh=None, linear_rgb_points=None, n_ticks=5):
"""Compute tick positions, labels, and contrast colors for the colorbar.

Args:
linthresh: Linear threshold for log/symlog scale (None for linear).
linear_rgb_points: RGB control points from the linear CTF, used to
sample contrast colors. Falls back to current CTF points.
n_intervals: Number of equal intervals for linear tick placement.
n_ticks: Desired number of tick marks for log/symlog scales.
n_ticks: Desired number of tick marks (all scale modes).
"""
vmin, vmax = self.config.color_range

# Ticks are computed the same regardless of discrete mode.
if self.config.use_log_scale == "linear":
data_range = vmax - vmin
ticks = []
if data_range > 0:
for i in range(1, n_intervals):
val = vmin + data_range * i / n_intervals
pos = i / n_intervals * 100
data_range = vmax - vmin
ticks = []
if data_range > 0:
if self.config.use_log_scale == "linear":
tick_vals = get_nice_ticks(vmin, vmax, n_ticks, scale="linear")
for val in tick_vals:
pos = (val - vmin) / data_range * 100
ticks.append({"position": round(pos, 2), "label": format_tick(val)})
else:
ticks = compute_color_ticks(
vmin,
vmax,
scale=self.config.use_log_scale,
n=n_ticks,
linthresh=linthresh,
)
elif self.config.use_log_scale in ("log", "symlog"):
lt = linthresh if linthresh is not None else 1.0

def _sl(v):
return np.sign(v) * np.log10(1.0 + np.abs(v) / lt)

sl_min = float(_sl(vmin))
sl_max = float(_sl(vmax))
sl_range = sl_max - sl_min
tick_vals = get_nice_ticks(
vmin,
vmax,
n_ticks,
scale=self.config.use_log_scale,
linthresh=linthresh,
)
for val in tick_vals:
if sl_range > 0:
pos = (float(_sl(val)) - sl_min) / sl_range * 100
else:
pos = (val - vmin) / data_range * 100
ticks.append(
{"position": round(pos, 2), "label": format_log_tick(val)}
)

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