Skip to content

Commit 82fd4ac

Browse files
fix(lut): symlog colorbar matching and discrete sampling (Kitware#67)
Co-authored-by: Patrick O'Leary <olearypatrick@gmail.com>
1 parent eeb40dc commit 82fd4ac

4 files changed

Lines changed: 70 additions & 30 deletions

File tree

src/e3sm_quickview/components/view.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,12 @@ def create_bottom_bar(config, update_color_preset):
227227
variant="outlined",
228228
flat=True,
229229
label=(
230-
"config.use_log_scale === 'linear' ? 'Colors per tick interval' : 'Colors per decade'",
230+
"config.use_log_scale === 'linear' ? 'Colors per tick interval' : 'Colors per order of magnitude'",
231231
),
232232
classes="mt-2",
233233
step=[1],
234234
min=[1],
235-
max=[5],
235+
max=[20],
236236
)
237237
with v3.VCardItem(
238238
v_show="config.override_range", classes="py-0 mb-2"

src/e3sm_quickview/utils/math.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,6 @@ def snap(val):
233233
ticks_set.add(val)
234234
if vmin <= 0 <= vmax:
235235
ticks_set.add(0.0)
236-
ticks_set.add(vmin)
237-
ticks_set.add(vmax)
238236
raw_ticks = np.array(sorted(ticks_set))
239237
# Skip snap — powers of 10 are already nice
240238
return raw_ticks
@@ -258,7 +256,7 @@ def format_tick(val):
258256
shown as '10^N', very large/small values use scientific notation, and
259257
intermediate values use fixed-point.
260258
"""
261-
if np.isclose(val, 0, atol=1e-12):
259+
if val == 0:
262260
return "0"
263261

264262
val_abs = abs(val)
@@ -354,7 +352,7 @@ def _symlog_fn(v):
354352
else:
355353
pos = (val - vmin) / data_range * 100
356354
if edge_margin <= pos <= (100 - edge_margin):
357-
is_zero = np.isclose(val, 0, atol=1e-12)
355+
is_zero = val == 0
358356
if is_zero:
359357
has_zero = True
360358
candidates.append(

src/e3sm_quickview/view_manager.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def update_color_preset(
226226
else:
227227
linthresh = 1.0
228228

229-
n_sub = max(1, min(5, int(n_discrete_colors)))
229+
n_sub = max(1, min(20, int(n_discrete_colors)))
230230
if log_scale == "linear" and discrete_log:
231231
display_rgb_points = self._apply_discrete_linear_to_lut(
232232
linear_rgb_points, n_sub
@@ -560,18 +560,24 @@ def symlog(v):
560560
# Sample RGB from the linear CTF at symlog-normalized positions
561561
rgb = [0.0, 0.0, 0.0]
562562
new_rgb_points = []
563+
display_rgb_points = []
563564
for v in breakpoints:
564565
t = (float(symlog(v)) - s_min) / s_range
565566
x_lookup = x_min + t * data_range
566567
linear_ctf.GetColor(x_lookup, rgb)
567-
new_rgb_points.extend(
568-
[float(v), float(rgb[0]), float(rgb[1]), float(rgb[2])]
569-
)
568+
r, g, b = float(rgb[0]), float(rgb[1]), float(rgb[2])
569+
new_rgb_points.extend([float(v), r, g, b])
570+
# Display points: uniform linear positions with symlog colors
571+
display_rgb_points.extend([x_lookup, r, g, b])
572+
573+
# Regenerate colorbar image from display points so it matches the 3D
574+
self.lut.UseLogScale = 0
575+
self.lut.RGBPoints = display_rgb_points
576+
self.config.lut_img = lut_to_img(self.lut)
570577

571-
# Store on proxy for bookkeeping — the actual CTF used by the
578+
# Store rendering points on proxy — the actual CTF used by the
572579
# mapper is a standalone vtkColorTransferFunction built in
573580
# update_color_preset to avoid proxy client-side object issues.
574-
self.lut.UseLogScale = 0
575581
self.lut.RGBPoints = new_rgb_points
576582

577583
def _apply_discrete_symlog_to_lut(self, linthresh, linear_rgb_points, n_sub=1):
@@ -666,7 +672,8 @@ def symlog(v):
666672
else:
667673
self._discrete_tick_data = all_tick_data
668674

669-
# Build a temporary linear CTF from the saved linear RGB points
675+
# Build a continuous symlog CTF (same as _apply_symlog_to_lut) so
676+
# discrete bands sample colours that match the continuous rendering.
670677
from vtkmodules.vtkRenderingCore import vtkColorTransferFunction
671678

672679
linear_ctf = vtkColorTransferFunction()
@@ -678,12 +685,24 @@ def symlog(v):
678685
linear_rgb_points[i + 3],
679686
)
680687

688+
n_samples = 256
689+
s_vals = np.linspace(s_min, s_max, n_samples)
690+
symlog_ctf = vtkColorTransferFunction()
691+
rgb_tmp = [0.0, 0.0, 0.0]
692+
for s in s_vals:
693+
v = float(np.sign(s) * linthresh * (10.0 ** abs(s) - 1.0))
694+
v = max(x_min, min(x_max, v))
695+
t = (s - s_min) / s_range
696+
x_lookup = x_min + t * data_range
697+
linear_ctf.GetColor(x_lookup, rgb_tmp)
698+
symlog_ctf.AddRGBPoint(v, rgb_tmp[0], rgb_tmp[1], rgb_tmp[2])
699+
681700
# For each decade interval, split into n_sub equal sub-bands in
682701
# symlog space. Each sub-band gets a flat color sampled from the
683-
# continuous LUT at the sub-band midpoint.
702+
# continuous symlog LUT at the sub-band midpoint.
684703
rgb = [0.0, 0.0, 0.0]
685704
eps_data = (x_max - x_min) * 1e-9
686-
eps_lin = 1e-9
705+
eps_lin = data_range * 1e-9
687706
display_rgb_points = []
688707
render_rgb_points = []
689708
band_idx = 0
@@ -696,9 +715,11 @@ def symlog(v):
696715
s_lo = s_lo_decade + (s_hi_decade - s_lo_decade) * j / n_sub
697716
s_hi = s_lo_decade + (s_hi_decade - s_lo_decade) * (j + 1) / n_sub
698717
s_mid = (s_lo + s_hi) / 2.0
699-
t_mid = (s_mid - s_min) / s_range
700-
x_lookup = x_min + t_mid * data_range
701-
linear_ctf.GetColor(x_lookup, rgb)
718+
719+
# Invert symlog to get data-space values
720+
v_mid = float(np.sign(s_mid) * linthresh * (10.0 ** abs(s_mid) - 1.0))
721+
v_mid = max(x_min, min(x_max, v_mid))
722+
symlog_ctf.GetColor(v_mid, rgb)
702723
r, g, b = float(rgb[0]), float(rgb[1]), float(rgb[2])
703724

704725
# Invert symlog to get data-space boundaries for rendering

src/e3sm_quickview/view_manager2.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def update_color_preset(
223223
else:
224224
linthresh = 1.0
225225

226-
n_sub = max(1, min(5, int(n_discrete_colors)))
226+
n_sub = max(1, min(20, int(n_discrete_colors)))
227227
if log_scale == "linear" and discrete_log:
228228
display_rgb_points = self._apply_discrete_linear_to_lut(
229229
linear_rgb_points, n_sub
@@ -560,18 +560,24 @@ def symlog(v):
560560
# Sample RGB from the linear CTF at symlog-normalized positions
561561
rgb = [0.0, 0.0, 0.0]
562562
new_rgb_points = []
563+
display_rgb_points = []
563564
for v in breakpoints:
564565
t = (float(symlog(v)) - s_min) / s_range
565566
x_lookup = x_min + t * data_range
566567
linear_ctf.GetColor(x_lookup, rgb)
567-
new_rgb_points.extend(
568-
[float(v), float(rgb[0]), float(rgb[1]), float(rgb[2])]
569-
)
568+
r, g, b = float(rgb[0]), float(rgb[1]), float(rgb[2])
569+
new_rgb_points.extend([float(v), r, g, b])
570+
# Display points: uniform linear positions with symlog colors
571+
display_rgb_points.extend([x_lookup, r, g, b])
572+
573+
# Regenerate colorbar image from display points so it matches the 3D
574+
self.lut.UseLogScale = 0
575+
self.lut.RGBPoints = display_rgb_points
576+
self.config.lut_img = lut_to_img(self.lut)
570577

571-
# Store on proxy for bookkeeping — the actual CTF used by the
578+
# Store rendering points on proxy — the actual CTF used by the
572579
# mapper is a standalone vtkColorTransferFunction built in
573580
# update_color_preset to avoid proxy client-side object issues.
574-
self.lut.UseLogScale = 0
575581
self.lut.RGBPoints = new_rgb_points
576582

577583
def _apply_discrete_symlog_to_lut(self, linthresh, linear_rgb_points, n_sub=1):
@@ -666,7 +672,8 @@ def symlog(v):
666672
else:
667673
self._discrete_tick_data = all_tick_data
668674

669-
# Build a temporary linear CTF from the saved linear RGB points
675+
# Build a continuous symlog CTF (same as _apply_symlog_to_lut) so
676+
# discrete bands sample colours that match the continuous rendering.
670677
from vtkmodules.vtkRenderingCore import vtkColorTransferFunction
671678

672679
linear_ctf = vtkColorTransferFunction()
@@ -678,12 +685,24 @@ def symlog(v):
678685
linear_rgb_points[i + 3],
679686
)
680687

688+
n_samples = 256
689+
s_vals = np.linspace(s_min, s_max, n_samples)
690+
symlog_ctf = vtkColorTransferFunction()
691+
rgb_tmp = [0.0, 0.0, 0.0]
692+
for s in s_vals:
693+
v = float(np.sign(s) * linthresh * (10.0 ** abs(s) - 1.0))
694+
v = max(x_min, min(x_max, v))
695+
t = (s - s_min) / s_range
696+
x_lookup = x_min + t * data_range
697+
linear_ctf.GetColor(x_lookup, rgb_tmp)
698+
symlog_ctf.AddRGBPoint(v, rgb_tmp[0], rgb_tmp[1], rgb_tmp[2])
699+
681700
# For each decade interval, split into n_sub equal sub-bands in
682701
# symlog space. Each sub-band gets a flat color sampled from the
683-
# continuous LUT at the sub-band midpoint.
702+
# continuous symlog LUT at the sub-band midpoint.
684703
rgb = [0.0, 0.0, 0.0]
685704
eps_data = (x_max - x_min) * 1e-9
686-
eps_lin = 1e-9
705+
eps_lin = data_range * 1e-9
687706
display_rgb_points = []
688707
render_rgb_points = []
689708
band_idx = 0
@@ -696,9 +715,11 @@ def symlog(v):
696715
s_lo = s_lo_decade + (s_hi_decade - s_lo_decade) * j / n_sub
697716
s_hi = s_lo_decade + (s_hi_decade - s_lo_decade) * (j + 1) / n_sub
698717
s_mid = (s_lo + s_hi) / 2.0
699-
t_mid = (s_mid - s_min) / s_range
700-
x_lookup = x_min + t_mid * data_range
701-
linear_ctf.GetColor(x_lookup, rgb)
718+
719+
# Invert symlog to get data-space values
720+
v_mid = float(np.sign(s_mid) * linthresh * (10.0 ** abs(s_mid) - 1.0))
721+
v_mid = max(x_min, min(x_max, v_mid))
722+
symlog_ctf.GetColor(v_mid, rgb)
702723
r, g, b = float(rgb[0]), float(rgb[1]), float(rgb[2])
703724

704725
# Invert symlog to get data-space boundaries for rendering

0 commit comments

Comments
 (0)