Skip to content

Commit d2a4449

Browse files
committed
Add symlog scale toggle with 3-state cycle and update colorbar formatting
1 parent 323acd9 commit d2a4449

4 files changed

Lines changed: 224 additions & 26 deletions

File tree

src/e3sm_quickview/components/view.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,15 @@ def create_bottom_bar(config, update_color_preset):
154154
)
155155
v3.VIconBtn(
156156
raw_attrs=[
157-
'''v-tooltip:bottom="config.use_log_scale ? 'Toggle to linear scale' : 'Toggle to log scale'"'''
157+
'''v-tooltip:bottom="config.use_log_scale === 'linear' ? 'Toggle to log scale' : config.use_log_scale === 'log' ? 'Toggle to symlog scale' : 'Toggle to linear scale'"'''
158158
],
159159
icon=(
160-
"config.use_log_scale ? 'mdi-math-log' : 'mdi-stairs'",
160+
"config.use_log_scale === 'log' ? 'mdi-math-log' : config.use_log_scale === 'symlog' ? 'mdi-chart-bell-curve-cumulative' : 'mdi-stairs'",
161161
),
162-
click="config.use_log_scale = !config.use_log_scale",
162+
click="config.use_log_scale = config.use_log_scale === 'linear' ? 'log' : config.use_log_scale === 'log' ? 'symlog' : 'linear'",
163163
size="small",
164164
text=(
165-
"config.use_log_scale ? 'Log scale' : 'Linear scale'",
165+
"config.use_log_scale === 'log' ? 'Log' : config.use_log_scale === 'symlog' ? 'SymLog' : 'Linear'",
166166
),
167167
variant="text",
168168
)
@@ -257,7 +257,7 @@ def create_bottom_bar(config, update_color_preset):
257257
classes="rounded",
258258
)
259259
html.Div(
260-
"{{ utils.quickview.formatRange(config.color_range?.[0], config.use_log_scale) }}",
260+
"{{ utils.quickview.formatRange(config.color_range?.[0], config.use_log_scale, config.color_range?.[0], config.color_range?.[1]) }}",
261261
classes="text-caption px-2 text-no-wrap",
262262
)
263263
with html.Div(classes="overflow-hidden rounded w-100", style="height:70%;"):
@@ -267,6 +267,6 @@ def create_bottom_bar(config, update_color_preset):
267267
draggable=False,
268268
)
269269
html.Div(
270-
"{{ utils.quickview.formatRange(config.color_range?.[1], config.use_log_scale) }}",
270+
"{{ utils.quickview.formatRange(config.color_range?.[1], config.use_log_scale, config.color_range?.[0], config.color_range?.[1]) }}",
271271
classes="text-caption px-2 text-no-wrap",
272272
)

src/e3sm_quickview/module/serve/utils.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
window.trame.utils.quickview = {
2-
formatRange(value, useLog) {
2+
formatRange(value, useLog, rangeMin, rangeMax) {
33
if (value === null || value === undefined || isNaN(value)) {
44
return "Auto";
55
}
6-
if (useLog && value > 0) {
6+
if (useLog === "log" && value > 0) {
77
return `10^(${Math.log10(value).toFixed(1)})`;
88
}
9+
if (useLog === "symlog") {
10+
if (value === 0) return "0";
11+
const linthresh =
12+
Math.max(Math.abs(rangeMin), Math.abs(rangeMax)) * 1e-2 || 1.0;
13+
const absVal = Math.abs(value);
14+
if (absVal <= linthresh) {
15+
return value.toExponential(1);
16+
}
17+
const sign = value < 0 ? "-" : "";
18+
return `${sign}10^(${Math.log10(absVal).toFixed(1)})`;
19+
}
920
const nSignDigit = Math.log10(Math.abs(value));
1021
if (Math.abs(nSignDigit) < 6 || value === 0) {
1122
if (nSignDigit > 0 && nSignDigit < 3) {

src/e3sm_quickview/view_manager.py

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import math
22

3+
import numpy as np
4+
35
from paraview import simple
46
from trame.app import TrameComponent, dataclass
57
from trame.decorators import controller
@@ -50,7 +52,7 @@ class ViewConfiguration(dataclass.StateDataModel):
5052
preset: str = dataclass.Sync(str, "BuGnYl")
5153
invert: bool = dataclass.Sync(bool, False)
5254
color_blind: bool = dataclass.Sync(bool, False)
53-
use_log_scale: bool = dataclass.Sync(bool, False)
55+
use_log_scale: str = dataclass.Sync(str, "linear")
5456
color_value_min: str = dataclass.Sync(str, "0")
5557
color_value_max: str = dataclass.Sync(str, "1")
5658
color_value_min_valid: bool = dataclass.Sync(bool, True)
@@ -152,14 +154,13 @@ def reset_camera(self):
152154

153155
def update_color_preset(self, name, invert, log_scale, n_colors=255):
154156
self.config.preset = name
155-
self.lut.UseLogScale = 0
156-
self.lut.ApplyPreset(self.config.preset, True)
157-
if invert:
158-
self.lut.InvertTransferFunction()
159157

160-
if log_scale:
161-
self.lut.MapControlPointsToLogSpace()
162-
self.lut.UseLogScale = 1
158+
if log_scale == "log":
159+
self._apply_log_to_lut(invert)
160+
elif log_scale == "symlog":
161+
self._apply_symlog_to_lut(invert)
162+
else:
163+
self._apply_linear_to_lut(invert)
163164

164165
if n_colors is not None:
165166
self.lut.NumberOfTableValues = n_colors
@@ -168,6 +169,93 @@ def update_color_preset(self, name, invert, log_scale, n_colors=255):
168169

169170
self.render()
170171

172+
def _apply_linear_to_lut(self, invert=False):
173+
"""Apply preset with linear scale."""
174+
self.lut.UseLogScale = 0
175+
self.lut.ApplyPreset(self.config.preset, True)
176+
if invert:
177+
self.lut.InvertTransferFunction()
178+
179+
def _apply_log_to_lut(self, invert=False):
180+
"""Apply preset with log scale."""
181+
self.lut.UseLogScale = 0
182+
self.lut.ApplyPreset(self.config.preset, True)
183+
if invert:
184+
self.lut.InvertTransferFunction()
185+
self.lut.MapControlPointsToLogSpace()
186+
self.lut.UseLogScale = 1
187+
188+
def _apply_symlog_to_lut(
189+
self, invert=False, linthresh=None, linscale=1.0, base=10, n_samples=256
190+
):
191+
"""Apply preset with symmetric log scale.
192+
193+
Uses:
194+
- Linear for |x| <= linthresh
195+
- Logarithmic for |x| > linthresh
196+
with continuity at the boundary.
197+
198+
Samples colors from the linear preset and redistributes them
199+
across the data range using symlog spacing.
200+
"""
201+
self.lut.UseLogScale = 0
202+
self.lut.ApplyPreset(self.config.preset, True)
203+
if invert:
204+
self.lut.InvertTransferFunction()
205+
206+
# Get the current data range from the LUT
207+
ctf = self.lut.GetClientSideObject()
208+
x_min, x_max = ctf.GetRange()
209+
data_range = x_max - x_min
210+
if data_range == 0:
211+
return
212+
213+
if linthresh is None:
214+
linthresh = max(abs(x_min), abs(x_max)) * 1e-2
215+
if linthresh == 0:
216+
linthresh = 1.0
217+
218+
log_base = np.log(base)
219+
linscale_adj = linscale / (1.0 - base**-1)
220+
221+
def symlog(x):
222+
abs_x = np.abs(x)
223+
out = np.where(
224+
abs_x <= linthresh,
225+
x * linscale_adj,
226+
np.sign(x)
227+
* linthresh
228+
* (linscale_adj + np.log(abs_x / linthresh) / log_base),
229+
)
230+
return out
231+
232+
# Sample colors from the linear LUT at uniform positions
233+
rgb = [0.0, 0.0, 0.0]
234+
s_min = symlog(x_min)
235+
s_max = symlog(x_max)
236+
s_range = s_max - s_min
237+
if s_range == 0:
238+
return
239+
240+
new_rgb_points = []
241+
for i in range(n_samples):
242+
# Uniform position in data space
243+
t = i / (n_samples - 1)
244+
x_data = x_min + t * data_range
245+
246+
# Map x_data through symlog, normalize to [0,1], then look up
247+
# the color at the corresponding linear position
248+
s_val = symlog(x_data)
249+
s_t = (s_val - s_min) / s_range
250+
x_lookup = x_min + s_t * data_range
251+
ctf.GetColor(x_lookup, rgb)
252+
new_rgb_points.extend(
253+
[float(x_data), float(rgb[0]), float(rgb[1]), float(rgb[2])]
254+
)
255+
256+
# Write back through the proxy so state stays in sync
257+
self.lut.RGBPoints = new_rgb_points
258+
171259
def color_range_str_to_float(self, color_value_min, color_value_max):
172260
try:
173261
min_value = float(color_value_min)
@@ -215,7 +303,13 @@ def update_color_range(self, *_):
215303
self.config.color_value_min_valid = True
216304
self.config.color_value_max_valid = True
217305
self.lut.RescaleTransferFunction(*data_range)
218-
self.render()
306+
307+
self.update_color_preset(
308+
self.config.preset,
309+
self.config.invert,
310+
self.config.use_log_scale,
311+
self.config.n_colors,
312+
)
219313

220314
def _build_ui(self):
221315
with DivLayout(

src/e3sm_quickview/view_manager2.py

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
import math
33

4+
import numpy as np
5+
46
# Rendering Factory
57
import vtkmodules.vtkRenderingOpenGL2 # noqa: F401
68
from paraview import simple
@@ -65,7 +67,7 @@ class ViewConfiguration(dataclass.StateDataModel):
6567
preset: str = dataclass.Sync(str, "BuGnYl")
6668
invert: bool = dataclass.Sync(bool, False)
6769
color_blind: bool = dataclass.Sync(bool, False)
68-
use_log_scale: bool = dataclass.Sync(bool, False)
70+
use_log_scale: str = dataclass.Sync(str, "linear")
6971
color_value_min: str = dataclass.Sync(str, "0")
7072
color_value_max: str = dataclass.Sync(str, "1")
7173
color_value_min_valid: bool = dataclass.Sync(bool, True)
@@ -169,21 +171,107 @@ def render(self):
169171

170172
def update_color_preset(self, name, invert, log_scale, n_colors=255):
171173
self.config.preset = name
172-
self.lut.UseLogScale = 0
173-
self.lut.ApplyPreset(self.config.preset, True)
174-
if invert:
175-
self.lut.InvertTransferFunction()
176174

177-
if log_scale:
178-
self.lut.MapControlPointsToLogSpace()
179-
self.lut.UseLogScale = 1
175+
if log_scale == "log":
176+
self._apply_log_to_lut(invert)
177+
elif log_scale == "symlog":
178+
self._apply_symlog_to_lut(invert)
179+
else:
180+
self._apply_linear_to_lut(invert)
180181

181182
if n_colors is not None:
182183
self.lut.NumberOfTableValues = n_colors
183184

184185
self.config.lut_img = lut_to_img(self.lut)
185186
self.render()
186187

188+
def _apply_linear_to_lut(self, invert=False):
189+
"""Apply preset with linear scale."""
190+
self.lut.UseLogScale = 0
191+
self.lut.ApplyPreset(self.config.preset, True)
192+
if invert:
193+
self.lut.InvertTransferFunction()
194+
195+
def _apply_log_to_lut(self, invert=False):
196+
"""Apply preset with log scale."""
197+
self.lut.UseLogScale = 0
198+
self.lut.ApplyPreset(self.config.preset, True)
199+
if invert:
200+
self.lut.InvertTransferFunction()
201+
self.lut.MapControlPointsToLogSpace()
202+
self.lut.UseLogScale = 1
203+
204+
def _apply_symlog_to_lut(
205+
self, invert=False, linthresh=None, linscale=1.0, base=10, n_samples=256
206+
):
207+
"""Apply preset with symmetric log scale.
208+
209+
Uses:
210+
- Linear for |x| <= linthresh
211+
- Logarithmic for |x| > linthresh
212+
with continuity at the boundary.
213+
214+
Samples colors from the linear preset and redistributes them
215+
across the data range using symlog spacing.
216+
"""
217+
self.lut.UseLogScale = 0
218+
self.lut.ApplyPreset(self.config.preset, True)
219+
if invert:
220+
self.lut.InvertTransferFunction()
221+
222+
# Get the current data range from the LUT
223+
ctf = self.lut.GetClientSideObject()
224+
x_min, x_max = ctf.GetRange()
225+
data_range = x_max - x_min
226+
if data_range == 0:
227+
return
228+
229+
if linthresh is None:
230+
linthresh = max(abs(x_min), abs(x_max)) * 1e-2
231+
if linthresh == 0:
232+
linthresh = 1.0
233+
234+
log_base = np.log(base)
235+
linscale_adj = linscale / (1.0 - base**-1)
236+
237+
def symlog(x):
238+
abs_x = np.abs(x)
239+
out = np.where(
240+
abs_x <= linthresh,
241+
x * linscale_adj,
242+
np.sign(x)
243+
* linthresh
244+
* (linscale_adj + np.log(abs_x / linthresh) / log_base),
245+
)
246+
return out
247+
248+
# Sample colors from the linear LUT at uniform positions
249+
rgb = [0.0, 0.0, 0.0]
250+
s_min = symlog(x_min)
251+
s_max = symlog(x_max)
252+
s_range = s_max - s_min
253+
if s_range == 0:
254+
return
255+
256+
new_rgb_points = []
257+
for i in range(n_samples):
258+
# Uniform position in data space
259+
t = i / (n_samples - 1)
260+
x_data = x_min + t * data_range
261+
262+
# Map x_data through symlog, normalize to [0,1], then look up
263+
# the color at the corresponding linear position
264+
s_val = symlog(x_data)
265+
s_t = (s_val - s_min) / s_range
266+
x_lookup = x_min + s_t * data_range
267+
ctf.GetColor(x_lookup, rgb)
268+
new_rgb_points.extend(
269+
[float(x_data), float(rgb[0]), float(rgb[1]), float(rgb[2])]
270+
)
271+
272+
# Write back through the proxy so state stays in sync
273+
self.lut.RGBPoints = new_rgb_points
274+
187275
def color_range_str_to_float(self, color_value_min, color_value_max):
188276
try:
189277
min_value = float(color_value_min)
@@ -232,7 +320,12 @@ def update_color_range(self, *_):
232320
self.config.color_value_max_valid = True
233321
self.lut.RescaleTransferFunction(*data_range)
234322

235-
self.render()
323+
self.update_color_preset(
324+
self.config.preset,
325+
self.config.invert,
326+
self.config.use_log_scale,
327+
self.config.n_colors,
328+
)
236329

237330
def _build_ui(self):
238331
with DivLayout(

0 commit comments

Comments
 (0)