From 6981407bcd97672b42818905096fdeaff60b1b1a Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Fri, 1 Aug 2025 15:42:02 -0700 Subject: [PATCH 1/2] Adding various fixes -- scalar bar image efficiency and revert color changes -- scalar bar probe location -- Removing ParaView text labels in favor of Trame -- Code cleanup --- quickview/interface.py | 106 +++++++++++++++++++++++++++- quickview/ui/slice_selection.py | 8 +-- quickview/view_manager.py | 119 ++++++++++---------------------- 3 files changed, 144 insertions(+), 89 deletions(-) diff --git a/quickview/interface.py b/quickview/interface.py index fa8f5d3..2c3b9a1 100644 --- a/quickview/interface.py +++ b/quickview/interface.py @@ -77,6 +77,7 @@ "varmin", "varmax", "override_range", # Track manual color range override per variable + "varaverage", # Track computed average per variable # Color options from toolbar "use_cvd_colors", "use_standard_colors", @@ -186,6 +187,10 @@ def __init__( state.varmax = [] state.override_range = [] state.colorbar_images = [] + state.varaverage = [] + + state.probe_enabled = False + state.probe_location = [] # Default probe ctrl.view_update = self.viewmanager.render_all_views ctrl.view_reset_camera = self.viewmanager.reset_camera @@ -399,6 +404,7 @@ def load_variables(self, use_cached_layout=False): state.varmax = [np.nan] * len(vars) state.override_range = [False] * len(vars) state.colorbar_images = [""] * len(vars) # Initialize empty images + state.varaverage = [np.nan] * len(vars) # Only use cached layout when explicitly requested (i.e., when loading state) layout_to_use = self._cached_layout if use_cached_layout else None @@ -717,6 +723,54 @@ def ui(self) -> SinglePageWithDrawerLayout: html.Div( style="position:absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1;" ) + # Top-left info: time and level info + with html.Div( + style="position: absolute; top: 8px; left: 8px; padding: 4px 8px; background-color: rgba(0, 0, 0, 0.7); color: white; font-size: 0.875rem; border-radius: 4px; z-index: 2;", + classes="drag-ignore font-monospace", + ): + # Show time + html.Div( + "t = {{ tstamp }}", + style="color: white;", + classes="font-weight-medium", + ) + # Show level for midpoint variables + html.Div( + v_if="midpoint_vars.includes(variables[idx])", + children="k = {{ midpoint }}", + style="color: white;", + classes="font-weight-medium", + ) + # Show level for interface variables + html.Div( + v_if="interface_vars.includes(variables[idx])", + children="k = {{ interface }}", + style="color: white;", + classes="font-weight-medium", + ) + # Top-right info: variable name and average + with html.Div( + style="position: absolute; top: 8px; right: 8px; padding: 4px 8px; background-color: rgba(0, 0, 0, 0.7); color: white; font-size: 0.875rem; border-radius: 4px; z-index: 2; text-align: right;", + classes="drag-ignore font-monospace", + ): + # Variable name + html.Div( + "{{ variables[idx] }}", + style="color: white;", + classes="font-weight-medium", + ) + # Average value + html.Div( + ( + "(avg: {{ " + "varaverage[idx] !== null && !isNaN(varaverage[idx]) ? " + "varaverage[idx].toExponential(2) : " + "'N/A' " + "}})" + ), + style="color: white;", + classes="font-weight-medium", + ) # Colorbar container (horizontal layout at bottom) with html.Div( style="position: absolute; bottom: 8px; left: 8px; right: 8px; display: flex; align-items: center; justify-content: center; padding: 4px 8px 4px 8px; background-color: rgba(255, 255, 255, 0.1); height: 28px; z-index: 3; overflow: visible; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);", @@ -733,13 +787,21 @@ def ui(self) -> SinglePageWithDrawerLayout: ) # Color min value html.Span( - "{{ varmin[idx] !== null && !isNaN(varmin[idx]) ? (uselogscale[idx] && varmin[idx] > 0 ? 'log₁₀(' + Math.log10(varmin[idx]).toFixed(3) + ')' : varmin[idx].toFixed(3)) : 'Auto' }}", + ( + "{{ " + "varmin[idx] !== null && !isNaN(varmin[idx]) ? (" + "uselogscale[idx] && varmin[idx] > 0 ? " + "'10^(' + Math.log10(varmin[idx]).toFixed(2) + ')' : " + "varmin[idx].toExponential(3)" + ") : 'Auto' " + "}}" + ), style="color: white;", classes="font-weight-medium", ) # Colorbar with html.Div( - style="flex: 1; display: flex; align-items: center; margin: 0 8px; height: 0.6rem;", + style="flex: 1; display: flex; align-items: center; margin: 0 8px; height: 0.6rem; position: relative;", classes="drag-ignore", ): # Colorbar image @@ -750,10 +812,48 @@ def ui(self) -> SinglePageWithDrawerLayout: ), style="height: 100%; width: 100%; object-fit: fill;", classes="rounded-lg border-thin", + v_on=( + "{" + "mousemove: (e) => { " + "const rect = e.target.getBoundingClientRect(); " + "const x = e.clientX - rect.left; " + "const width = rect.width; " + "const fraction = Math.max(0, Math.min(1, x / width)); " + "probe_location = [x, width, fraction, idx]; " + "}, " + "mouseenter: () => { probe_enabled = true; }, " + "mouseleave: () => { probe_enabled = false; probe_location = null; } " + "}" + ), + ) + # Probe tooltip (pan3d style - as sibling to colorbar) + html.Div( + v_if="probe_enabled && probe_location && probe_location[3] === idx", + v_bind_style="{position: 'absolute', bottom: '100%', left: probe_location[0] + 'px', transform: 'translateX(-50%)', marginBottom: '0.25rem', backgroundColor: '#000000', color: '#ffffff', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.875rem', whiteSpace: 'nowrap', pointerEvents: 'none', zIndex: 1000, fontFamily: 'monospace', boxShadow: '0 2px 4px rgba(0,0,0,0.3)'}", + children=( + "{{ " + "probe_location && varmin[idx] !== null && varmax[idx] !== null ? (" + "uselogscale[idx] && varmin[idx] > 0 && varmax[idx] > 0 ? " + "'10^(' + (" + "Math.log10(varmin[idx]) + " + "(Math.log10(varmax[idx]) - Math.log10(varmin[idx])) * probe_location[2]" + ").toFixed(2) + ')' : " + "(varmin[idx] + (varmax[idx] - varmin[idx]) * probe_location[2]).toExponential(3)" + ") : '' " + "}}" + ), ) # Color max value html.Span( - "{{ varmax[idx] !== null && !isNaN(varmax[idx]) ? (uselogscale[idx] && varmax[idx] > 0 ? 'log₁₀(' + Math.log10(varmax[idx]).toFixed(3) + ')' : varmax[idx].toFixed(3)) : 'Auto' }}", + ( + "{{ " + "varmax[idx] !== null && !isNaN(varmax[idx]) ? (" + "uselogscale[idx] && varmax[idx] > 0 ? " + "'10^(' + Math.log10(varmax[idx]).toFixed(2) + ')' : " + "varmax[idx].toExponential(3)" + ") : 'Auto' " + "}}" + ), style="color: white;", classes="font-weight-medium", ) diff --git a/quickview/ui/slice_selection.py b/quickview/ui/slice_selection.py index 6dd0eb4..4868653 100644 --- a/quickview/ui/slice_selection.py +++ b/quickview/ui/slice_selection.py @@ -67,7 +67,7 @@ def __init__(self, source: EAMVisSource, view_manager: ViewManager): "{{midpoints.length > 0 ? parseFloat(midpoints[midpoint]).toFixed(2) + ' hPa (k=' + midpoint + ')' : '0.00 hPa (k=0)'}}", classes="font-weight-medium", ) - v2.VDivider(classes="my-2") + # v2.VDivider(classes="my-2") with v2.VRow( classes="text-center align-center justify-center text-subtitle-1 pt-3 px-3" @@ -116,7 +116,7 @@ def __init__(self, source: EAMVisSource, view_manager: ViewManager): "{{interfaces.length > 0 ? parseFloat(interfaces[interface]).toFixed(2) + ' hPa (k=' + interface + ')' : '0.00 hPa (k=0)'}}", classes="font-weight-medium", ) - v2.VDivider(classes="my-2") + # v2.VDivider(classes="my-2") with v2.VRow( classes="text-center align-center justify-center text-subtitle-1 pt-3 px-3" @@ -165,7 +165,7 @@ def __init__(self, source: EAMVisSource, view_manager: ViewManager): "{{timesteps.length > 0 ? parseFloat(timesteps[tstamp]).toFixed(2) + ' (t=' + tstamp + ')' : '0.00 (t=0)'}}", classes="font-weight-medium", ) - v2.VDivider(classes="my-4") + # v2.VDivider(classes="my-4") with v2.VRow(classes="text-center align-center text-subtitle-1 pt-2 pa-2"): with v2.VCol(cols=3, classes="py-0"): @@ -196,7 +196,7 @@ def __init__(self, source: EAMVisSource, view_manager: ViewManager): variant="solo", classes="pt-2 px-6", ) - v2.VDivider(classes="my-4") + # v2.VDivider(classes="my-4") with v2.VRow(classes="text-center align-center text-subtitle-1 pt-2 pa-2"): with v2.VCol(cols=3, classes="py-0"): diff --git a/quickview/view_manager.py b/quickview/view_manager.py index 3adb952..7a6c7da 100644 --- a/quickview/view_manager.py +++ b/quickview/view_manager.py @@ -96,21 +96,15 @@ def __init__( class ViewState: - """Runtime state for a view - ParaView objects and computed values""" + """Runtime state for a view - ParaView objects""" def __init__( self, view_proxy=None, data_representation=None, - var_text_proxy=None, - var_info_proxy=None, - computed_average: float = None, ): self.view_proxy = view_proxy self.data_representation = data_representation - self.var_text_proxy = var_text_proxy - self.var_info_proxy = var_info_proxy - self.computed_average = computed_average class ViewContext: @@ -243,7 +237,9 @@ def update_views_for_timestep(self): for var, context in self.registry.items(): varavg = self.compute_average(var, vtkdata=data) - context.state.computed_average = varavg + # Directly set average in trame state + self.state.varaverage[context.index] = varavg + self.state.dirty("varaverage") if not context.config.override_range: context.state.data_representation.RescaleTransferFunctionToDataRange( False, True @@ -251,47 +247,19 @@ def update_views_for_timestep(self): range = self.compute_range(var=var) context.config.min_value = range[0] context.config.max_value = range[1] - (v_text, V_info) = self.get_var_info(var, context.state.computed_average) - if context.state.var_text_proxy is not None: - context.state.var_text_proxy.Text = v_text - if context.state.var_info_proxy is not None: - context.state.var_info_proxy.Text = V_info self.sync_color_config_to_state(context.index, context) self.generate_colorbar_image(context.index) - def refresh_view_display(self, var, context: ViewContext): + def refresh_view_display(self, context: ViewContext): if not context.config.override_range: context.state.data_representation.RescaleTransferFunctionToDataRange( False, True ) - (v_text, V_info) = self.get_var_info(var, context.state.computed_average) - if context.state.var_text_proxy is not None: - context.state.var_text_proxy.Text = v_text - if context.state.var_info_proxy is not None: - context.state.var_info_proxy.Text = V_info rview = context.state.view_proxy Render(rview) # ResetCamera(rview) - def get_var_info(self, var, average): - var_text = var + "\n(avg: " + "{:.2E}".format(average) + ")" - info_text = None - surface_vars = self.source.vars.get("surface", []) - t = self.state.tstamp - if surface_vars and var in surface_vars: - info_text = f"t = {t}" - midpoint_vars = self.source.vars.get("midpoint", []) - if midpoint_vars and var in midpoint_vars: - k = self.state.midpoint - info_text = f"k = {k}\nt = {t}" - interface_vars = self.source.vars.get("interface", []) - if interface_vars and var in interface_vars: - k = self.state.interface - info_text = f"k = {k}\nt = {t}" - - return (var_text, info_text) - def configure_new_view(self, var, context: ViewContext, sources): rview = context.state.view_proxy @@ -304,27 +272,25 @@ def configure_new_view(self, var, context: ViewContext, sources): coltrfunc.ApplyPreset(context.config.colormap, True) coltrfunc.NanOpacity = 0.0 + # Apply log scale if configured + if context.config.use_log_scale: + coltrfunc.MapControlPointsToLogSpace() + coltrfunc.UseLogScale = 1 + + # Apply inversion if configured + if context.config.invert_colors: + coltrfunc.InvertTransferFunction() + # Ensure the color transfer function is scaled to the data range if not context.config.override_range: rep.RescaleTransferFunctionToDataRange(False, True) + else: + coltrfunc.RescaleTransferFunction( + context.config.min_value, context.config.max_value + ) # ParaView scalar bar is always hidden - using custom HTML colorbar instead - (v_text, V_info) = self.get_var_info(var, context.state.computed_average) - text = Text(registrationName=f"Text{var}") - text.Text = v_text - context.state.var_text_proxy = text - textrep = Show(text, rview, "TextSourceRepresentation") - textrep.WindowLocation = "Upper Right Corner" - textrep.FontFamily = "Times" - - info = Text(registrationName=f"Info{var}") - info.Text = V_info - context.state.var_info_proxy = info - textrep = Show(info, rview, "TextSourceRepresentation") - textrep.WindowLocation = "Upper Left Corner" - textrep.FontFamily = "Times" - # Update common sources to all render views globe = sources["continents"] @@ -357,15 +323,21 @@ def sync_color_config_to_state(self, index, context: ViewContext): self.state.varmax[index] = context.config.max_value self.state.uselogscale[index] = context.config.use_log_scale self.state.override_range[index] = context.config.override_range + self.state.invert[index] = context.config.invert_colors # Mark arrays as dirty to ensure UI updates self.state.dirty("varcolor") self.state.dirty("varmin") self.state.dirty("varmax") self.state.dirty("uselogscale") self.state.dirty("override_range") + self.state.dirty("invert") def generate_colorbar_image(self, index): - """Generate colorbar image for a variable at given index""" + """Generate colorbar image for a variable at given index. + + This is a read-only operation that captures the current state of the + color transfer function without modifying it. + """ if index >= len(self.state.variables): return @@ -374,42 +346,24 @@ def generate_colorbar_image(self, index): if context is None: return - # Get the ParaView color transfer function + # Get the current ParaView color transfer function - already in correct state coltrfunc = GetColorTransferFunction(var) - # Store current state - current_use_log = coltrfunc.UseLogScale - - # Reset to linear scale and original range for image generation - if current_use_log: - coltrfunc.MapControlPointsToLinearSpace() - coltrfunc.UseLogScale = 0 - - # Apply the colormap preset to get clean colors - coltrfunc.ApplyPreset(context.config.colormap, True) - - # Generate the colorbar image with only inversion applied + # Generate the colorbar image from current state try: + # Note: We pass log_scale=False because the colorbar image should + # always show linear color gradients. The log scale affects data mapping, + # not the color gradient display. image_data = build_colorbar_image( coltrfunc, - log_scale=False, # Always False for image generation - invert=context.config.invert_colors, + log_scale=False, + invert=False, # Inversion is already applied to coltrfunc ) - # Update state with the new image without context manager to avoid recursive flush + # Update state with the new image self.state.colorbar_images[index] = image_data self.state.dirty("colorbar_images") except Exception as e: print(f"Error generating colorbar image for {var}: {e}") - finally: - # Restore the log scale state if it was enabled - if current_use_log: - coltrfunc.MapControlPointsToLogSpace() - coltrfunc.UseLogScale = 1 - # Restore the range if it was modified - if context.config.override_range: - coltrfunc.RescaleTransferFunction( - context.config.min_value, context.config.max_value - ) def reset_camera(self, **kwargs): for widget in self.widgets: @@ -546,7 +500,6 @@ def rebuild_visualization_layout(self, cached_layout=None): context: ViewContext = self.registry.get_view(var) if context is not None: view = context.state.view_proxy - context.state.computed_average = varavg if view is None: view = CreateRenderView() view.UseColorPaletteForBackground = 0 @@ -557,7 +510,7 @@ def rebuild_visualization_layout(self, cached_layout=None): context.config.max_value = varrange[1] self.configure_new_view(var, context, self.source.views) else: - self.refresh_view_display(var, context) + self.refresh_view_display(context) else: view = CreateRenderView() # Preserve override flag if context already exists @@ -585,7 +538,6 @@ def rebuild_visualization_layout(self, cached_layout=None): ) view_state = ViewState( view_proxy=view, - computed_average=varavg, ) context = ViewContext(config, view_state, index) view.UseColorPaletteForBackground = 0 @@ -593,6 +545,9 @@ def rebuild_visualization_layout(self, cached_layout=None): self.registry.register_view(var, context) self.configure_new_view(var, context, self.source.views) context.index = index + # Set the computed average directly in trame state + self.state.varaverage[index] = varavg + self.state.dirty("varaverage") self.sync_color_config_to_state(index, context) self.generate_colorbar_image(index) From 14b6a0096231017b808d8326e90269add5be04eb Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Fri, 1 Aug 2025 17:12:13 -0700 Subject: [PATCH 2/2] =?UTF-8?q?Bump=20version:=200.1.6=20=E2=86=92=200.1.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pyproject.toml | 2 +- quickview/__init__.py | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a4a1f9d..65b3f7d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.6 +current_version = 0.1.7 commit = True tag = True diff --git a/pyproject.toml b/pyproject.toml index 314021d..50dc96f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "quickview" -version = "0.1.6" +version = "0.1.7" description = "An application to explore/analyze data for atmosphere component for E3SM" authors = [ {name = "Kitware Inc."}, diff --git a/quickview/__init__.py b/quickview/__init__.py index 60295e5..4bde9de 100644 --- a/quickview/__init__.py +++ b/quickview/__init__.py @@ -1,5 +1,5 @@ """QuickView: Visual Analysis for E3SM Atmosphere Data.""" -__version__ = "0.1.6" +__version__ = "0.1.7" __author__ = "Kitware Inc." __license__ = "Apache-2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6e59e88..72f5d45 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.6" +version = "0.1.7" description = "QuickView: Visual Analyis for E3SM Atmosphere Data" authors = ["Kitware"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 434d51e..10af770 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "QuickView", - "version": "0.1.6" + "version": "0.1.7" }, "tauri": { "allowlist": {