@@ -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 )
0 commit comments