From 21787d86db02f115716a70ee1fd03ca41a585625 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Mon, 28 Jul 2025 10:54:21 -0700 Subject: [PATCH 1/5] =?UTF-8?q?Bump=20version:=200.1.3=20=E2=86=92=200.1.4?= 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 9d3d2f0..d66c946 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.3 +current_version = 0.1.4 commit = True tag = True diff --git a/pyproject.toml b/pyproject.toml index aaa96f1..5c7c3a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "quickview" -version = "0.1.3" +version = "0.1.4" 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 7b6d16e..7610272 100644 --- a/quickview/__init__.py +++ b/quickview/__init__.py @@ -1,5 +1,5 @@ """QuickView: Visual Analysis for E3SM Atmosphere Data.""" -__version__ = "0.1.3" +__version__ = "0.1.4" __author__ = "Kitware Inc." __license__ = "Apache-2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 191801b..56fab31 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.3" +version = "0.1.4" 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 7348f2b..1893ef4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "QuickView", - "version": "0.1.3" + "version": "0.1.4" }, "tauri": { "allowlist": { From e27eeead2083e189e437002b9242109079063914 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Mon, 21 Jul 2025 09:20:44 -0700 Subject: [PATCH 2/5] Adding scalar bar prototype --- quickview/ui/scalar_bar.py | 257 +++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 quickview/ui/scalar_bar.py diff --git a/quickview/ui/scalar_bar.py b/quickview/ui/scalar_bar.py new file mode 100644 index 0000000..4bf312a --- /dev/null +++ b/quickview/ui/scalar_bar.py @@ -0,0 +1,257 @@ +from trame.widgets import html +from trame.widgets import vuetify2 as v2 +import base64 + +from vtkmodules.vtkCommonCore import vtkUnsignedCharArray +from vtkmodules.vtkCommonDataModel import vtkImageData +from vtkmodules.vtkIOImage import vtkPNGWriter +from vtkmodules.vtkCommonCore import vtkLookupTable + + +def paraview_to_vtk_lut(paraview_lut, num_colors=256): + """ + Convert a ParaView color transfer function to a VTK lookup table. + + Parameters: + ----------- + paraview_lut : paraview.servermanager.PVLookupTable + The ParaView color transfer function from GetColorTransferFunction() + num_colors : int, optional + Number of colors in the VTK lookup table (default: 256) + + Returns: + -------- + vtk.vtkLookupTable + A VTK lookup table with interpolated colors from the ParaView LUT + """ + import vtk + import numpy as np + + # Get RGB points from ParaView LUT + rgb_points = paraview_lut.RGBPoints + + if len(rgb_points) < 8: + raise ValueError("ParaView LUT must have at least 2 color points") + + # Create VTK lookup table + vtk_lut = vtk.vtkLookupTable() + + # Extract scalars and colors from the flat RGB points array + scalars = np.array([rgb_points[i] for i in range(0, len(rgb_points), 4)]) + colors = np.array( + [ + [rgb_points[i + 1], rgb_points[i + 2], rgb_points[i + 3]] + for i in range(0, len(rgb_points), 4) + ] + ) + + # Get range + min_val = scalars[0] + max_val = scalars[-1] + + # Generate all scalar values for the lookup table + table_scalars = np.linspace(min_val, max_val, num_colors) + + # Vectorized interpolation for all colors at once + r_values = np.interp(table_scalars, scalars, colors[:, 0]) + g_values = np.interp(table_scalars, scalars, colors[:, 1]) + b_values = np.interp(table_scalars, scalars, colors[:, 2]) + + # Set up the VTK lookup table + vtk_lut.SetRange(min_val, max_val) + vtk_lut.SetNumberOfTableValues(num_colors) + vtk_lut.Build() + + # Set all colors at once + for i in range(num_colors): + vtk_lut.SetTableValue(i, r_values[i], g_values[i], b_values[i], 1.0) + + return vtk_lut + + +def to_image(lut, samples=255): + colorArray = vtkUnsignedCharArray() + colorArray.SetNumberOfComponents(3) + colorArray.SetNumberOfTuples(samples) + + dataRange = lut.GetRange() + delta = (dataRange[1] - dataRange[0]) / float(samples) + + # Add the color array to an image data + imgData = vtkImageData() + imgData.SetDimensions(samples, 1, 1) + imgData.GetPointData().SetScalars(colorArray) + + # Loop over all presets + rgb = [0, 0, 0] + for i in range(samples): + lut.GetColor(dataRange[0] + float(i) * delta, rgb) + r = int(round(rgb[0] * 255)) + g = int(round(rgb[1] * 255)) + b = int(round(rgb[2] * 255)) + colorArray.SetTuple3(i, r, g, b) + + writer = vtkPNGWriter() + writer.WriteToMemoryOn() + writer.SetInputData(imgData) + writer.SetCompressionLevel(6) + writer.Write() + + writer.GetResult() + + base64_img = base64.standard_b64encode(writer.GetResult()).decode("utf-8") + return f"data:image/png;base64,{base64_img}" + + +class ScalarBar(v2.VTooltip): + """ + Scalar bar for the XArray Explorers. + """ + + _next_id = 0 + + @classmethod + def next_id(cls): + """Get the next unique ID for the scalar bar.""" + cls._next_id += 1 + return f"pan3d_scalarbar{cls._next_id}" + + def __init__( + self, + preset="Fast", + color_min=0.0, + color_max=1.0, + ctx_name=None, + paraview_lut=None, + **kwargs, + ): + """Scalar bar for the XArray Explorers. + + Parameters: + ----------- + preset : str, optional + Color preset name (default: "Fast") + color_min : float, optional + Minimum color value (default: 0.0) + color_max : float, optional + Maximum color value (default: 1.0) + ctx_name : str, optional + Context name for trame + paraview_lut : paraview.servermanager.PVLookupTable, optional + ParaView color transfer function to initialize from + **kwargs : dict + Additional keyword arguments + """ + # Initialize VTK lookup table + if paraview_lut is not None: + self._lut = paraview_to_vtk_lut(paraview_lut) + # Get range from the converted LUT + lut_range = self._lut.GetRange() + color_min = lut_range[0] + color_max = lut_range[1] + else: + self._lut = vtkLookupTable() + + super().__init__(location="top", ctx_name=ctx_name) + + ns = self.next_id() + self.__preset_image = f"{ns}_preset" + self.__color_min = f"{ns}_color_min" + self.__color_max = f"{ns}_color_max" + # Probe enables mouse events for scalar bar + self.__probe_location = f"{ns}_probe_location" + self.__probe_enabled = f"{ns}_probe_enabled" + + # Initialize state + self.preset = preset + self.set_color_range(color_min, color_max) + self.state[self.__probe_location] = [] + self.state[self.__probe_enabled] = 0 + self.state.client_only( + self.__probe_location, + self.__probe_enabled, + ) + + # Generate initial scalar bar image + self._update_preset_image() + + with self: + # Content + with html.Template(v_slot_activator="{ props }"): + with html.Div( + classes="scalarbar", + rounded="pill", + v_bind="props", + **kwargs, + ): + html.Div( + f"{{{{{self.__color_min}.toFixed(6) }}}}", + classes="scalarbar-left", + ) + html.Img( + src=(self.__preset_image, None), + style="height: 100%; width: 100%;", + classes="rounded-lg border-thin", + mousemove=f"{self.__probe_location} = [$event.x, $event.target.getBoundingClientRect()]", + mouseenter=f"{self.__probe_enabled} = 1", + mouseleave=f"{self.__probe_enabled} = 0", + __events=["mousemove", "mouseenter", "mouseleave"], + ) + html.Div( + v_show=(self.__probe_enabled, False), + classes="scalar-cursor", + style=( + f"`left: ${{{self.__probe_location}?.[0] - {self.__probe_location}?.[1]?.left}}px`", + ), + ) + html.Div( + f"{{{{ {self.__color_max}.toFixed(6) }}}}", + classes="scalarbar-right", + ) + html.Span( + f"{{{{ (({self.__color_max} - {self.__color_min}) * ({self.__probe_location}?.[0] - {self.__probe_location}?.[1]?.left) / {self.__probe_location}?.[1]?.width + {self.__color_min}).toFixed(6) }}}}" + ) + + def set_color_range(self, color_min, color_max): + """Set the color range for the scalar bar.""" + self.state[self.__color_min] = color_min + self.state[self.__color_max] = color_max + + def update_from_paraview_lut(self, paraview_lut, num_colors=256): + """Update the internal VTK lookup table from a ParaView color transfer function. + + Parameters: + ----------- + paraview_lut : paraview.servermanager.PVLookupTable + ParaView color transfer function to update from + num_colors : int, optional + Number of colors in the VTK lookup table (default: 256) + """ + # Convert ParaView LUT to VTK LUT + self._lut = paraview_to_vtk_lut(paraview_lut, num_colors) + + # Update color range from the new LUT + lut_range = self._lut.GetRange() + self.set_color_range(lut_range[0], lut_range[1]) + + # Update the scalar bar image + self._update_preset_image() + + def _update_preset_image(self): + """Generate and update the scalar bar image from the current lookup table.""" + if self._lut is not None: + image_data = to_image(self._lut) + self.state[self.__preset_image] = image_data + + def update_preset(self, preset_name): + """Update the color preset and regenerate the scalar bar image. + + Parameters: + ----------- + preset_name : str + Name of the color preset to apply + """ + self.preset = preset_name + # If you have a method to apply presets to VTK LUT, call it here + # For now, just update the image with current LUT + self._update_preset_image() From 900ce4c9abecff825c6d14a13ec971def0452682 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Mon, 28 Jul 2025 13:50:54 -0700 Subject: [PATCH 3/5] feat: Replace ParaView scalar bar with custom HTML colorbar - Remove ParaView scalar bar functionality throughout the codebase - Always hide ParaView scalar bars via SetScalarBarVisibility(False) - Remove GetScalarBar imports and usage - Remove "Show color bar" toggle from toolbar UI - Implement custom HTML colorbar at bottom of each view - Display colorbar as horizontal bar with min/max values - Generate colorbar images from ParaView color transfer functions - Store images as base64-encoded PNGs in trame state - Fix colorbar image generation to preserve original colormap - Colorbar image unaffected by log scale or manual range settings - Only color inversion affects the colorbar image - Temporarily reset transformations during image generation --- quickview/interface.py | 39 +++++- quickview/ui/scalar_bar.py | 257 ------------------------------------- quickview/ui/toolbar.py | 29 ----- quickview/utilities.py | 139 ++++++++++++++++++++ quickview/view_manager.py | 83 +++++++++--- 5 files changed, 239 insertions(+), 308 deletions(-) delete mode 100644 quickview/ui/scalar_bar.py diff --git a/quickview/interface.py b/quickview/interface.py index 5c000f7..b22f1d7 100644 --- a/quickview/interface.py +++ b/quickview/interface.py @@ -83,7 +83,6 @@ # Color options from toolbar "use_cvd_colors", "use_standard_colors", - "show_color_bar", # Grid layout "layout", ] @@ -189,6 +188,7 @@ def __init__( state.varmin = [] state.varmax = [] state.override_range = [] + state.colorbar_images = [] ctrl.view_update = self.viewmanager.render_all_views ctrl.view_reset_camera = self.viewmanager.reset_camera @@ -401,6 +401,7 @@ def load_variables(self): state.varmin = [np.nan] * len(vars) state.varmax = [np.nan] * len(vars) state.override_range = [False] * len(vars) + state.colorbar_images = [""] * len(vars) # Initialize empty images self.viewmanager.rebuild_visualization_layout(self._cached_layout) # Update cached layout after rebuild @@ -606,7 +607,6 @@ def ui(self) -> SinglePageWithDrawerLayout: load_state=self.load_state, load_variables=self.load_variables, update_available_color_maps=self.update_available_color_maps, - update_scalar_bars=self.update_scalar_bars, generate_state=self.generate_state, ) @@ -695,9 +695,10 @@ def ui(self) -> SinglePageWithDrawerLayout: style="height: calc(100% - 0.66rem); position: relative;", classes="pa-0", ) as cardcontent: + # VTK View takes up most of the space cardcontent.add_child( """ - + """, ) @@ -719,12 +720,13 @@ def ui(self) -> SinglePageWithDrawerLayout: # height: $refs[vref].vtkContainer.getBoundingClientRect().height}] # ''') ) + # Mask to prevent VTK view from getting scroll/mouse events html.Div( - style="position:absolute; top: 0; left: 0; width: 100%; height: calc(100% - 0.66rem); z-index: 1;" + style="position:absolute; top: 0; left: 0; width: 100%; height: calc(100% - 30px); z-index: 1;" ) - # with v2.VCardActions(classes="pa-0"): + # View Properties with html.Div( - style="position:absolute; bottom: 1rem; left: 1rem; height: 2rem; z-index: 2;" + style="position:absolute; bottom: 40px; left: 1rem; height: 2rem; z-index: 2;" ): ViewProperties( apply=self.update_view_color_settings, @@ -732,4 +734,29 @@ def ui(self) -> SinglePageWithDrawerLayout: reset=self.revert_to_auto_color_range, ) + # Colorbar container (horizontal layout at bottom) + with html.Div( + style="position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; padding: 4px 12px; background-color: rgba(255, 255, 255, 0.9); height: 30px; z-index: 3;" + ): + # 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' }}", + style="font-size: 12px; color: #333; white-space: nowrap;", + ) + # Colorbar image + html.Img( + src=( + "colorbar_images[idx] || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='", + None, + ), + style="height: 0.75rem; flex: 1; min-width: 0; max-width: 100%; margin: 0 12px; object-fit: fill;", + classes="rounded-lg border-thin", + v_bind_style="{ background: colorbar_images[idx] ? 'none' : 'linear-gradient(to right, blue, cyan, green, yellow, red)' }", + ) + # 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' }}", + style="font-size: 12px; color: #333; white-space: nowrap;", + ) + return self._ui diff --git a/quickview/ui/scalar_bar.py b/quickview/ui/scalar_bar.py deleted file mode 100644 index 4bf312a..0000000 --- a/quickview/ui/scalar_bar.py +++ /dev/null @@ -1,257 +0,0 @@ -from trame.widgets import html -from trame.widgets import vuetify2 as v2 -import base64 - -from vtkmodules.vtkCommonCore import vtkUnsignedCharArray -from vtkmodules.vtkCommonDataModel import vtkImageData -from vtkmodules.vtkIOImage import vtkPNGWriter -from vtkmodules.vtkCommonCore import vtkLookupTable - - -def paraview_to_vtk_lut(paraview_lut, num_colors=256): - """ - Convert a ParaView color transfer function to a VTK lookup table. - - Parameters: - ----------- - paraview_lut : paraview.servermanager.PVLookupTable - The ParaView color transfer function from GetColorTransferFunction() - num_colors : int, optional - Number of colors in the VTK lookup table (default: 256) - - Returns: - -------- - vtk.vtkLookupTable - A VTK lookup table with interpolated colors from the ParaView LUT - """ - import vtk - import numpy as np - - # Get RGB points from ParaView LUT - rgb_points = paraview_lut.RGBPoints - - if len(rgb_points) < 8: - raise ValueError("ParaView LUT must have at least 2 color points") - - # Create VTK lookup table - vtk_lut = vtk.vtkLookupTable() - - # Extract scalars and colors from the flat RGB points array - scalars = np.array([rgb_points[i] for i in range(0, len(rgb_points), 4)]) - colors = np.array( - [ - [rgb_points[i + 1], rgb_points[i + 2], rgb_points[i + 3]] - for i in range(0, len(rgb_points), 4) - ] - ) - - # Get range - min_val = scalars[0] - max_val = scalars[-1] - - # Generate all scalar values for the lookup table - table_scalars = np.linspace(min_val, max_val, num_colors) - - # Vectorized interpolation for all colors at once - r_values = np.interp(table_scalars, scalars, colors[:, 0]) - g_values = np.interp(table_scalars, scalars, colors[:, 1]) - b_values = np.interp(table_scalars, scalars, colors[:, 2]) - - # Set up the VTK lookup table - vtk_lut.SetRange(min_val, max_val) - vtk_lut.SetNumberOfTableValues(num_colors) - vtk_lut.Build() - - # Set all colors at once - for i in range(num_colors): - vtk_lut.SetTableValue(i, r_values[i], g_values[i], b_values[i], 1.0) - - return vtk_lut - - -def to_image(lut, samples=255): - colorArray = vtkUnsignedCharArray() - colorArray.SetNumberOfComponents(3) - colorArray.SetNumberOfTuples(samples) - - dataRange = lut.GetRange() - delta = (dataRange[1] - dataRange[0]) / float(samples) - - # Add the color array to an image data - imgData = vtkImageData() - imgData.SetDimensions(samples, 1, 1) - imgData.GetPointData().SetScalars(colorArray) - - # Loop over all presets - rgb = [0, 0, 0] - for i in range(samples): - lut.GetColor(dataRange[0] + float(i) * delta, rgb) - r = int(round(rgb[0] * 255)) - g = int(round(rgb[1] * 255)) - b = int(round(rgb[2] * 255)) - colorArray.SetTuple3(i, r, g, b) - - writer = vtkPNGWriter() - writer.WriteToMemoryOn() - writer.SetInputData(imgData) - writer.SetCompressionLevel(6) - writer.Write() - - writer.GetResult() - - base64_img = base64.standard_b64encode(writer.GetResult()).decode("utf-8") - return f"data:image/png;base64,{base64_img}" - - -class ScalarBar(v2.VTooltip): - """ - Scalar bar for the XArray Explorers. - """ - - _next_id = 0 - - @classmethod - def next_id(cls): - """Get the next unique ID for the scalar bar.""" - cls._next_id += 1 - return f"pan3d_scalarbar{cls._next_id}" - - def __init__( - self, - preset="Fast", - color_min=0.0, - color_max=1.0, - ctx_name=None, - paraview_lut=None, - **kwargs, - ): - """Scalar bar for the XArray Explorers. - - Parameters: - ----------- - preset : str, optional - Color preset name (default: "Fast") - color_min : float, optional - Minimum color value (default: 0.0) - color_max : float, optional - Maximum color value (default: 1.0) - ctx_name : str, optional - Context name for trame - paraview_lut : paraview.servermanager.PVLookupTable, optional - ParaView color transfer function to initialize from - **kwargs : dict - Additional keyword arguments - """ - # Initialize VTK lookup table - if paraview_lut is not None: - self._lut = paraview_to_vtk_lut(paraview_lut) - # Get range from the converted LUT - lut_range = self._lut.GetRange() - color_min = lut_range[0] - color_max = lut_range[1] - else: - self._lut = vtkLookupTable() - - super().__init__(location="top", ctx_name=ctx_name) - - ns = self.next_id() - self.__preset_image = f"{ns}_preset" - self.__color_min = f"{ns}_color_min" - self.__color_max = f"{ns}_color_max" - # Probe enables mouse events for scalar bar - self.__probe_location = f"{ns}_probe_location" - self.__probe_enabled = f"{ns}_probe_enabled" - - # Initialize state - self.preset = preset - self.set_color_range(color_min, color_max) - self.state[self.__probe_location] = [] - self.state[self.__probe_enabled] = 0 - self.state.client_only( - self.__probe_location, - self.__probe_enabled, - ) - - # Generate initial scalar bar image - self._update_preset_image() - - with self: - # Content - with html.Template(v_slot_activator="{ props }"): - with html.Div( - classes="scalarbar", - rounded="pill", - v_bind="props", - **kwargs, - ): - html.Div( - f"{{{{{self.__color_min}.toFixed(6) }}}}", - classes="scalarbar-left", - ) - html.Img( - src=(self.__preset_image, None), - style="height: 100%; width: 100%;", - classes="rounded-lg border-thin", - mousemove=f"{self.__probe_location} = [$event.x, $event.target.getBoundingClientRect()]", - mouseenter=f"{self.__probe_enabled} = 1", - mouseleave=f"{self.__probe_enabled} = 0", - __events=["mousemove", "mouseenter", "mouseleave"], - ) - html.Div( - v_show=(self.__probe_enabled, False), - classes="scalar-cursor", - style=( - f"`left: ${{{self.__probe_location}?.[0] - {self.__probe_location}?.[1]?.left}}px`", - ), - ) - html.Div( - f"{{{{ {self.__color_max}.toFixed(6) }}}}", - classes="scalarbar-right", - ) - html.Span( - f"{{{{ (({self.__color_max} - {self.__color_min}) * ({self.__probe_location}?.[0] - {self.__probe_location}?.[1]?.left) / {self.__probe_location}?.[1]?.width + {self.__color_min}).toFixed(6) }}}}" - ) - - def set_color_range(self, color_min, color_max): - """Set the color range for the scalar bar.""" - self.state[self.__color_min] = color_min - self.state[self.__color_max] = color_max - - def update_from_paraview_lut(self, paraview_lut, num_colors=256): - """Update the internal VTK lookup table from a ParaView color transfer function. - - Parameters: - ----------- - paraview_lut : paraview.servermanager.PVLookupTable - ParaView color transfer function to update from - num_colors : int, optional - Number of colors in the VTK lookup table (default: 256) - """ - # Convert ParaView LUT to VTK LUT - self._lut = paraview_to_vtk_lut(paraview_lut, num_colors) - - # Update color range from the new LUT - lut_range = self._lut.GetRange() - self.set_color_range(lut_range[0], lut_range[1]) - - # Update the scalar bar image - self._update_preset_image() - - def _update_preset_image(self): - """Generate and update the scalar bar image from the current lookup table.""" - if self._lut is not None: - image_data = to_image(self._lut) - self.state[self.__preset_image] = image_data - - def update_preset(self, preset_name): - """Update the color preset and regenerate the scalar bar image. - - Parameters: - ----------- - preset_name : str - Name of the color preset to apply - """ - self.preset = preset_name - # If you have a method to apply presets to VTK LUT, call it here - # For now, just update the image with current LUT - self._update_preset_image() diff --git a/quickview/ui/toolbar.py b/quickview/ui/toolbar.py index e5173d5..92b6fca 100644 --- a/quickview/ui/toolbar.py +++ b/quickview/ui/toolbar.py @@ -71,13 +71,6 @@ def _update_color_maps(self): # Directly call update_available_color_maps without parameters self._update_available_color_maps() - def _handle_color_bar_toggle(self): - """Toggle the color bar visibility""" - with self.state: - self.state.show_color_bar = not self.state.show_color_bar - if self._update_scalar_bars is not None: - self._update_scalar_bars(self.state.show_color_bar) - def __init__( self, layout_toolbar, @@ -86,7 +79,6 @@ def __init__( load_state=None, load_variables=None, update_available_color_maps=None, - update_scalar_bars=None, generate_state=None, **kwargs, ): @@ -97,21 +89,15 @@ def __init__( self._generate_state = generate_state self._load_state = load_state self._update_available_color_maps = update_available_color_maps - self._update_scalar_bars = update_scalar_bars # Initialize toggle states with self.state: self.state.use_cvd_colors = False self.state.use_standard_colors = True - self.state.show_color_bar = True # Set initial color maps based on default toggle states self._update_color_maps() - # Apply initial scalar bar visibility - if self._update_scalar_bars is not None: - self._update_scalar_bars(True) - with layout_toolbar as toolbar: toolbar.density = "compact" toolbar.style = "overflow-x: auto; overflow-y: hidden;" @@ -162,21 +148,6 @@ def __init__( ): v2.VIcon("mdi-palette") html.Span("Standard colors") - v2.VDivider(vertical=True, classes="mx-2", style="height: 24px;") - with v2.VTooltip(bottom=True): - with html.Template(v_slot_activator="{ on, attrs }"): - with v2.VBtn( - icon=True, - dense=True, - small=True, - v_bind="attrs", - v_on="on", - click=self._handle_color_bar_toggle, - color=("show_color_bar ? 'primary' : ''",), - classes="mx-1", - ): - v2.VIcon("mdi-format-color-fill") - html.Span("Show color bar") v2.VDivider(vertical=True, classes="mx-2") with v2.VCard( flat=True, diff --git a/quickview/utilities.py b/quickview/utilities.py index 473a166..d8f8183 100644 --- a/quickview/utilities.py +++ b/quickview/utilities.py @@ -1,5 +1,11 @@ import os from enum import Enum +import base64 +import numpy as np + +from vtkmodules.vtkCommonCore import vtkUnsignedCharArray, vtkLookupTable +from vtkmodules.vtkCommonDataModel import vtkImageData +from vtkmodules.vtkIOImage import vtkPNGWriter class EventType(Enum): @@ -24,3 +30,136 @@ def ValidateArguments(conn_file, data_file, state_file, work_dir): if work_dir is None: print("No working directory is provided, using current directory as default") return True + + +def get_lut_from_color_transfer_function(paraview_lut, num_colors=256): + """ + Convert a ParaView color transfer function to a VTK lookup table. + + Parameters: + ----------- + paraview_lut : paraview.servermanager.PVLookupTable + The ParaView color transfer function from GetColorTransferFunction() + num_colors : int, optional + Number of colors in the VTK lookup table (default: 256) + + Returns: + -------- + vtkLookupTable + A VTK lookup table with interpolated colors from the ParaView LUT + """ + # Get RGB points from ParaView LUT + rgb_points = paraview_lut.RGBPoints + + if len(rgb_points) < 8: + raise ValueError("ParaView LUT must have at least 2 color points") + + # Create VTK lookup table + vtk_lut = vtkLookupTable() + + # Extract scalars and colors from the flat RGB points array + scalars = np.array([rgb_points[i] for i in range(0, len(rgb_points), 4)]) + colors = np.array( + [ + [rgb_points[i + 1], rgb_points[i + 2], rgb_points[i + 3]] + for i in range(0, len(rgb_points), 4) + ] + ) + + # Get range + min_val = scalars[0] + max_val = scalars[-1] + + # Generate all scalar values for the lookup table + table_scalars = np.linspace(min_val, max_val, num_colors) + + # Vectorized interpolation for all colors at once + r_values = np.interp(table_scalars, scalars, colors[:, 0]) + g_values = np.interp(table_scalars, scalars, colors[:, 1]) + b_values = np.interp(table_scalars, scalars, colors[:, 2]) + + # Set up the VTK lookup table + vtk_lut.SetRange(min_val, max_val) + vtk_lut.SetNumberOfTableValues(num_colors) + vtk_lut.Build() + + # Set all colors at once + for i in range(num_colors): + vtk_lut.SetTableValue(i, r_values[i], g_values[i], b_values[i], 1.0) + + return vtk_lut + + +def vtk_lut_to_image(lut, samples=255): + """ + Convert a VTK lookup table to a base64-encoded PNG image. + + Parameters: + ----------- + lut : vtkLookupTable + The VTK lookup table to convert + samples : int, optional + Number of samples for the color bar (default: 255) + + Returns: + -------- + str + Base64-encoded PNG image as a data URI + """ + colorArray = vtkUnsignedCharArray() + colorArray.SetNumberOfComponents(3) + colorArray.SetNumberOfTuples(samples) + + dataRange = lut.GetRange() + delta = (dataRange[1] - dataRange[0]) / float(samples) + + # Add the color array to an image data + imgData = vtkImageData() + imgData.SetDimensions(samples, 1, 1) + imgData.GetPointData().SetScalars(colorArray) + + # Loop over all presets + rgb = [0, 0, 0] + for i in range(samples): + lut.GetColor(dataRange[0] + float(i) * delta, rgb) + r = int(round(rgb[0] * 255)) + g = int(round(rgb[1] * 255)) + b = int(round(rgb[2] * 255)) + colorArray.SetTuple3(i, r, g, b) + + writer = vtkPNGWriter() + writer.WriteToMemoryOn() + writer.SetInputData(imgData) + writer.SetCompressionLevel(6) + writer.Write() + + writer.GetResult() + + base64_img = base64.standard_b64encode(writer.GetResult()).decode("utf-8") + return f"data:image/png;base64,{base64_img}" + + +def build_colorbar_image(paraview_lut, log_scale=False, invert=False): + """ + Build a colorbar image from a ParaView color transfer function. + + Parameters: + ----------- + paraview_lut : paraview.servermanager.PVLookupTable + The ParaView color transfer function + log_scale : bool, optional + Whether to apply log scale (affects data mapping, not image) + invert : bool, optional + Whether to invert colors (will affect the image) + + Returns: + -------- + str + Base64-encoded PNG image as a data URI + """ + # Convert to VTK LUT - this will get the current state from ParaView + # including any inversions already applied by InvertTransferFunction + vtk_lut = get_lut_from_color_transfer_function(paraview_lut) + + # Convert to image + return vtk_lut_to_image(vtk_lut) diff --git a/quickview/view_manager.py b/quickview/view_manager.py index ec5d15e..3997e62 100644 --- a/quickview/view_manager.py +++ b/quickview/view_manager.py @@ -11,7 +11,6 @@ Text, Show, CreateRenderView, - GetScalarBar, ColorBy, GetColorTransferFunction, AddCameraLink, @@ -19,7 +18,7 @@ ) from quickview.pipeline import EAMVisSource -from quickview.utilities import EventType +from quickview.utilities import EventType, build_colorbar_image from typing import Dict, List, Optional @@ -258,6 +257,7 @@ def update_views_for_timestep(self): 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): if not context.config.override_range: @@ -303,13 +303,8 @@ def configure_new_view(self, var, context: ViewContext, sources): coltrfunc = GetColorTransferFunction(var) coltrfunc.ApplyPreset(context.config.colormap, True) coltrfunc.NanOpacity = 0.0 - LUTColorBar = GetScalarBar(coltrfunc, rview) - LUTColorBar.AutoOrient = 1 - LUTColorBar.WindowLocation = "Lower Right Corner" - LUTColorBar.Title = "" - LUTColorBar.ComponentTitle = "" - LUTColorBar.ScalarBarLength = 0.75 - # LUTColorBar.NanOpacity = 0.0 + + # 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}") @@ -344,7 +339,8 @@ def configure_new_view(self, var, context: ViewContext, sources): repAn.DiffuseColor = [0.67, 0.67, 0.67] repAn.Opacity = 0.4 - rep.SetScalarBarVisibility(rview, self.state.show_color_bar) + # Always hide ParaView scalar bar - using custom HTML colorbar + rep.SetScalarBarVisibility(rview, False) rview.CameraParallelProjection = 1 Render(rview) @@ -358,6 +354,54 @@ def sync_color_config_to_state(self, index, context: ViewContext): state.uselogscale[index] = context.config.use_log_scale state.override_range[index] = context.config.override_range + def generate_colorbar_image(self, index): + """Generate colorbar image for a variable at given index""" + if index >= len(self.state.variables): + return + + var = self.state.variables[index] + context = self.registry.get_view(var) + if context is None: + return + + # Get the ParaView color transfer function + 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 + try: + image_data = build_colorbar_image( + coltrfunc, + log_scale=False, # Always False for image generation + invert=context.config.invert_colors, + ) + # Update state with the new image + with self.state as state: + state.colorbar_images[index] = image_data + 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: widget.reset_camera() @@ -535,6 +579,7 @@ def rebuild_visualization_layout(self, cached_layout=None): self.configure_new_view(var, context, self.source.views) context.index = index self.sync_color_config_to_state(index, context) + self.generate_colorbar_image(index) if index == 0: view0 = view @@ -580,7 +625,13 @@ def update_view_color_settings(self, index, type, value): context: ViewContext = self.registry.get_view(var) if type == EventType.COL.value: context.config.colormap = value + # Generate new colorbar image BEFORE applying any transformations + self.generate_colorbar_image(index) + # Now apply the preset with current transformations coltrfunc.ApplyPreset(context.config.colormap, True) + # Reapply inversion if it was enabled + if context.config.invert_colors: + coltrfunc.InvertTransferFunction() elif type == EventType.LOG.value: context.config.use_log_scale = value if context.config.use_log_scale: @@ -589,20 +640,20 @@ def update_view_color_settings(self, index, type, value): else: coltrfunc.MapControlPointsToLinearSpace() coltrfunc.UseLogScale = 0 + # Log scale doesn't change the image, just the data mapping elif type == EventType.INV.value: context.config.invert_colors = value coltrfunc.InvertTransferFunction() + # Generate new colorbar image when colors are inverted + self.generate_colorbar_image(index) self.render_view_by_index(index) def update_scalar_bars(self, event): + # Always hide ParaView scalar bars - using custom HTML colorbar + # The HTML colorbar is always visible, no toggle needed for var, context in self.registry.items(): view = context.state.view_proxy - context.state.data_representation.SetScalarBarVisibility(view, event) - coltrfunc = GetColorTransferFunction(var) - coltrfunc.ApplyPreset(context.config.colormap, True) - LUTColorBar = GetScalarBar(coltrfunc, view) - LUTColorBar.Title = "" - LUTColorBar.ComponentTitle = "" + context.state.data_representation.SetScalarBarVisibility(view, False) self.render_all_views() def set_manual_color_range(self, index, min, max): From cf42dcad1b066a027c1a33d4cd50b02316d350ea Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Mon, 28 Jul 2025 15:07:28 -0700 Subject: [PATCH 4/5] refactor: Replace EventType enum with direct method calls for color settings - Remove clunky EventType enum from utilities.py - Replace generic update_view_color_settings() with specific methods: - update_colormap() for colormap changes - update_log_scale() for log scale toggling - update_invert_colors() for color inversion - Update ViewProperties widget to accept specific callback methods - Improve state synchronization to avoid recursive updates - Add fallback for non-tauri environments in toolbar - Ensure colorbar regenerates when color ranges change - Fix view configuration initialization with proper index-based values --- quickview/interface.py | 74 +++++++++---------- quickview/ui/toolbar.py | 19 ++++- quickview/ui/view_settings.py | 28 ++++--- quickview/utilities.py | 7 -- quickview/view_manager.py | 134 ++++++++++++++++++++++------------ 5 files changed, 155 insertions(+), 107 deletions(-) diff --git a/quickview/interface.py b/quickview/interface.py index b22f1d7..60903ea 100644 --- a/quickview/interface.py +++ b/quickview/interface.py @@ -26,9 +26,6 @@ # Build color cache here from quickview.view_manager import build_color_information - -from quickview.utilities import EventType - from quickview.view_manager import ViewManager from paraview.simple import ImportPresets, GetLookupTableNames @@ -419,18 +416,17 @@ def load_variables(self): "h": item.get("h", 3), } - def update_view_color_settings(self, index, type, value): - with self.state as state: - if type == EventType.COL.value: - state.varcolor[index] = value - state.dirty("varcolor") - elif type == EventType.LOG.value: - state.uselogscale[index] = value - state.dirty("uselogscale") - elif type == EventType.INV.value: - state.invert[index] = value - state.dirty("invert") - self.viewmanager.update_view_color_settings(index, type, value) + def update_colormap(self, index, value): + """Update the colormap for a variable.""" + self.viewmanager.update_colormap(index, value) + + def update_log_scale(self, index, value): + """Update the log scale setting for a variable.""" + self.viewmanager.update_log_scale(index, value) + + def update_invert_colors(self, index, value): + """Update the color inversion setting for a variable.""" + self.viewmanager.update_invert_colors(index, value) def update_scalar_bars(self, event): self.viewmanager.update_scalar_bars(event) @@ -449,16 +445,11 @@ def update_available_color_maps(self): state.colormaps = noncvd def set_manual_color_range(self, index, type, value): - with self.state as state: - if type.lower() == "min": - state.varmin[index] = value - state.dirty("varmin") - elif type.lower() == "max": - state.varmax[index] = value - state.dirty("varmax") - self.viewmanager.set_manual_color_range( - index, state.varmin[index], state.varmax[index] - ) + # Get current values from state to handle min/max independently + min_val = self.state.varmin[index] if type.lower() == "max" else value + max_val = self.state.varmax[index] if type.lower() == "min" else value + # Delegate to view manager which will update both the view and sync state + self.viewmanager.set_manual_color_range(index, min_val, max_val) def revert_to_auto_color_range(self, index): self.viewmanager.revert_to_auto_color_range(index) @@ -729,30 +720,37 @@ def ui(self) -> SinglePageWithDrawerLayout: style="position:absolute; bottom: 40px; left: 1rem; height: 2rem; z-index: 2;" ): ViewProperties( - apply=self.update_view_color_settings, - update=self.set_manual_color_range, + update_colormap=self.update_colormap, + update_log_scale=self.update_log_scale, + update_invert=self.update_invert_colors, + update_range=self.set_manual_color_range, reset=self.revert_to_auto_color_range, ) # Colorbar container (horizontal layout at bottom) with html.Div( - style="position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; padding: 4px 12px; background-color: rgba(255, 255, 255, 0.9); height: 30px; z-index: 3;" + style="position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; padding: 4px 12px; background-color: rgba(255, 255, 255, 0.9); height: 30px; z-index: 3; overflow: visible;", + classes="drag-ignore", ): # 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' }}", style="font-size: 12px; color: #333; white-space: nowrap;", ) - # Colorbar image - html.Img( - src=( - "colorbar_images[idx] || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='", - None, - ), - style="height: 0.75rem; flex: 1; min-width: 0; max-width: 100%; margin: 0 12px; object-fit: fill;", - classes="rounded-lg border-thin", - v_bind_style="{ background: colorbar_images[idx] ? 'none' : 'linear-gradient(to right, blue, cyan, green, yellow, red)' }", - ) + # Colorbar + with html.Div( + style="flex: 1; position: relative; margin: 0 12px; height: 0.75rem;", + classes="drag-ignore", + ): + # Colorbar image + html.Img( + src=( + "colorbar_images[idx] || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='", + None, + ), + style="height: 100%; width: 100%; object-fit: fill;", + classes="rounded-lg border-thin", + ) # 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' }}", diff --git a/quickview/ui/toolbar.py b/quickview/ui/toolbar.py index 92b6fca..094d49c 100644 --- a/quickview/ui/toolbar.py +++ b/quickview/ui/toolbar.py @@ -1,5 +1,11 @@ from trame.decorators import TrameApp, task -from trame.widgets import html, vuetify2 as v2, tauri +from trame.widgets import html, vuetify2 as v2 + +try: + from trame.widgets import tauri +except ImportError: + # Fallback if tauri is not available + tauri = None import json @@ -83,9 +89,14 @@ def __init__( **kwargs, ): self.server = server - with tauri.Dialog() as dialog: - self.ctrl.open = dialog.open - self.ctrl.save = dialog.save + if tauri: + with tauri.Dialog() as dialog: + self.ctrl.open = dialog.open + self.ctrl.save = dialog.save + else: + # Fallback for non-tauri environments + self.ctrl.open = lambda title: None + self.ctrl.save = lambda title: None self._generate_state = generate_state self._load_state = load_state self._update_available_color_maps = update_available_color_maps diff --git a/quickview/ui/view_settings.py b/quickview/ui/view_settings.py index 52ec981..12ab93b 100644 --- a/quickview/ui/view_settings.py +++ b/quickview/ui/view_settings.py @@ -1,12 +1,18 @@ from trame.widgets import vuetify2 as v2, html from trame.decorators import TrameApp -from quickview.utilities import EventType - @TrameApp() class ViewProperties(v2.VMenu): - def __init__(self, apply=None, update=None, reset=None, **kwargs): + def __init__( + self, + update_colormap=None, + update_log_scale=None, + update_invert=None, + update_range=None, + reset=None, + **kwargs, + ): super().__init__( transition="slide-y-transition", close_on_content_click=False, @@ -37,8 +43,8 @@ def __init__(self, apply=None, update=None, reset=None, **kwargs): items=("colormaps",), outlined=True, change=( - apply, - f"[idx, {EventType.COL.value}, $event]", + update_colormap, + "[idx, $event]", ), **style, ) @@ -49,8 +55,8 @@ def __init__(self, apply=None, update=None, reset=None, **kwargs): label="Log Scale", v_model=("uselogscale[idx]",), change=( - apply, - f"[idx, {EventType.LOG.value}, $event]", + update_log_scale, + "[idx, $event]", ), **style, ) @@ -59,8 +65,8 @@ def __init__(self, apply=None, update=None, reset=None, **kwargs): label="Revert Colors", v_model=("invert[idx]",), change=( - apply, - f"[idx, {EventType.INV.value}, $event]", + update_invert, + "[idx, $event]", ), **style, ) @@ -82,7 +88,7 @@ def __init__(self, apply=None, update=None, reset=None, **kwargs): label="min", outlined=True, change=( - update, + update_range, "[idx, 'min', $event]", ), style="height=50px", @@ -95,7 +101,7 @@ def __init__(self, apply=None, update=None, reset=None, **kwargs): label="max", outlined=True, change=( - update, + update_range, "[idx, 'max', $event]", ), style="height=50px", diff --git a/quickview/utilities.py b/quickview/utilities.py index d8f8183..f5914f5 100644 --- a/quickview/utilities.py +++ b/quickview/utilities.py @@ -1,5 +1,4 @@ import os -from enum import Enum import base64 import numpy as np @@ -8,12 +7,6 @@ from vtkmodules.vtkIOImage import vtkPNGWriter -class EventType(Enum): - COL = 0 - LOG = 1 - INV = 2 - - def ValidateArguments(conn_file, data_file, state_file, work_dir): if (conn_file is None or data_file is None) and state_file is None: print( diff --git a/quickview/view_manager.py b/quickview/view_manager.py index 3997e62..3adb952 100644 --- a/quickview/view_manager.py +++ b/quickview/view_manager.py @@ -18,7 +18,7 @@ ) from quickview.pipeline import EAMVisSource -from quickview.utilities import EventType, build_colorbar_image +from quickview.utilities import build_colorbar_image from typing import Dict, List, Optional @@ -304,6 +304,10 @@ def configure_new_view(self, var, context: ViewContext, sources): coltrfunc.ApplyPreset(context.config.colormap, True) coltrfunc.NanOpacity = 0.0 + # Ensure the color transfer function is scaled to the data range + if not context.config.override_range: + rep.RescaleTransferFunctionToDataRange(False, True) + # ParaView scalar bar is always hidden - using custom HTML colorbar instead (v_text, V_info) = self.get_var_info(var, context.state.computed_average) @@ -347,12 +351,18 @@ def configure_new_view(self, var, context: ViewContext, sources): # ResetCamera(rview) def sync_color_config_to_state(self, index, context: ViewContext): - with self.state as state: - state.varcolor[index] = context.config.colormap - state.varmin[index] = context.config.min_value - state.varmax[index] = context.config.max_value - state.uselogscale[index] = context.config.use_log_scale - state.override_range[index] = context.config.override_range + # Update state arrays directly without context manager to avoid recursive flush + self.state.varcolor[index] = context.config.colormap + self.state.varmin[index] = context.config.min_value + 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 + # 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") def generate_colorbar_image(self, index): """Generate colorbar image for a variable at given index""" @@ -385,10 +395,9 @@ def generate_colorbar_image(self, index): log_scale=False, # Always False for image generation invert=context.config.invert_colors, ) - # Update state with the new image - with self.state as state: - state.colorbar_images[index] = image_data - state.dirty("colorbar_images") + # Update state with the new image without context manager to avoid recursive flush + 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: @@ -561,9 +570,15 @@ def rebuild_visualization_layout(self, cached_layout=None): config = ViewConfiguration( variable=var, - colormap=state.varcolor[0], - use_log_scale=False, - invert_colors=False, + colormap=state.varcolor[index] + if index < len(state.varcolor) + else state.varcolor[0], + use_log_scale=state.uselogscale[index] + if index < len(state.uselogscale) + else False, + invert_colors=state.invert[index] + if index < len(state.invert) + else False, min_value=varrange[0], max_value=varrange[1], override_range=override, @@ -618,34 +633,58 @@ async def flushViews(self): self.render_all_views() """ - def update_view_color_settings(self, index, type, value): + def update_colormap(self, index, value): + """Update the colormap for a variable.""" var = self.state.variables[index] coltrfunc = GetColorTransferFunction(var) - context: ViewContext = self.registry.get_view(var) - if type == EventType.COL.value: - context.config.colormap = value - # Generate new colorbar image BEFORE applying any transformations - self.generate_colorbar_image(index) - # Now apply the preset with current transformations - coltrfunc.ApplyPreset(context.config.colormap, True) - # Reapply inversion if it was enabled - if context.config.invert_colors: - coltrfunc.InvertTransferFunction() - elif type == EventType.LOG.value: - context.config.use_log_scale = value - if context.config.use_log_scale: - coltrfunc.MapControlPointsToLogSpace() - coltrfunc.UseLogScale = 1 - else: - coltrfunc.MapControlPointsToLinearSpace() - coltrfunc.UseLogScale = 0 - # Log scale doesn't change the image, just the data mapping - elif type == EventType.INV.value: - context.config.invert_colors = value + + context.config.colormap = value + # Generate new colorbar image BEFORE applying any transformations + self.generate_colorbar_image(index) + # Now apply the preset with current transformations + coltrfunc.ApplyPreset(context.config.colormap, True) + # Reapply inversion if it was enabled + if context.config.invert_colors: coltrfunc.InvertTransferFunction() - # Generate new colorbar image when colors are inverted - self.generate_colorbar_image(index) + + # Sync all color configuration changes back to state + self.sync_color_config_to_state(index, context) + self.render_view_by_index(index) + + def update_log_scale(self, index, value): + """Update the log scale setting for a variable.""" + var = self.state.variables[index] + coltrfunc = GetColorTransferFunction(var) + context: ViewContext = self.registry.get_view(var) + + context.config.use_log_scale = value + if context.config.use_log_scale: + coltrfunc.MapControlPointsToLogSpace() + coltrfunc.UseLogScale = 1 + else: + coltrfunc.MapControlPointsToLinearSpace() + coltrfunc.UseLogScale = 0 + # Regenerate colorbar after log scale change + self.generate_colorbar_image(index) + + # Sync all color configuration changes back to state + self.sync_color_config_to_state(index, context) + self.render_view_by_index(index) + + def update_invert_colors(self, index, value): + """Update the color inversion setting for a variable.""" + var = self.state.variables[index] + coltrfunc = GetColorTransferFunction(var) + context: ViewContext = self.registry.get_view(var) + + context.config.invert_colors = value + coltrfunc.InvertTransferFunction() + # Generate new colorbar image when colors are inverted + self.generate_colorbar_image(index) + + # Sync all color configuration changes back to state + self.sync_color_config_to_state(index, context) self.render_view_by_index(index) def update_scalar_bars(self, event): @@ -662,11 +701,13 @@ def set_manual_color_range(self, index, min, max): context.config.override_range = True context.config.min_value = float(min) context.config.max_value = float(max) - # Update state to reflect manual override - self.state.override_range[index] = True - self.state.dirty("override_range") + # Sync all changes back to state + self.sync_color_config_to_state(index, context) + # Update color transfer function coltrfunc = GetColorTransferFunction(var) coltrfunc.RescaleTransferFunction(float(min), float(max)) + # Generate new colorbar image with updated range + self.generate_colorbar_image(index) self.render_view_by_index(index) def revert_to_auto_color_range(self, index): @@ -677,15 +718,14 @@ def revert_to_auto_color_range(self, index): context.config.override_range = False context.config.min_value = varrange[0] context.config.max_value = varrange[1] - self.state.varmin[index] = context.config.min_value - self.state.dirty("varmin") - self.state.varmax[index] = context.config.max_value - self.state.dirty("varmax") - self.state.override_range[index] = context.config.override_range - self.state.dirty("override_range") + # Sync all changes back to state + self.sync_color_config_to_state(index, context) + # Rescale transfer function to data range context.state.data_representation.RescaleTransferFunctionToDataRange( False, True ) + # Generate new colorbar image with updated range + self.generate_colorbar_image(index) self.render_all_views() def zoom_in(self, index=0): From d4f286c07166e2c607910f4b93d85962766311b8 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Mon, 28 Jul 2025 15:23:57 -0700 Subject: [PATCH 5/5] =?UTF-8?q?Bump=20version:=200.1.4=20=E2=86=92=200.1.5?= 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 d66c946..de659a9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.4 +current_version = 0.1.5 commit = True tag = True diff --git a/pyproject.toml b/pyproject.toml index 5c7c3a9..621a87d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "quickview" -version = "0.1.4" +version = "0.1.5" 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 7610272..957169c 100644 --- a/quickview/__init__.py +++ b/quickview/__init__.py @@ -1,5 +1,5 @@ """QuickView: Visual Analysis for E3SM Atmosphere Data.""" -__version__ = "0.1.4" +__version__ = "0.1.5" __author__ = "Kitware Inc." __license__ = "Apache-2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 56fab31..11fbb18 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.4" +version = "0.1.5" 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 1893ef4..d02f94a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "QuickView", - "version": "0.1.4" + "version": "0.1.5" }, "tauri": { "allowlist": {