Skip to content

Commit b507f11

Browse files
olearypatrickjourdain
authored andcommitted
fix(symlog): align epsilon dead zone with symlog transform
Skip _inject_epsilon_band for symlog scale and pass epsilon directly to apply_symlog / apply_discrete_symlog. Dead zone is now injected after the symlog CTF is built via _remap_with_dead_zone, which compresses points in (0,vmax]→[eps,vmax] and [vmin,0)→[vmin,-eps]. Display points use asymmetric boundaries from symlog(±eps) so the dead zone edges align exactly with tick marks.
1 parent 372e2d8 commit b507f11

3 files changed

Lines changed: 321 additions & 6 deletions

File tree

src/trame_colormaps/core/transforms.py

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,19 +412,92 @@ def _sl(v):
412412
return display_rgb_points, discrete_tick_data, lut_img, lut_img_v
413413

414414

415-
def apply_symlog(ctf, linthresh, linear_rgb_points=None, n_samples=256):
415+
def _remap_with_dead_zone(pts, vmin, vmax, center, neg_eps, pos_eps, cr, cg, cb):
416+
"""Compress control points outward from a dead zone.
417+
418+
Points in ``[vmin, center)`` are linearly remapped into ``[vmin, neg_eps]``.
419+
Points in ``(center, vmax]`` are linearly remapped into ``[pos_eps, vmax]``.
420+
The band ``[neg_eps, center, pos_eps]`` is filled with the center color
421+
``(cr, cg, cb)``.
422+
423+
All original colors are retained — they are squeezed outward from
424+
the dead zone rather than discarded.
425+
426+
Args:
427+
pts: Flat list ``[x, r, g, b, ...]`` of control points.
428+
vmin: Minimum x value of the range.
429+
vmax: Maximum x value of the range.
430+
center: The x value around which the dead zone is centered.
431+
neg_eps: Negative boundary of the dead zone.
432+
pos_eps: Positive boundary of the dead zone.
433+
cr, cg, cb: Center color (flat band fill).
434+
435+
Returns a new flat list ``[x, r, g, b, ...]``.
436+
"""
437+
n = len(pts) // 4
438+
left_pts = []
439+
right_pts = []
440+
for i in range(n):
441+
x = pts[i * 4]
442+
r, g, b = pts[i * 4 + 1], pts[i * 4 + 2], pts[i * 4 + 3]
443+
if x < center:
444+
left_pts.append((x, r, g, b))
445+
elif x > center:
446+
right_pts.append((x, r, g, b))
447+
448+
new_pts = []
449+
# Remap left: [vmin, center) → [vmin, neg_eps]
450+
if left_pts:
451+
old_range = center - vmin
452+
new_range = neg_eps - vmin
453+
for x, r, g, b in left_pts:
454+
if old_range != 0:
455+
t = (x - vmin) / old_range
456+
nx = vmin + t * new_range
457+
else:
458+
nx = vmin
459+
new_pts.extend([nx, r, g, b])
460+
461+
# Dead zone band
462+
new_pts.extend([neg_eps, cr, cg, cb])
463+
new_pts.extend([center, cr, cg, cb])
464+
new_pts.extend([pos_eps, cr, cg, cb])
465+
466+
# Remap right: (center, vmax] → [pos_eps, vmax]
467+
if right_pts:
468+
old_range = vmax - center
469+
new_range = vmax - pos_eps
470+
for x, r, g, b in right_pts:
471+
if old_range != 0:
472+
t = (x - center) / old_range
473+
nx = pos_eps + t * new_range
474+
else:
475+
nx = vmax
476+
new_pts.extend([nx, r, g, b])
477+
478+
return new_pts
479+
480+
481+
def apply_symlog(ctf, linthresh, linear_rgb_points=None, n_samples=256, epsilon=0.0):
416482
"""Build a symlog CTF with decade control points.
417483
418484
Control points are placed at powers of 10 (and ±linthresh, 0 for
419485
mixed-sign data). The RGB color for each control point is sampled
420486
from the linear colorbar at the position where that value falls in
421487
symlog space: t = (symlog(v) - symlog(min)) / (symlog(max) - symlog(min)).
422488
489+
When *epsilon* > 0 (diverging mode), a dead zone is injected around
490+
zero. Control points in (0, vmax] are compressed into [+eps, vmax]
491+
and points in [vmin, 0) into [vmin, -eps]. The band [-eps, +eps]
492+
is held at the center color. All original colors are retained —
493+
they are squeezed outward from the dead zone rather than discarded.
494+
423495
Args:
424496
ctf: vtkColorTransferFunction.
425497
linthresh: Linear threshold for symlog transformation.
426498
linear_rgb_points: RGB control points from the linear LUT.
427499
n_samples: Number of uniform samples in symlog space for building the CTF.
500+
epsilon: Half-width of the dead zone around zero (data-space units).
428501
429502
Returns:
430503
Tuple of (lut_img_h, lut_img_v) base64 PNG strings, or None if
@@ -485,6 +558,47 @@ def symlog(v):
485558
# Display points: uniform linear positions with symlog colors
486559
display_rgb_points.extend([x_lookup, r, g, b])
487560

561+
# --- Inject epsilon dead zone in symlog space ---
562+
eps = max(0.0, float(epsilon))
563+
if eps > 0 and x_min < 0 < x_max:
564+
# Sample center color from the unmodified points at x=0
565+
center_t = (float(symlog(0.0)) - s_min) / s_range
566+
center_lookup = x_min + center_t * data_range
567+
linear_ctf.GetColor(center_lookup, rgb)
568+
cr, cg, cb = float(rgb[0]), float(rgb[1]), float(rgb[2])
569+
570+
# Display-space positions of center and ±epsilon
571+
s_eps_pos = float(symlog(eps))
572+
s_eps_neg = float(symlog(-eps))
573+
d_center = center_lookup
574+
d_eps_pos = x_min + (s_eps_pos - s_min) / s_range * data_range
575+
d_eps_neg = x_min + (s_eps_neg - s_min) / s_range * data_range
576+
577+
# Remap rendering points (data space, center=0)
578+
new_rgb_points = _remap_with_dead_zone(
579+
new_rgb_points,
580+
x_min,
581+
x_max,
582+
0.0,
583+
-eps,
584+
eps,
585+
cr,
586+
cg,
587+
cb,
588+
)
589+
# Remap display points (display space, asymmetric boundaries)
590+
display_rgb_points = _remap_with_dead_zone(
591+
display_rgb_points,
592+
x_min,
593+
x_max,
594+
d_center,
595+
d_eps_neg,
596+
d_eps_pos,
597+
cr,
598+
cg,
599+
cb,
600+
)
601+
488602
# Regenerate colorbar image from display points so it matches the 3D
489603
set_rgb_points(ctf, display_rgb_points)
490604
lut_img = lut_to_img_h(ctf)
@@ -498,7 +612,14 @@ def symlog(v):
498612
return lut_img, lut_img_v
499613

500614

501-
def apply_discrete_symlog(ctf, linthresh, linear_rgb_points, n_sub=1, n_samples=256):
615+
def apply_discrete_symlog(
616+
ctf,
617+
linthresh,
618+
linear_rgb_points,
619+
n_sub=1,
620+
n_samples=256,
621+
epsilon=0.0,
622+
):
502623
"""Build a discrete (stepped) symlog CTF.
503624
504625
Each decade interval is split into *n_sub* equal sub-bands in symlog
@@ -507,12 +628,16 @@ def apply_discrete_symlog(ctf, linthresh, linear_rgb_points, n_sub=1, n_samples=
507628
steps at the sub-band boundaries. The display image is also replaced
508629
with a banded colorbar.
509630
631+
When *epsilon* > 0, the finished discrete bands are remapped to
632+
inject a dead zone around zero (same approach as ``apply_symlog``).
633+
510634
Args:
511635
ctf: vtkColorTransferFunction.
512636
linthresh: Linear threshold for symlog transformation.
513637
linear_rgb_points: RGB control points from the linear LUT.
514638
n_sub: Number of sub-bands per decade interval.
515639
n_samples: Number of uniform samples in symlog space for building the CTF.
640+
epsilon: Half-width of the dead zone around zero (data-space units).
516641
517642
Returns:
518643
Tuple of (display_rgb_points, discrete_tick_data, lut_img_h,
@@ -673,6 +798,48 @@ def symlog(v):
673798

674799
band_idx += 1
675800

801+
# --- Inject epsilon dead zone ---
802+
dead_eps = max(0.0, float(epsilon))
803+
if dead_eps > 0 and x_min < 0 < x_max:
804+
# Sample center color from the linear CTF at symlog(0)
805+
center_t = (float(symlog(0.0)) - s_min) / s_range
806+
center_lookup = x_min + center_t * data_range
807+
rgb_center = [0.0, 0.0, 0.0]
808+
linear_ctf.GetColor(center_lookup, rgb_center)
809+
cr = float(rgb_center[0])
810+
cg = float(rgb_center[1])
811+
cb = float(rgb_center[2])
812+
813+
# Display-space positions of center and ±epsilon
814+
s_eps_pos = float(symlog(dead_eps))
815+
s_eps_neg = float(symlog(-dead_eps))
816+
d_center = center_lookup
817+
d_eps_pos = x_min + (s_eps_pos - s_min) / s_range * data_range
818+
d_eps_neg = x_min + (s_eps_neg - s_min) / s_range * data_range
819+
820+
render_rgb_points = _remap_with_dead_zone(
821+
render_rgb_points,
822+
x_min,
823+
x_max,
824+
0.0,
825+
-dead_eps,
826+
dead_eps,
827+
cr,
828+
cg,
829+
cb,
830+
)
831+
display_rgb_points = _remap_with_dead_zone(
832+
display_rgb_points,
833+
x_min,
834+
x_max,
835+
d_center,
836+
d_eps_neg,
837+
d_eps_pos,
838+
cr,
839+
cg,
840+
cb,
841+
)
842+
676843
# Generate the discrete banded colorbar image
677844
set_rgb_points(ctf, display_rgb_points)
678845
lut_img = lut_to_img_h(ctf)

src/trame_colormaps/dataclasses.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,10 @@ def update_color_preset(
662662
apply_linear(self._ctf, name, invert)
663663
rescale_ctf(self._ctf, *self.color_range)
664664

665-
# In diverging mode, inject an epsilon dead zone around zero
666-
if self.diverging:
665+
# In diverging mode, inject an epsilon dead zone around zero.
666+
# For symlog, the dead zone is handled inside apply_symlog /
667+
# apply_discrete_symlog so that it aligns with the symlog transform.
668+
if self.diverging and log_scale != "symlog":
667669
self._inject_epsilon_band()
668670

669671
# Capture the linear colorbar image (always the same regardless of scale)
@@ -737,14 +739,32 @@ def update_color_preset(
737739
self.lut_img_h = result[0]
738740
self.lut_img_v = result[1]
739741
elif log_scale == "symlog":
742+
# Compute epsilon for diverging symlog dead zone
743+
symlog_eps = 0.0
744+
if self.diverging and self.epsilon_valid:
745+
try:
746+
symlog_eps = max(0.0, float(self.epsilon))
747+
except (ValueError, TypeError):
748+
symlog_eps = 0.0
740749
if discrete_log:
741-
result = apply_discrete_symlog(self._ctf, linthresh, linear_rgb_points, n_sub)
750+
result = apply_discrete_symlog(
751+
self._ctf,
752+
linthresh,
753+
linear_rgb_points,
754+
n_sub,
755+
epsilon=symlog_eps,
756+
)
742757
if result[0] is not None:
743758
linear_rgb_points = result[0]
744759
self.lut_img_h = result[2]
745760
self.lut_img_v = result[3]
746761
else:
747-
result = apply_symlog(self._ctf, linthresh, linear_rgb_points)
762+
result = apply_symlog(
763+
self._ctf,
764+
linthresh,
765+
linear_rgb_points,
766+
epsilon=symlog_eps,
767+
)
748768
if result:
749769
self.lut_img_h = result[0]
750770
self.lut_img_v = result[1]

0 commit comments

Comments
 (0)