From bfca5811799d2f7a7a18c81e0c1a4efe0166bf59 Mon Sep 17 00:00:00 2001 From: Hui Wan Date: Mon, 25 Aug 2025 17:22:21 -0700 Subject: [PATCH 1/6] docs: correct a screenshot filename in toolbar.md --- docs/userguide/toolbar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/toolbar.md b/docs/userguide/toolbar.md index d764ca3..e5513de 100644 --- a/docs/userguide/toolbar.md +++ b/docs/userguide/toolbar.md @@ -136,7 +136,7 @@ from [PareView](https://www.paraview.org/). -![camera actions](../images/camera_actions.png){ width="25%", align=right } +![camera actions](../images/toolbar_camera_actions.png){ width="25%", align=right } **Camera Actions**: A set of buttons are provided to simultaneously adjust all views From 71a1cc7b05c01490d4ae5a3cbbcd2bd7627f05a3 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Mon, 25 Aug 2025 21:01:09 -0700 Subject: [PATCH 2/6] fix: make average return type trame friendly --- quickview/utils/math.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/quickview/utils/math.py b/quickview/utils/math.py index d856290..9f0d7ea 100644 --- a/quickview/utils/math.py +++ b/quickview/utils/math.py @@ -23,7 +23,8 @@ def calculate_weighted_average( The (weighted) average, handling NaN values """ data = np.array(data_array) - + weights = np.array(weights) + print(data, weights) # Handle NaN values if np.isnan(data).any(): mask = ~np.isnan(data) @@ -31,12 +32,12 @@ def calculate_weighted_average( return np.nan # all values are NaN data = data[mask] if weights is not None: - weights = np.array(weights)[mask] + weights = weights[mask] if weights is not None: - return np.average(data, weights=weights) + return float(np.average(data, weights=weights)) else: - return np.mean(data) + return float(np.mean(data)) def calculate_data_range(bounds: List[float]) -> Tuple[float, float, float]: From 18604bc2bd658e2b3f420d9249387e62db29162f Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Tue, 26 Aug 2025 14:43:47 -0700 Subject: [PATCH 3/6] feat: Adding close button for views known issues -- After close views, layout operations are not honored for some reason -- latent selection, after closing a few views some variables are not unchecked in the UI --- quickview/interface.py | 67 +++++++++++++++++++++++++++------------ quickview/view_manager.py | 49 ++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 35 deletions(-) diff --git a/quickview/interface.py b/quickview/interface.py index 0d3ca32..451688f 100644 --- a/quickview/interface.py +++ b/quickview/interface.py @@ -324,21 +324,21 @@ def update_state_from_config(self, initstate): @trigger("layout_changed") def on_layout_changed_trigger(self, layout, **kwargs): - """Cache layout changes to ensure they are properly saved""" + # There should always be a 1:1 correspondence + # between the layout and the variables + assert len(layout) == len(self.state.variables) # Cache the layout data with variable names as keys for easier lookup self._cached_layout = {} - if layout and hasattr(self.state, "variables"): - for item in layout: - if isinstance(item, dict) and "i" in item: - idx = item["i"] - if idx < len(self.state.variables): - var_name = self.state.variables[idx] - self._cached_layout[var_name] = { - "x": item.get("x", 0), - "y": item.get("y", 0), - "w": item.get("w", 4), - "h": item.get("h", 3), - } + for idx, item in enumerate(layout): + if idx < len(self.state.variables): + var_name = self.state.variables[idx] + self._cached_layout[var_name] = { + "i": idx, + "x": item.get("x", 0), + "y": item.get("y", 0), + "w": item.get("w", 4), + "h": item.get("h", 3), + } def generate_state(self): # Force state synchronization @@ -421,7 +421,7 @@ def load_data(self): self.init_app_configuration() else: # Keep the defaults that were set in __init__ - # but ensure arrays are empty if pipeline failed + # but ensure arrays are empty if pipeline state.timesteps = [] state.midpoints = [] state.interfaces = [] @@ -477,10 +477,12 @@ def load_variables(self, use_cached_layout=False): self.source.LoadVariables(surf, mid, intf) vars = surf + mid + intf + varorigin = [0] * len(surf) + [1] * len(mid) + [2] * len(intf) # Tracking variables to control camera and color properties with self.state as state: state.variables = vars + state.varorigin = varorigin # Initialize arrays that are always needed regardless of cache status # Color configuration arrays will be populated by ViewContext via sync_color_config_to_state @@ -712,7 +714,30 @@ def clear_interface_vars(self, clear_var_name): self.state.dirty("interface_vars_state") def close_view(self, index): - print("Requested close view for ", index) + var = self.state.variables.pop(index) + self._cached_layout.pop(var) + self.state.dirty("variables") + self.viewmanager.close_view(var, index, self._cached_layout) + with self.state as state: + origin = state.varorigin[index] + # Find variable to unselect from the UI + if origin == 0: + # Find and clear surface display + if var in state.surface_vars: + var_index = state.surface_vars.index(var) + self.update_surface_var_selection(var_index, False) + elif origin == 1: + # Find and clear midpoints display + # Find and clear surface display + if var in state.midpoint_vars: + var_index = state.midpoint_vars.index(var) + self.update_midpoint_var_selection(var_index, False) + elif origin == 2: + # Find and clear interface display + # Find and clear surface display + if var in state.interface_vars: + var_index = state.interface_vars.index(var) + self.update_interface_var_selection(var_index, False) def start(self, **kwargs): """Initialize the UI and start the server for GeoTrame.""" @@ -970,11 +995,11 @@ def ui(self) -> SinglePageWithDrawerLayout: style="color: white;", classes="font-weight-medium", ) - # with v2.VBtn( - # icon=True, - # style="position: absolute; top: 8px; right: 8px; padding: 4px 8px; z-index: 2; color: white;", - # click=(self.close_view, "[idx]"), - # ): - # v2.VIcon("mdi-close") + with v2.VBtn( + icon=True, + style="position: absolute; top: 8px; right: 8px; padding: 4px 8px; z-index: 2; color: white;", + click=(self.close_view, "[idx]"), + ): + v2.VIcon("mdi-close") return self._ui diff --git a/quickview/view_manager.py b/quickview/view_manager.py index 47cb713..c8cfc1f 100644 --- a/quickview/view_manager.py +++ b/quickview/view_manager.py @@ -219,6 +219,37 @@ def _on_change_pipeline_valid(self, pipeline_valid, **kwargs): self.state.dirty("views") self.state.dirty("layout") + def close_view(self, var, index, layout_cache): + # Clear cache and remove layout and widget entry + with self.state as state: + self.registry.remove_view(var) + state.varcolor.pop(index) + state.varmin.pop(index) + state.varmax.pop(index) + state.uselogscale.pop(index) + state.override_range.pop(index) + state.invert.pop(index) + state.views.pop(index) + state.colorbar_images.pop(index) + + # Rebuild layout + new_layout = [] + vars = self.state.variables + for var in vars: + cache = layout_cache[var] + new_layout.append(cache) + self.state.layout = new_layout + + 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") + self.state.dirty("layout") + self.state.dirty("views") + self.state.dirty("colorbar_images") + def update_views_for_timestep(self): if len(self.registry) == 0: return @@ -226,9 +257,10 @@ def update_views_for_timestep(self): first_view = None for var, context in self.registry.items(): + index = self.state.variables.index(var) varavg = self.compute_average(var, vtkdata=data) # Directly set average in trame state - self.state.varaverage[context.index] = varavg + self.state.varaverage[index] = varavg self.state.dirty("varaverage") if not context.config.override_range: context.state.data_representation.RescaleTransferFunctionToDataRange( @@ -237,8 +269,8 @@ def update_views_for_timestep(self): range = self.compute_range(var=var) context.config.min_value = range[0] context.config.max_value = range[1] - self.sync_color_config_to_state(context.index, context) - self.generate_colorbar_image(context.index) + self.sync_color_config_to_state(index, context) + self.generate_colorbar_image(index) # Track the first view for camera fitting if first_view is None and context.state.view_proxy: @@ -562,17 +594,6 @@ def rebuild_visualization_layout(self, cached_layout=None): self.state.layout = layout self.state.dirty("views") self.state.dirty("layout") - # from trame.app import asynchronous - # asynchronous.create_task(self.flushViews()) - - """ - async def flushViews(self): - await self.server.network_completion - self.render_all_views() - import asyncio - await asyncio.sleep(1) - self.render_all_views() - """ def update_colormap(self, index, value): """Update the colormap for a variable.""" From 27d08888f9e063d37619f0088baffe5136689c98 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Tue, 26 Aug 2025 15:07:43 -0700 Subject: [PATCH 4/6] fix: Remove unnecessary print and change TrameApp to app instead of decorator --- quickview/interface.py | 26 +++++++++++--------------- quickview/utils/math.py | 1 - 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/quickview/interface.py b/quickview/interface.py index 451688f..4f752fd 100644 --- a/quickview/interface.py +++ b/quickview/interface.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Union -from trame.app import get_server -from trame.decorators import TrameApp, life_cycle, trigger, change +from trame.app import TrameApp +from trame.decorators import life_cycle, trigger, change from trame.ui.vuetify import SinglePageWithDrawerLayout from trame.widgets import vuetify as v2, html, client @@ -143,8 +143,7 @@ def get_logo_base64(): print("Error loading presets :", e) -@TrameApp() -class EAMApp: +class EAMApp(TrameApp): def __init__( self, source: EAMVisSource = None, @@ -152,23 +151,18 @@ def __init__( initstate: dict = None, workdir: Union[str, Path] = None, ) -> None: - server = get_server(initserver, client_type="vue2") - state = server.state - ctrl = server.controller + super().__init__(initserver, client_type="vue2") self._ui = None self._cached_layout = {} # Cache for layout positions by variable name - self.workdir = workdir - self.server = server - - pvWidgets.initialize(server) + pvWidgets.initialize(self.server) self.source = source - self.viewmanager = ViewManager(source, server, state) + self.viewmanager = ViewManager(source, self.server, self.state) + state = self.state # Load state variables from the source object - state.data_file = source.data_file if source.data_file else "" state.conn_file = source.conn_file if source.conn_file else "" @@ -231,10 +225,12 @@ def __init__( state.probe_enabled = False state.probe_location = [] # Default probe + ctrl = self.ctrl ctrl.view_update = self.viewmanager.render_all_views ctrl.view_reset_camera = self.viewmanager.reset_camera ctrl.on_server_ready.add(ctrl.view_update) - server.trigger_name(ctrl.view_reset_camera) + + self.server.trigger_name(ctrl.view_reset_camera) state.colormaps = noncvd @@ -827,7 +823,7 @@ def ui(self) -> SinglePageWithDrawerLayout: with layout.content: with grid.GridLayout( - layout=("layout"), + layout=("layout",), col_num=12, row_height=100, is_draggable=True, diff --git a/quickview/utils/math.py b/quickview/utils/math.py index 9f0d7ea..b4e80f9 100644 --- a/quickview/utils/math.py +++ b/quickview/utils/math.py @@ -24,7 +24,6 @@ def calculate_weighted_average( """ data = np.array(data_array) weights = np.array(weights) - print(data, weights) # Handle NaN values if np.isnan(data).any(): mask = ~np.isnan(data) From 57f2e51be55a0df8872ea62f0172a6c60578cd80 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Thu, 28 Aug 2025 13:35:15 -0700 Subject: [PATCH 5/6] fix: fixing close view issues --- quickview/interface.py | 57 ++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/quickview/interface.py b/quickview/interface.py index 4f752fd..6b6f383 100644 --- a/quickview/interface.py +++ b/quickview/interface.py @@ -593,31 +593,34 @@ def pan_camera(self, dir): self.viewmanager.pan_camera(0, 0) def update_surface_var_selection(self, index, event): - self.state.surface_vars_state[index] = event - self.state.dirty("surface_vars_state") + with self.state as state: + state.surface_vars_state[index] = event if self.ind_surface is not None: ind = self.ind_surface[index] self.surface_vars_state[ind] = event else: self.surface_vars_state[index] = event + self.state.dirty("surface_vars_state") def update_midpoint_var_selection(self, index, event): - self.state.midpoint_vars_state[index] = event - self.state.dirty("midpoint_vars_state") + with self.state as state: + state.midpoint_vars_state[index] = event if self.ind_midpoint is not None: ind = self.ind_midpoint[index] self.midpoint_vars_state[ind] = event else: self.midpoint_vars_state[index] = event + self.state.dirty("midpoint_vars_state") def update_interface_var_selection(self, index, event): - self.state.interface_vars_state[index] = event - self.state.dirty("interface_vars_state") + with self.state as state: + state.interface_vars_state[index] = event if self.ind_interface is not None: ind = self.ind_interface[index] self.interface_vars_state[ind] = event else: self.interface_vars_state[index] = event + self.state.dirty("interface_vars_state") def search_surface_vars(self, search: str): if search is None or len(search) == 0: @@ -711,29 +714,29 @@ def clear_interface_vars(self, clear_var_name): def close_view(self, index): var = self.state.variables.pop(index) + origin = self.state.varorigin.pop(index) self._cached_layout.pop(var) self.state.dirty("variables") + self.state.dirty("varorigin") self.viewmanager.close_view(var, index, self._cached_layout) - with self.state as state: - origin = state.varorigin[index] - # Find variable to unselect from the UI - if origin == 0: - # Find and clear surface display - if var in state.surface_vars: - var_index = state.surface_vars.index(var) - self.update_surface_var_selection(var_index, False) - elif origin == 1: - # Find and clear midpoints display - # Find and clear surface display - if var in state.midpoint_vars: - var_index = state.midpoint_vars.index(var) - self.update_midpoint_var_selection(var_index, False) - elif origin == 2: - # Find and clear interface display - # Find and clear surface display - if var in state.interface_vars: - var_index = state.interface_vars.index(var) - self.update_interface_var_selection(var_index, False) + state = self.state + + # Find variable to unselect from the UI + if origin == 0: + # Find and clear surface display + if var in state.surface_vars: + var_index = state.surface_vars.index(var) + self.update_surface_var_selection(var_index, False) + elif origin == 1: + # Find and clear midpoints display + if var in state.midpoint_vars: + var_index = state.midpoint_vars.index(var) + self.update_midpoint_var_selection(var_index, False) + elif origin == 2: + # Find and clear interface display + if var in state.interface_vars: + var_index = state.interface_vars.index(var) + self.update_interface_var_selection(var_index, False) def start(self, **kwargs): """Initialize the UI and start the server for GeoTrame.""" @@ -830,7 +833,7 @@ def ui(self) -> SinglePageWithDrawerLayout: is_resizable=True, vertical_compact=True, layout_updated="layout = $event; trigger('layout_changed', [$event])", - ): + ) as self.grid: with grid.GridItem( v_for="vref, idx in views", key="vref", From ff297959028ad4fc173bf07b2288d2b726764b82 Mon Sep 17 00:00:00 2001 From: Abhishek Yenpure Date: Thu, 28 Aug 2025 13:40:39 -0700 Subject: [PATCH 6/6] =?UTF-8?q?Bump=20version:=201.0.0=20=E2=86=92=201.0.1?= 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 4d7b504..a034b1f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.0 +current_version = 1.0.1 commit = True tag = True diff --git a/pyproject.toml b/pyproject.toml index d2e2b91..10b900b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "quickview" -version = "1.0.0" +version = "1.0.1" 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 977623e..7ce5c4b 100644 --- a/quickview/__init__.py +++ b/quickview/__init__.py @@ -1,5 +1,5 @@ """QuickView: Visual Analysis for E3SM Atmosphere Data.""" -__version__ = "1.0.0" +__version__ = "1.0.1" __author__ = "Kitware Inc." __license__ = "Apache-2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4a7f244..9234c63 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "1.0.0" +version = "1.0.1" 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 33ac02e..eb2a8c3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "QuickView", - "version": "1.0.0" + "version": "1.0.1" }, "tauri": { "allowlist": {