diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 320624f..ea978ab 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.1.8
+current_version = 0.1.9
commit = True
tag = True
diff --git a/README.md b/README.md
index a1a7b2a..9549822 100644
--- a/README.md
+++ b/README.md
@@ -3,70 +3,141 @@
[](https://github.com/ayenpure/QuickView/actions/workflows/test.yml)
[](https://github.com/ayenpure/QuickView/actions/workflows/package-and-release.yml)
-A scientific visualization application for exploring atmospheric data from the
-E3SM (Energy Exascale Earth System Model) project.
+**QuickView** is an interactive visualization tool designed specifically for
+atmospheric scientists working with E3SM (Energy Exascale Earth System Model)
+data. Built on ParaView and Trame, it provides an intuitive interface for
+exploring atmospheric simulation outputs without the steep learning curve of
+general-purpose visualization tools.
-## Setting up conda environment
+
-This trame application has two requirements.
+## Why QuickView?
-### Python version 3.10
+Traditional visualization tools like ParaView and VisIt, while powerful, often
+require significant time investment to master their complex interfaces and may
+lack atmospheric science-specific features out of the box. QuickView addresses
+these challenges by:
-Python can be installed using either `homebrew` if using macOS like
+- **Reducing the learning curve** - Atmospheric scientists can start visualizing
+ their data immediately
+- **Eliminating "last-mile" effort** - No need to write custom scripts or
+ plugins for common tasks
+- **Accelerating insights** - Focus on science, not software configuration
+- **Building on proven technology** - Leverages ParaView's robust data
+ processing with a tailored interface
-`brew install python3.10`
+## Installation
-or `apt` if using Ubuntu Linux.
+### Using Conda (Recommended)
-`sudo apt install python3.10`
+1. Create and activate a conda environment:
-Alternatively, python can also be installed using anaconda/miniconda
+```bash
+conda env create -f quickview-env.yml
+conda activate quickview
+```
-`conda create --name eamapp python=3.10.0 conda activate eamapp`
+2. Install QuickView:
-However, this would require activating the conda environment before using the
-app.
+```bash
+pip install .
+```
-### ParaView 5.13 installed on the system
+### Requirements
-ParaView can be installed from the binaries found at
-https://www.paraview.org/download
+- Python 3.13
+- ParaView 5.13.3 (installed automatically with conda environment)
+- Trame and other dependencies (installed automatically)
-The additional requirements for the app are satisfied once the app is launched
-for the very first time using Python virtual environments `venv`. An additional
-step for the use is to provide the path to ParaView's Python client that is
-distributed with the ParaView binaries. The `pvpython` binary is present in the
-`bin` directory of ParaView, on macOS the path is something like
-`/Applications/ParaView-5.13.0.app/Contents/bin/pvpython`
+## Getting the Code
-To clone this repository use the following commands
+### Clone from GitHub
+```bash
+git clone https://github.com/ayenpure/QuickView.git
+cd QuickView
```
-git clone https://gitlab.kitware.com/ayenpure/eamapp.git
-cd eamapp
-git lfs install
-git lfs pull
+
+### Download as Archive
+
+```bash
+wget https://github.com/ayenpure/QuickView/archive/main.tar.gz
+tar -xvzf main.tar.gz
+cd QuickView-main
```
-Alternatively, the code can also be downloaded as a tarball
+## Running the Application
+
+To run QuickView with a data file:
+```bash
+python -m quickview.app --data data/aerosol_F2010.eam.h0.2014-12.nc
```
-wget https://gitlab.kitware.com/ayenpure/eamapp/-/archive/master/eamapp-master.tar.gz
-tar -xvzf eamapp-master.tar.gz
-cd eamapp-master
+
+### Command Line Options
+
+- `--data`: Path to the NetCDF data file (required)
+- `--conn`: Path to the connectivity file (optional)
+- `--port`: Server port (default: 8080)
+- `--host`: Server host (default: localhost)
+
+### Example with Custom Files
+
+```bash
+python -m quickview.app --data /path/to/your/data.nc --conn /path/to/your/connectivity.nc
```
-To run the app, execute
+The application will start a Trame web server. Open your browser and navigate
+to:
```
-python3 launch.py --data data/aerosol_F2010.eam.h0.2014-12.nc
+http://localhost:8080
```
-Please ensure that the `python3` is the correct version. In the above execution
-the data file is provided as the sample data out of the repository. The
-repository also contains the connectivity file that it uses by default. If
-another connectivity and data files are to be used please specify the paths
-using the `--conn` and `--data` options.
+## Sample Data
+
+The repository includes sample data files in the `data/` directory for testing:
+
+- `aerosol_F2010.eam.h0.2014-12.nc` - Sample atmospheric data
+- Default connectivity file is automatically loaded
+
+## Development
+
+For development setup and contribution guidelines, please see
+[CONTRIBUTING.md](CONTRIBUTING.md).
+
+## Documentation
+
+Comprehensive documentation is available in the [docs/](docs/) directory,
+including:
+
+- [User Guide](docs/userguide/launch.md) - Detailed usage instructions
+- [Control Panel Guide](docs/userguide/control_panel.md) - Interface overview
+- [Viewport Customization](docs/userguide/viewport.md) - Working with multiple
+ variables
+- [Data Requirements](docs/data-requirements.md) - NetCDF file format and
+ required variables
+
+## About
+
+QuickView is developed by [Kitware, Inc.](https://www.kitware.com/) in
+collaboration with
+[Pacific Northwest National Laboratory](https://www.pnnl.gov/). It is supported
+by the U.S. Department of Energy's
+[Biological and Environmental Research (BER)](https://www.energy.gov/science/ber/biological-and-environmental-research)
+and
+[Advanced Scientific Computing Research (ASCR)](https://www.energy.gov/science/ascr/advanced-scientific-computing-research)
+programs via the
+[Scientific Discovery through Advanced Computing (SciDAC)](https://www.scidac.gov/)
+program.
+
+### Contributors
+
+- **Lead Developer**: Abhishek Yenpure (Kitware, Inc.)
+- **Key Contributors**: Berk Geveci, Sebastien Jourdain (Kitware, Inc.); Hui
+ Wan, Kai Zhang (PNNL)
+
+## License
-The above command will start the Trame app server. On the browser proceed to
-`http://localhost:8080` to use the app
+This project is licensed under the Apache Software License - see the
+[LICENSE](LICENSE) file for details.
diff --git a/docs/README.md b/docs/README.md
index 07f82fe..69ca30a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -51,6 +51,9 @@ To learn more about the installation of EAM QuickView, checkout the
To learn more about using EAM QuickView, checkout the
[brief overview.](tutorials/eamapp.md)
+For information about data file requirements and supported formats, see the
+[data requirements documentation](data-requirements.md)
+
## Point of Contact
The lead developer of EAM QuickView is
diff --git a/docs/data-requirements.md b/docs/data-requirements.md
new file mode 100644
index 0000000..74ef904
--- /dev/null
+++ b/docs/data-requirements.md
@@ -0,0 +1,154 @@
+# QuickView Data File Requirements
+
+This document describes the NetCDF file format and variable requirements for
+QuickView to properly read and visualize E3SM atmospheric data.
+
+## Overview
+
+QuickView requires two NetCDF files:
+
+1. **Data File** - Contains the atmospheric variables and time-varying data
+2. **Connectivity File** - Contains the mesh geometry and grid structure
+
+## Required Dimensions
+
+The following dimensions must be present in the data files:
+
+### In Data File:
+
+- `time` - Time dimension for temporal data
+- `ncol` - Number of columns (horizontal grid points)
+- `lev` - Number of vertical levels at layer midpoints
+- `ilev` - Number of vertical levels at layer interfaces
+
+### In Connectivity File:
+
+- `grid_size` or `ncol` - Total number of grid cells
+
+## Required Variables
+
+### 1. Coordinate Variables
+
+#### Latitude and Longitude (Required)
+
+The connectivity file must contain corner coordinates for grid cells:
+
+- **Variables containing `corner_lat`** - Latitude coordinates of cell corners
+- **Variables containing `corner_lon`** - Longitude coordinates of cell corners
+
+These are used to construct the unstructured grid geometry.
+
+#### Vertical Coordinate Variables
+
+For proper vertical level display, the data file should contain either:
+
+**Option A: Direct level values**
+
+- `lev` - Pressure levels at layer midpoints (hPa)
+- `ilev` - Pressure levels at layer interfaces (hPa)
+
+**Option B: Hybrid coordinate coefficients** If `lev` and `ilev` are not
+directly provided, they will be computed from:
+
+- Variables containing `hyam` - Hybrid A coefficient at layer midpoints
+- Variables containing `hybm` - Hybrid B coefficient at layer midpoints
+- Variables containing `hyai` - Hybrid A coefficient at layer interfaces
+- Variables containing `hybi` - Hybrid B coefficient at layer interfaces
+
+The pressure levels are computed as:
+
+```
+pressure = (hyam * P0) + (hybm * PS0)
+```
+
+where P0 = 100000 Pa (reference pressure) and PS0 = 100000 Pa (surface
+pressure).
+
+### 2. Time Variable (Required)
+
+- `time` - Time coordinate variable containing timestamps for each time step
+
+### 3. Area Variable (Optional but Recommended)
+
+- Variable containing `area` in its name - Grid cell areas used for computing
+ area-weighted averages
+ - If not present, simple arithmetic averaging will be used instead
+
+## Variable Types Supported
+
+QuickView categorizes variables based on their dimensions:
+
+### 1D Variables (Info Variables)
+
+- Dimensions: `(ncol)`
+- Example: `area`
+- These are typically time-invariant grid properties
+
+### 2D Variables (Surface Variables)
+
+- Dimensions: `(time, ncol)`
+- Example: `TS` (surface temperature), `PS` (surface pressure)
+- These represent surface or column-integrated quantities
+
+### 3D Midpoint Variables
+
+- Dimensions: `(time, lev, ncol)` or `(time, ncol, lev)`
+- Example: `T` (temperature), `U` (zonal wind), `Q` (specific humidity)
+- These are defined at layer midpoints
+
+### 3D Interface Variables
+
+- Dimensions: `(time, ilev, ncol)` or `(time, ncol, ilev)`
+- Example: `OMEGA` (vertical velocity)
+- These are defined at layer interfaces
+
+## Variable Attributes
+
+### Fill Values
+
+Variables should include the `_FillValue` attribute to indicate missing or
+undefined data. QuickView will convert these to NaN values for proper handling.
+
+## Example Data Structure
+
+### Data File Structure:
+
+```
+dimensions:
+ time = 12 ;
+ ncol = 48602 ;
+ lev = 72 ;
+ ilev = 73 ;
+
+variables:
+ double time(time) ;
+ double T(time, lev, ncol) ;
+ T:_FillValue = 9.96920996838687e+36 ;
+ double TS(time, ncol) ;
+ TS:_FillValue = 9.96920996838687e+36 ;
+ double hyam(lev) ;
+ double hybm(lev) ;
+ double hyai(ilev) ;
+ double hybi(ilev) ;
+```
+
+### Connectivity File Structure:
+
+```
+dimensions:
+ grid_size = 48602 ;
+ grid_corners = 4 ;
+
+variables:
+ double grid_corner_lat(grid_size, grid_corners) ;
+ double grid_corner_lon(grid_size, grid_corners) ;
+```
+
+## Notes
+
+1. The reader automatically detects and categorizes variables based on their
+ dimensions
+2. Variables with dimensions not matching the expected patterns are ignored
+3. The reader caches geometry and special variables for performance
+4. Time interpolation is handled automatically when requesting specific time
+ steps
diff --git a/docs/images/main.png b/docs/images/main.png
new file mode 100644
index 0000000..a8d4bbf
--- /dev/null
+++ b/docs/images/main.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4fbfe24c8789ee9ddbd09550d91f3ca683caf59eb1a3569faa4e64ab7c039a8
+size 1285101
diff --git a/pyproject.toml b/pyproject.toml
index 7d8eada..f834a61 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "quickview"
-version = "0.1.8"
+version = "0.1.9"
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 7bbf27b..9b9f9f9 100644
--- a/quickview/__init__.py
+++ b/quickview/__init__.py
@@ -1,5 +1,5 @@
"""QuickView: Visual Analysis for E3SM Atmosphere Data."""
-__version__ = "0.1.8"
+__version__ = "0.1.9"
__author__ = "Kitware Inc."
__license__ = "Apache-2.0"
diff --git a/quickview/colorbar_cache.py b/quickview/colorbar_cache.py
new file mode 100644
index 0000000..e2dbc41
--- /dev/null
+++ b/quickview/colorbar_cache.py
@@ -0,0 +1,65 @@
+# Auto-generated colorbar cache
+# Generated using generate_colorbar_cache.py
+
+COLORBAR_CACHE = {
+ "oslo": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAmUlEQVQokXWRWQ7EMAxCedz/zp2PeiEZVa0QEJzEDoAwtjAYiggGAUGjASkcWVB/c4RovzkR0ByhU3Yedof201xe99Th36t/ye70DmTXaRLml/QrXWSxHNzfFNwbeaQc5B2DUUgZ1QP0+NFwTimkTapMF3k+A8EjP2exS1cVeTG6EPezmMRu09PvORwH5pR8jXt4b2Z75AYKf0CgA/rlIrMBAAAAAElFTkSuQmCC",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAmElEQVQokW2RUQ7EQAhC6f3vzNuPjoqdTRMDQo2DD2ADviperLU0UB8lYTid7qeEqq9S1RXrAGQVQIxHIKTB7xwFDbyc0kw7nmf5/xjkwWzazlnMszBB93sjlk84GeeOeYJfWWKuYR+qoNrNltRNLamrxoYuZ/YPXmoYRP6l925JQe89w38mNFaP7TWoUb32ucH1FlcCxv4BzjllJjnD7IwAAAAASUVORK5CYII=",
+ },
+ "batlow": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAArklEQVQokW2RwZEEMQjEWk0UG9rmHwj3AGzmZj+ULBqbKsPniwVggTAgvVjFCEswICFASC4uyeKVYWeuBEntf9TV4so8x8VZYTckyN3NBqWvT1ThnKrFPVLJU1deXn515ecl77DZmWZ3prbVGF3uIyNlYckwsoACBgyW3V/qYxDGFpZ5gOmAza3TMgooMMQ/EEHvFf2UYnaJ2Tdqdy0/fPxPWRxH5ls+Z/PymYrEfz0jPL9tQdmmAAAAAElFTkSuQmCC",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAArklEQVQokW2RQQ4DIQzEnP9/rS+qxLgHCLDqXkaOGciB+n6+mDIwMBgcGEjZhpZrXFzr6CG55X33VR4/vLwGhgajA+LsEI0vEBmazJHkTqLOo5DghhgxZC6wIQe0IQfokR5pSZhjtVzsMjsrIrU5lIvL06nd2dIrd/nUHsn9iNfSTk7Tsq/oBtwrZPYVRRpEis2ymAUvyWLE9e4xt7y8h/O8mFUw3Y8KQfGP57/+AEbyxodkWlsHAAAAAElFTkSuQmCC",
+ },
+ "vik": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAuUlEQVQokXWOUXIGIQyCAfcEvWHvf4ZAH4xbbf1nMuwHQVfi65siSCz9Y1uv4U35qfxut7/MMklKFKWFIiWJWnZMu2CcsOlmx2X73O3H5Nlv5r9EHNIjDi77CxjkEAZ7BIgZgBDCSggzPbARw9Xqig1XXLCTysue7FTFhWzJ0blaJ0cCO7nbrsXrJb2CnX6Su7a4m/Gy5ZWnmr3Ytue3smzK201OGX4nJwTJYW0kcNjbGb4wt2YfDH4AjnNMdUGwXfEAAAAASUVORK5CYII=",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAwUlEQVQokXWOQZbDMAxCAW3minP/a1TQha20btM8P/xBODb/8SdCBAkRUkhIEEFhjxYI4mt66RwcfQehBInayhJV1PoW1HasksRhXqzNpHCxRBbeeO4oct1X6wh11Yqczp3F/OdIVL8sVaComtfW0dnPq/USqKACNapwFmQykMEGHBrozDI66aCddjq54OG0va3T9mOP/JlM7fHq3ydfBbfTfdpz6hN8ghM7GZtt1xY4cZDEwQUJxn5o7sKf+hXmDJ8lOkiIWk7JIwAAAABJRU5ErkJggg==",
+ },
+ "roma": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAxUlEQVQokW2PQQ7DIAwEd80j+tv+nJ0eDCRpK1nWMF6I4/dLHl5V9pCrWS67XBtcl9T96C+W7tKd1za2T+Bn6n/J/ZqOP+b6UFftTO2+QNU/c+RYcrNr6MHjGNWwjxnyv+5iM67uaNBehSsuVDSrUHefHjkNOPK9T/QANGnWXEclmg1o5jIJE82058hEExIlZJtcZgEhiJAISOAJ3AGRBQoAEUGsjHamzckICOrRuX5iPY0I65Etl1+B5w7Zu4UAWTsndH0AoR4v4nBAd8IAAAAASUVORK5CYII=",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAA0ElEQVQokWWPQY4gIRDD4vCi+e18lmQPTQM9K6GScQUoGD+/Nnth2WAMGCzMWQijxejtCrBkALz8kssA0gqfLjy3rdjKXAdPBt0zPJkN/oItjBHrXzI3yMboMeM1A9kaxmhYg0uiYRkNtI3RoK+skelAVm/41li1aoVPjRUaHm5opNBJgyaNuqoy6dTL/9dmKrOHs0xmF2dzEyU9Jk3bLdsPVO27bdO2emFL9Rilra6w2otXTOueP8ndPQ+9x093sfZI0Z4kh9XZpp1q2tnO/gOZg0urikp4YQAAAABJRU5ErkJggg==",
+ },
+ "tokyo": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAoUlEQVQokW2Syw3DMAxD+eh7J+gY3X+2HiRZih0EEEjq+R++nx/IwsgCyWDxCDlD2C2MzJPhWZ08IwfhEGVDxBSZSNXVsGqbcCd6dCXuVjNHIilm0+jedVheAN6BS/Tq99JcQ/pERU5s3M/J+4DBewgg+RbQ5K4onyyf8NQpnLU+XMZhQrnUGnZFXdtie5kKqXAKhTVqi8hEgC2QHdvtX89/EjgCyIkUtH0AAAAASUVORK5CYII=",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAq0lEQVQokXWSQQ6DMAwEp/9/Y9/QD5SdHuIEJ6gCWbvLYISd1+f7FiVRsekENakkYkxKJ2bZmHh1ca0kV4WZYVaNmcBto9M5WtZloqtqVIllm0aMqOGuQlRQwikEGSSOEITs1tmq3c0KnDw7xkbWIzayC5c9XuQfwAE8+nvw/vtu6X0CbH/nPi7bfGx2JpqRr9Om4jx8qby2Z2iLnZV723PJU4x9doZHyB3+APLS0HEVvfePAAAAAElFTkSuQmCC",
+ },
+ "davos": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAApklEQVQokYWSUa7DIAwEd0Z6l+v975N+2CbQNnqRsGbXGKMY8veKRqJABAxQEpEADRbWqhIAzQCQL6iqrfxMdV9WdjZwf9MhG5dqcMkVG3I4HxySQ36bCVynf0Gy+Amyqi5IPv1/5Hn+c5e6WwKXDxeWzWmZ+eu3Y4+ix95ynoAiOIA3qz8jKi6nti9gM7eDnSey2Jl6X2rAlN9RUilSZixIVWWZzW/flBj7cleqDQAAAABJRU5ErkJggg==",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAnElEQVQokX2SQRLDMAgDlf//ebcHQ0xdTy/MSgGiTHiAxMSEqFkSNYvFDV2DkvhWQ8S1QRSXX4yWpM0FgPryAEtuvlbH/F5dmVBTaViZUimNQ05Ho0/JbGcxg/Xpnhu0zPaPtrvMlJe2E3KG+Qrs+SF/TCfnNN2VlwesW2h2s8PvGRwN3U1v5/fRhnol9Kq+rq9x6jCXW+P07yf4AfjdPHOVKVO9AAAAAElFTkSuQmCC",
+ },
+ "acton": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAi0lEQVQokYWRyREDIQwEu+fvNByH84/KD/CuhKGWD6O5isP36+NYmTtqfsPCFwldPJWhBDfSKfKM2fJnI+FiUMOQEISAOvAcoai3tKj55y+/lWkgLW56bZrTxoyTi5h5OzIvvX8t2oeq8yWIFEMhU8kbGOMhm41/re2gNTuaWf3sVFhTXD2r4RkDfgEEQgLT9KY7/QAAAABJRU5ErkJggg==",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAjUlEQVQokZWSOxJDMQgDlfvfNFXqFNpXYByDPZ5JtyAJf1+f9xeQEBKIqPiHRxZzNrg2HZFdRZ4DF1tCMRQwR78R3v2AcBu7rWUyMg2zKhzLDxAC5LgPCQXLYnB2SqlhHtmUOowUTfoxvb/6D2puMlVi5/MvWHlebQe/3Mi5f2XfPOtbtEg327Vch7j1H1G+JZaGOgELAAAAAElFTkSuQmCC",
+ },
+ "grayC": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAASUlEQVQokbWSOw4AMAhCgfsfukOTxrRWTT8MBh6MEoAkSSRlRLITngrAq2U82LUuX2FK6rFY3fg0BjDgn+T+zwonYqPr68aN4zZHpwL88xPUcgAAAABJRU5ErkJggg==",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAR0lEQVQokbWSSQoAIAwD6///nHgQROxiEM2hZKY9tpEkCSCcGYql6B5Dk8l/MTPdb7LAV11fHVE0mSy8slUOLi591recGdgB5Ri1JqWVr6UAAAAASUVORK5CYII=",
+ },
+ "navia": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAr0lEQVQokXWS0Q3EIAxD/ZwRbojbf0LuwyGlrQ6h6OE4CVWhPl9sYYGwMOS4t+/HePzXw6PE05BhsMzotA62yIqh18mA2tVRzQLUoCcosEBS9LVFoRWQxrCECOhIjfmdOmqvJjr67ynTIXd4zVpM9qqK8yqBu/+hv5Qz+r/ijjrZ+XuzHSCxHKbApsBgXK24sKGwCb9ANuWt3I5qEdWGI+IM31dgsvvd5Emdyv4++AEbhyvIyFxhOAAAAABJRU5ErkJggg==",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAr0lEQVQokXWSQY7EIBADzf9fuP/Y41I1h24IzMxGqFU2dkgkxt/vj0E1LtgTorIdNgsdQNvfEzGzgOXgPLjlXPINpqBTgKlTaUcoKTrreNck2MslNTj0Yh0YF//n9Ex05MMpTi4/xbuSoUlXjnySs1KywzFHN7v1QHrrCew3xHzfeutex/VnP6f0L1TxE1xc0DMU99M35WE6V9dKCiJKBRafPnEt0EvmkN7ye+ZhIy9nOgFitgzqrgAAAABJRU5ErkJggg==",
+ },
+ "bam": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAvUlEQVQokW2Q0W3FMAwDSXm8ztH9F6jIfkhWlNcmgnFHEXAQfsdXnHMi4tQ0xon1vAPuVZBrwbUnm/kIWzjtYU4tyEoW7/Clp8LRP7AYh0/5kEGsQnOQh8MfgCBZAAZxuYEVAiQCLCD+GQwYhDFgwKWXawTYtiFDBXKxml1JyrJTlpwqUF7O4VRr+kdqTVWSnXywJGVaD0uS5AtSSvIVe4WexC91e2084vt207fZ3bXSbffdfa/v4fVh8zv0CzRYXG5OJkEEAAAAAElFTkSuQmCC",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAvUlEQVQokW2QQY7DMAwDSarX/dn+/zcme7DsCG2DQJ6RaMcI//4hsqQSJUkUVdLlBkl8Apwdsol8pmd0suQM8x4+k5pnnjvc72pcTCWJ1aDqysEqqYr1g3k7L11lqX/DZB0Vd0X1lSlCpIjLJBqAzSQ274rmoDXA1gBbf7xJAAMJgvjWwA1xcxJ4a/INaza9tQMrsXvaMd/84AG9cYPt2X/Un03bSRynyV+8/YHed3hgr+c5uj5W2+vIWl6tb9Q4Uct7Ls3FAAAAAElFTkSuQmCC",
+ },
+ "lajolla": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAArElEQVQokXWRUY5EIQgEq5pTzP0vuh+Co+/tJMY01YgKfj6ogrQAFVmw9QW9LJ/WkaNC8N8wAxfJT60SjIrxEFfIDjOud3i4bFgaePGLVPO9MznrnWxLiUbcbvcIQ6ZB04juhYEDdtE9g6zxLCEPuIb3zV93D7+TOcZwHX+m+So7K8ctd+X+dn/EGzqPl57o6k6mRyHRkGD87kVKQwrLFMYUFpYWv3SvjM6b/AE3AwLH88Pa5AAAAABJRU5ErkJggg==",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAqklEQVQokW2RWw7DMAzDuPsfbWeKuI84TrMGKAqZVl7yx3xhYHBoCbwQiw9y12ZgTPOQyYOZfxINRURNUKJKwAmnwIggRKd4QjEsrcIuKX9a+1pC70kOz8WWxynHtq9r5IBuXTdsMt/acH7R1aIScgawQpJgKq0S0fBsOR56df0jo8jJD6hn2WZPc5cusxJq+RrpRe/Z1rtXxq+yRrEFlZk94d3ir7XhnuEPk2frRoSfIrIAAAAASUVORK5CYII=",
+ },
+ "Rainbow Desaturated": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAf0lEQVQokb2MSwrDMAxExxkV52a5/7oncSxZXfSD6rgkIVAQjydpmLQs99asNR3SfXzfpbuNCwnkG7Kc43wy/6QkaIEVWHnJT67HYl3bGl+SLAsy8eEc/Aq3PQJAAQXqP0Qb1KC2I/VAppO6uTiSTBPfIyTjGpzk1xq/ZBcevh6R9SSE4xjKlAAAAABJRU5ErkJggg==",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAgUlEQVQokb2MwRLCIAxENyHI1/X/r35GM02CB2WYYq3Ug5c3L7sLdF8Wj/AIi/AId9+d36rX2aqDdl8RqiRIgjCekpvItOTpMTIggPxDDFCDeufqY/Ib3/+xmiAFqTTeuvfwlMOrs1mBVegGtWtcL+7VoBs5mBOzDCQ6CGdI9LF9AEoJAHMyopBpAAAAAElFTkSuQmCC",
+ },
+ "Cool to Warm": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAj0lEQVQokY2PwRHEIAwDJdq6Fq7oq0y6B2BsQib57Qhhr/n5/kg0giQbFjMYbFxMzrAXRnNxHXKckPI08GZC1otm1WDK0V4fMvjhkJy/PWTw/SHxEQAkyLY7wLK8hbYgQ7I9QIYF2RIcyexfwtKX4YC5zo69h3Dvh8xZ7+y8622e/Wms28MiPx2K58XhyfkPxwMk8uPUcnsAAAAASUVORK5CYII=",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAkElEQVQokY2PQRLDIBRCgelZev9FzydZfE3QmLY75onM+/y83hBJQYREEhJEimAF4YTXa4dRi74IimKE6M9w9EdIhyv0py384kxx8dzC0V/kS2/23MKz/6h3cxgQgI1m2HCD7Z4duVXBxXtu8dGOkVxAq18t8sy3C7G8aOTCohF6z4ekxs9Dkm8Xbgf+e0jlA67XGVo/7bonAAAAAElFTkSuQmCC",
+ },
+ "Jet": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAPElEQVQokb2MwQ0AIAjEamB/Fz7EFbwHJn22XbAhHeJJi2Xp7v7DPFFQid6Z9q0kuhAUyMHyR+eiz+ieC/Oio4VU6TUjAAAAAElFTkSuQmCC",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAPElEQVQokbXNwQ0AIAwCQGo7uIOX6AqUaMLzgNhAAjXJ1Mf6fDDy1jgjG6WEUKVd8TwaaihLw9vjPM8PLpPXnpUG7pGjAAAAAElFTkSuQmCC",
+ },
+ "Yellow - Gray - Blue": {
+ "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAABAklEQVQokW2NTVJAMQiDSQKMju49lfc/iW5eCy76fqojw6RfSUrx+f6RVtGVVtmV6MTSTnZ4pzq9wy3ULgs/NdTh5mq5yZeaByKXQgE6FGCADnNgtQinOSGYC05I5oTL3OFu7g/r1oAHFOZhSihtNQNKYxgTDGMYAwyD26MBhCGBMEuzbEuz/P56m/MY4xjjmPMYY4xxzDHGGHOMueqG31WzrsDGt7llatacVbOqLl51Q3VVdfd1WbDu/SfwT6yfJdukz0z3pc/WqZerX40OkiRBnOfOeoaPJWq9OV1Sp6vTuIdXcJtfrJu18C9fg32HpP0RpV9hEttXD0vUGUaIIYTzB37sE9oBp0aLAAAAAElFTkSuQmCC",
+ "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAABCElEQVQokW2OQW7FIAxEbUM27f1P2Cu0amXPeLqA0OirFhoe9iPEPz6rqEIXVWxSZLOb7FVkd+uF15TkDTvJ1T53+ZSP+nLt1h+vHvM+PPt6NHWmbDVb+19P6mjPkTZYY/B78GetiAj3iPCIcPfwnbHPsfkoN9z9LbkfZ1v/aX9fG/c+Fo8Ym8dYvOrA2P354H9rzgNzzjHnnNecc4xrzmvOa4zr7f3LLN3SLM1SKtPKsi4TVqrLuqxLXdZpXWJalzGNKaYxDSWWUEIZIWAnINDw5BYptIGiDC202IIEGdRQl1hqiKVKoVTrBRhhhBMGesGKXnDQdtILlvCkFzzbU2tFytMjLcrjF8c3wYUVNTbdAAAAAElFTkSuQmCC",
+ },
+}
diff --git a/quickview/interface.py b/quickview/interface.py
index 2c3b9a1..c3cd8a3 100644
--- a/quickview/interface.py
+++ b/quickview/interface.py
@@ -18,6 +18,7 @@
from quickview.pipeline import EAMVisSource
+from quickview import __version__ as version
from quickview.ui.slice_selection import SliceSelection
from quickview.ui.projection_selection import ProjectionSelection
from quickview.ui.variable_selection import VariableSelection
@@ -30,6 +31,11 @@
from paraview.simple import ImportPresets, GetLookupTableNames
+from paraview.modules import vtkRemotingCore as rc
+
+rc.vtkProcessModule.GetProcessModule().UpdateProcessType(
+ rc.vtkProcessModule.PROCESS_BATCH, 0
+)
# -----------------------------------------------------------------------------
# trame setup
@@ -596,7 +602,7 @@ def ui(self) -> SinglePageWithDrawerLayout:
self._ui = SinglePageWithDrawerLayout(self.server)
with self._ui as layout:
# layout.footer.clear()
- layout.title.set_text("EAM QuickView v1.0")
+ layout.title.set_text(f"QuickView {version}")
with layout.toolbar as toolbar:
Toolbar(
diff --git a/quickview/pipeline.py b/quickview/pipeline.py
index d36ab64..2f6374e 100644
--- a/quickview/pipeline.py
+++ b/quickview/pipeline.py
@@ -219,13 +219,13 @@ def Update(self, data_file, conn_file, midpoint=0, interface=0):
self.interfaces = data_wrapped.FieldData["ilev"].tolist()
self.surface_vars = list(
- np.asarray(self.data.GetProperty("a2DVariablesInfo"))[::2]
+ np.asarray(self.data.GetProperty("SurfaceVariablesInfo"))[::2]
)
self.midpoint_vars = list(
- np.asarray(self.data.GetProperty("a3DMiddleLayerVariablesInfo"))[::2]
+ np.asarray(self.data.GetProperty("MidpointVariablesInfo"))[::2]
)
self.interface_vars = list(
- np.asarray(self.data.GetProperty("a3DInterfaceLayerVariablesInfo"))[::2]
+ np.asarray(self.data.GetProperty("InterfaceVariablesInfo"))[::2]
)
tk = GetTimeKeeper()
@@ -311,9 +311,9 @@ def Update(self, data_file, conn_file, midpoint=0, interface=0):
def LoadVariables(self, surf, mid, intf):
if not self.valid:
return
- self.data.a2DVariables = surf
- self.data.a3DMiddleLayerVariables = mid
- self.data.a3DInterfaceLayerVariables = intf
+ self.data.SurfaceVariables = surf
+ self.data.MidpointVariables = mid
+ self.data.InterfaceVariables = intf
self.vars["surface"] = surf
self.vars["midpoint"] = mid
self.vars["interface"] = intf
diff --git a/quickview/plugins/eam_projection.py b/quickview/plugins/eam_projection.py
index 048cdae..8e866b0 100644
--- a/quickview/plugins/eam_projection.py
+++ b/quickview/plugins/eam_projection.py
@@ -283,14 +283,23 @@ def RequestData(self, request, inInfo, outInfo):
x = flat[0::3] - 180.0 if self.translate else flat[0::3]
y = flat[1::3]
- latlon = Proj(init="epsg:4326")
- if self.project == 1:
- proj = Proj(proj="robin")
- elif self.project == 2:
- proj = Proj(proj="moll")
-
- xformer = Transformer.from_proj(latlon, proj)
- res = xformer.transform(x, y)
+ try:
+ # Use proj4 string for WGS84 instead of EPSG code to avoid database dependency
+ latlon = Proj(proj="latlong", datum="WGS84")
+ if self.project == 1:
+ proj = Proj(proj="robin")
+ elif self.project == 2:
+ proj = Proj(proj="moll")
+ else:
+ # Should not reach here, but return without transformation
+ return 1
+
+ xformer = Transformer.from_proj(latlon, proj, always_xy=True)
+ res = xformer.transform(x, y)
+ except Exception as e:
+ print(f"Projection error: {e}")
+ # If projection fails, return without modifying coordinates
+ return 1
flat[0::3] = np.array(res[0])
flat[1::3] = np.array(res[1])
diff --git a/quickview/plugins/eam_reader.py b/quickview/plugins/eam_reader.py
index 1c154ee..c2c13c8 100644
--- a/quickview/plugins/eam_reader.py
+++ b/quickview/plugins/eam_reader.py
@@ -138,383 +138,6 @@ def _markmodified(*args, **kwars):
return _markmodified
-@smproxy.reader(
- name="EAMSource",
- label="EAM Data Reader",
- extensions="nc",
- file_description="NETCDF files for EAM",
-)
-@smproperty.xml("""""")
-@smproperty.xml("""""")
-@smproperty.xml("""""")
-@smproperty.xml(
- """
-
-
- Specify the NetCDF data file name.
-
- """
-)
-@smproperty.xml(
- """
-
-
- Specify the NetCDF connecticity file name.
-
- """
-)
-class EAMSource(VTKPythonAlgorithmBase):
- def __init__(self):
- VTKPythonAlgorithmBase.__init__(
- self, nInputPorts=0, nOutputPorts=3, outputType="vtkUnstructuredGrid"
- )
- self._DataFileName = None
- self._ConnFileName = None
- # Variables for dimension sliders
- self._time = 0
- self._lev = 0
- self._ilev = 0
- # Arrays to store field names in netCDF file
- self._vars1D = []
- self._vars2D = []
- self._vars3Di = []
- self._vars3Dm = []
- self._timeSteps = []
-
- # vtkDataArraySelection to allow users choice for fields
- # to fetch from the netCDF data set
- self._vars1Darr = vtkDataArraySelection()
- self._vars2Darr = vtkDataArraySelection()
- self._vars3Diarr = vtkDataArraySelection()
- self._vars3Dmarr = vtkDataArraySelection()
- # Cache for non temporal variables
- # Store { names : data }
- self._vars1DCacahe = {}
- # Add observers for the selection arrays
- self._vars1Darr.AddObserver("ModifiedEvent", createModifiedCallback(self))
- self._vars2Darr.AddObserver("ModifiedEvent", createModifiedCallback(self))
- self._vars3Diarr.AddObserver("ModifiedEvent", createModifiedCallback(self))
- self._vars3Dmarr.AddObserver("ModifiedEvent", createModifiedCallback(self))
-
- # Storing Area as FieldData if available in file
- self._areavar = False
-
- # Method to clear all the variable names
- def _clear(self):
- self._vars1D.clear()
- self._vars2D.clear()
- self._vars3Di.clear()
- self._vars3Dm.clear()
-
- def _populate_variable_metadata(self):
- if self._DataFileName is None:
- return
- vardata = netCDF4.Dataset(self._DataFileName, "r")
- for name, info in vardata.variables.items():
- if "ncol_d" in info.dimensions:
- continue
- varmeta = VarMeta(name, info)
- if varmeta.type == VarType._1D:
- self._vars1D.append(varmeta)
- if name == "area":
- self._areavar = True
- elif varmeta.type == VarType._2D:
- self._vars2D.append(varmeta)
- self._vars2Darr.AddArray(name)
- elif varmeta.type == VarType._3Di:
- self._vars3Di.append(varmeta)
- self._vars3Diarr.AddArray(name)
- elif varmeta.type == VarType._3Dm:
- self._vars3Dm.append(varmeta)
- self._vars3Dmarr.AddArray(name)
- try:
- fillval = info.getncattr("_FillValue")
- varmeta.fillval = fillval
- except Exception:
- traceback.print_exc()
- pass
- self._vars2Darr.DisableAllArrays()
- self._vars3Diarr.DisableAllArrays()
- self._vars3Dmarr.DisableAllArrays()
- timesteps = vardata["time"][:].data.flatten()
- self._timeSteps.extend(timesteps)
- self.timeDim = vardata.dimensions["time"].size
- self.ilevDim = vardata.dimensions["ilev"].size
- self.levDim = vardata.dimensions["lev"].size
-
- def SetDataFileName(self, fname):
- if fname is not None and fname != "None":
- if fname != self._DataFileName:
- self._DataFileName = fname
- self._clear()
- self._populate_variable_metadata()
- self.Modified()
-
- def SetConnFileName(self, fname):
- if fname != self._ConnFileName:
- self._ConnFileName = fname
- self.Modified()
-
- @smproperty.doublevector(
- name="TimestepValues", information_only="1", si_class="vtkSITimeStepsProperty"
- )
- def GetTimestepValues(self):
- return self._timeSteps
-
- # Array selection API is typical with readers in VTK
- # This is intended to allow ability for users to choose which arrays to
- # load. To expose that in ParaView, simply use the
- # smproperty.dataarrayselection().
- # This method **must** return a `vtkDataArraySelection` instance.
- @smproperty.dataarrayselection(name="2D Variables")
- def Get2DDataArrays(self):
- return self._vars2Darr
-
- @smproperty.dataarrayselection(name="3D Middle Layer Variables")
- def Get3DmDataArrays(self):
- return self._vars3Dmarr
-
- @smproperty.dataarrayselection(name="3D Interface Layer Variables")
- def Get3DiDataArrays(self):
- return self._vars3Diarr
-
- def RequestInformation(self, request, inInfo, outInfo):
- executive = self.GetExecutive()
- for i in range(3):
- port = outInfo.GetInformationObject(i)
- port.Remove(executive.TIME_STEPS())
- port.Remove(executive.TIME_RANGE())
- if self._timeSteps is not None and len(self._timeSteps) > 0:
- for t in self._timeSteps:
- port.Append(executive.TIME_STEPS(), t)
- port.Append(executive.TIME_RANGE(), self._timeSteps[0])
- port.Append(executive.TIME_RANGE(), self._timeSteps[-1])
- return 1
-
- # TODO : implement request extents
- def RequestUpdateExtent(self, request, inInfo, outInfo):
- return super().RequestUpdateExtent(request, inInfo, outInfo)
-
- def GetTimeIndex(self, time):
- timeInd = 0
- if self._timeSteps is not None and len(self._timeSteps) > 1:
- for t in self._timeSteps[1:]:
- if time == t:
- break
- else:
- timeInd = timeInd + 1
- return timeInd
- return 0
-
- def RequestData(self, request, inInfo, outInfo):
- if (
- self._ConnFileName is None
- or self._ConnFileName == "None"
- or self._DataFileName is None
- or self._DataFileName == "None"
- ):
- print_error(
- "Either one or both, the data file or connectivity file, are not provided!"
- )
- return 0
- global _has_deps
- if not _has_deps:
- print_error("Required Python module 'netCDF4' or 'numpy' missing!")
- return 0
-
- executive = self.GetExecutive()
- from_port = request.Get(executive.FROM_OUTPUT_PORT())
- timeInfo = outInfo.GetInformationObject(from_port)
- timeInd = 0
- if timeInfo.Has(executive.UPDATE_TIME_STEP()) and len(self._timeSteps) > 0:
- time = timeInfo.Get(executive.UPDATE_TIME_STEP())
- timeInd = self.GetTimeIndex(time)
-
- meshdata = netCDF4.Dataset(self._ConnFileName, "r")
- vardata = netCDF4.Dataset(self._DataFileName, "r")
-
- lat = meshdata["cell_corner_lat"][:].data.flatten()
- lon = meshdata["cell_corner_lon"][:].data.flatten()
-
- output2D = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo, 0))
- output3Dm = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo, 1))
- output3Di = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo, 2))
-
- coords = np.empty((len(lat), 3), dtype=np.float64)
- coords[:, 0] = lon
- coords[:, 1] = lat
- coords[:, 2] = 0.0
- _coords = dsa.numpyTovtkDataArray(coords)
- vtk_coords = vtkPoints()
- vtk_coords.SetData(_coords)
- output2D.SetPoints(vtk_coords)
-
- ncells2D = meshdata["cell_corner_lat"][:].data.shape[0]
- cellTypes = np.empty(ncells2D, dtype=np.uint8)
- offsets = np.arange(0, (4 * ncells2D) + 1, 4, dtype=np.int64)
- cells = np.arange(ncells2D * 4, dtype=np.int64)
- cellTypes.fill(vtkConstants.VTK_QUAD)
- cellTypes = numpy_support.numpy_to_vtk(
- num_array=cellTypes.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_UNSIGNED_CHAR,
- )
- offsets = numpy_support.numpy_to_vtk(
- num_array=offsets.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE
- )
- cells = numpy_support.numpy_to_vtk(
- num_array=cells.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE
- )
- cellArray = vtkCellArray()
- cellArray.SetData(offsets, cells)
- output2D.VTKObject.SetCells(cellTypes, cellArray)
-
- gridAdapter2D = dsa.WrapDataObject(output2D)
- for varmeta in self._vars2D:
- if self._vars2Darr.ArrayIsEnabled(varmeta.name):
- data = vardata[varmeta.name][:].data[timeInd].flatten()
- data = np.where(data == varmeta.fillval, np.nan, data)
- gridAdapter2D.CellData.append(data, varmeta.name)
-
- lev = None
- try:
- lev = FindSpecialVariable(
- vardata, EAMConstants.LEV, EAMConstants.HYAM, EAMConstants.HYBM
- )
- if lev is not None:
- coords3Dm = np.empty((self.levDim, len(lat), 3), dtype=np.float64)
- levInd = 0
- for z in lev:
- coords = np.empty((len(lat), 3), dtype=np.float64)
- coords[:, 0] = lon
- coords[:, 1] = lat
- coords[:, 2] = z
- coords3Dm[levInd] = coords
- levInd = levInd + 1
- coords3Dm = coords3Dm.flatten().reshape(self.levDim * len(lat), 3)
- _coords = dsa.numpyTovtkDataArray(coords3Dm)
- vtk_coords = vtkPoints()
- vtk_coords.SetData(_coords)
- output3Dm.SetPoints(vtk_coords)
- cellTypesm = np.empty(ncells2D * self.levDim, dtype=np.uint8)
- offsetsm = np.arange(
- 0, (4 * ncells2D * self.levDim) + 1, 4, dtype=np.int64
- )
- cellsm = np.arange(ncells2D * self.levDim * 4, dtype=np.int64)
- cellTypesm.fill(vtkConstants.VTK_QUAD)
- cellTypesm = numpy_support.numpy_to_vtk(
- num_array=cellTypesm.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_UNSIGNED_CHAR,
- )
- offsetsm = numpy_support.numpy_to_vtk(
- num_array=offsetsm.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_ID_TYPE,
- )
- cellsm = numpy_support.numpy_to_vtk(
- num_array=cellsm.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_ID_TYPE,
- )
- cellArraym = vtkCellArray()
- cellArraym.SetData(offsetsm, cellsm)
- output3Dm.VTKObject.SetCells(cellTypesm, cellArraym)
-
- gridAdapter3Dm = dsa.WrapDataObject(output3Dm)
- for varmeta in self._vars3Dm:
- if self._vars3Dmarr.ArrayIsEnabled(varmeta.name):
- if not varmeta.transpose:
- data = vardata[varmeta.name][:].data[timeInd].flatten()
- else:
- data = (
- vardata[varmeta.name][:]
- .data[timeInd]
- .transpose()
- .flatten()
- )
- data = np.where(data == varmeta.fillval, np.nan, data)
- gridAdapter3Dm.CellData.append(data, varmeta.name)
- gridAdapter3Dm.FieldData.append(self.levDim, "numlev")
- gridAdapter3Dm.FieldData.append(lev, "lev")
- except Exception as e:
- print_error("Error occurred while processing middle layer variables :", e)
-
- ilev = None
- try:
- ilev = FindSpecialVariable(
- vardata, EAMConstants.ILEV, EAMConstants.HYAI, EAMConstants.HYBI
- )
- if ilev is not None:
- coords3Di = np.empty((self.ilevDim, len(lat), 3), dtype=np.float64)
- ilevInd = 0
- for z in ilev:
- coords = np.empty((len(lat), 3), dtype=np.float64)
- coords[:, 0] = lon
- coords[:, 1] = lat
- coords[:, 2] = z
- coords3Di[ilevInd] = coords
- ilevInd = ilevInd + 1
- coords3Di = coords3Di.flatten().reshape(self.ilevDim * len(lat), 3)
- _coords = dsa.numpyTovtkDataArray(coords3Di)
- vtk_coords = vtkPoints()
- vtk_coords.SetData(_coords)
- output3Di.SetPoints(vtk_coords)
- cellTypesi = np.empty(ncells2D * self.ilevDim, dtype=np.uint8)
- offsetsi = np.arange(
- 0, (4 * ncells2D * self.ilevDim) + 1, 4, dtype=np.int64
- )
- cellsi = np.arange(ncells2D * self.ilevDim * 4, dtype=np.int64)
- cellTypesi.fill(vtkConstants.VTK_QUAD)
- cellTypesi = numpy_support.numpy_to_vtk(
- num_array=cellTypesi.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_UNSIGNED_CHAR,
- )
- offsetsi = numpy_support.numpy_to_vtk(
- num_array=offsetsi.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_ID_TYPE,
- )
- cellsi = numpy_support.numpy_to_vtk(
- num_array=cellsi.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_ID_TYPE,
- )
- cellArrayi = vtkCellArray()
- cellArrayi.SetData(offsetsi, cellsi)
- output3Di.VTKObject.SetCells(cellTypesi, cellArrayi)
-
- gridAdapter3Di = dsa.WrapDataObject(output3Di)
- for varmeta in self._vars3Di:
- if self._vars3Diarr.ArrayIsEnabled(varmeta.name):
- if not varmeta.transpose:
- data = vardata[varmeta.name][:].data[timeInd].flatten()
- else:
- data = (
- vardata[varmeta.name][:]
- .data[timeInd]
- .transpose()
- .flatten()
- )
- data = np.where(data == varmeta.fillval, np.nan, data)
- gridAdapter3Di.CellData.append(data, varmeta.name)
- gridAdapter3Di.FieldData.append(self.ilevDim, "numilev")
- gridAdapter3Di.FieldData.append(ilev, "ilev")
- except Exception as e:
- print_error(
- "Error occurred while processing interface layer variables :", e
- )
-
- return 1
-
-
import traceback # noqa: E402
@@ -524,7 +147,7 @@ def RequestData(self, request, inInfo, outInfo):
extensions="nc",
file_description="NETCDF files for EAM",
)
-@smproperty.xml("""""")
+@smproperty.xml("""""")
@smproperty.xml(
"""
-1)
+ | (np.char.find(mdims, "ncol") > -1)
+ )[0][0]
+ ]
+ ].size
+ self._cached_ncells2D = ncells2D
+
+ # Find lat/lon dimensions
+ latdim = mvars[np.where(np.char.find(mvars, "corner_lat") > -1)][0]
+ londim = mvars[np.where(np.char.find(mvars, "corner_lon") > -1)][0]
+
+ # Build coordinates
+ lat = meshdata[latdim][:].data.flatten()
+ lon = meshdata[londim][:].data.flatten()
+
+ coords = np.empty((len(lat), 3), dtype=np.float64)
+ coords[:, 0] = lon
+ coords[:, 1] = lat
+ coords[:, 2] = 0.0
+
+ # Create VTK points
+ _coords = dsa.numpyTovtkDataArray(coords)
+ vtk_coords = vtkPoints()
+ vtk_coords.SetData(_coords)
+ self._cached_points = vtk_coords
+
+ # Build cell arrays
+ cellTypes = np.empty(ncells2D, dtype=np.uint8)
+ cellTypes.fill(vtkConstants.VTK_QUAD)
+ self._cached_cell_types = numpy_support.numpy_to_vtk(
+ num_array=cellTypes.ravel(),
+ deep=True,
+ array_type=vtkConstants.VTK_UNSIGNED_CHAR,
+ )
+
+ offsets = np.arange(0, (4 * ncells2D) + 1, 4, dtype=np.int64)
+ self._cached_offsets = numpy_support.numpy_to_vtk(
+ num_array=offsets.ravel(),
+ deep=True,
+ array_type=vtkConstants.VTK_ID_TYPE,
+ )
+
+ cells = np.arange(ncells2D * 4, dtype=np.int64)
+ self._cached_cells = numpy_support.numpy_to_vtk(
+ num_array=cells.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE
+ )
def _populate_variable_metadata(self):
if self._DataFileName is None:
return
- vardata = netCDF4.Dataset(self._DataFileName, "r")
+ vardata = self._get_var_dataset()
for name, info in vardata.variables.items():
dims = set(info.dimensions)
if not (dims == dims1 or dims == dims2 or dims == dims3m or dims == dims3i):
continue
varmeta = VarMeta(name, info)
if varmeta.type == VarType._1D:
- self._vars1D.append(varmeta)
+ self._info_vars.append(varmeta)
if "area" in name:
self._areavar = varmeta
elif varmeta.type == VarType._2D:
- self._vars2D.append(varmeta)
- self._vars2Darr.AddArray(name)
+ self._surface_vars.append(varmeta)
+ self._surface_selection.AddArray(name)
elif varmeta.type == VarType._3Dm:
- self._vars3Dm.append(varmeta)
- self._vars3Dmarr.AddArray(name)
+ self._midpoint_vars.append(varmeta)
+ self._midpoint_selection.AddArray(name)
elif varmeta.type == VarType._3Di:
- self._vars3Di.append(varmeta)
- self._vars3Diarr.AddArray(name)
+ self._interface_vars.append(varmeta)
+ self._interface_selection.AddArray(name)
try:
fillval = info.getncattr("_FillValue")
varmeta.fillval = fillval
except Exception:
pass
- self._vars2Darr.DisableAllArrays()
- self._vars3Diarr.DisableAllArrays()
- self._vars3Dmarr.DisableAllArrays()
+ self._surface_selection.DisableAllArrays()
+ self._interface_selection.DisableAllArrays()
+ self._midpoint_selection.DisableAllArrays()
timesteps = vardata["time"][:].data.flatten()
self._timeSteps.extend(timesteps)
@@ -653,10 +488,17 @@ def SetDataFileName(self, fname):
if fname != self._DataFileName:
self._DataFileName = fname
self._dirty = True
- self._2d_update = True
- self._lev_update = True
- self._ilev_update = True
+ self._surface_update = True
+ self._midpoint_update = True
+ self._interface_update = True
self._clear()
+ # Close old dataset if filename changed
+ if self._cached_var_filename != fname and self._var_dataset is not None:
+ try:
+ self._var_dataset.close()
+ except Exception:
+ pass
+ self._var_dataset = None
self._populate_variable_metadata()
self.Modified()
@@ -664,21 +506,30 @@ def SetConnFileName(self, fname):
if fname != self._ConnFileName:
self._ConnFileName = fname
self._dirty = True
- self._2d_update = True
- self._lev_update = True
- self._ilev_update = True
+ self._surface_update = True
+ self._midpoint_update = True
+ self._interface_update = True
+ # Close old dataset if filename changed
+ if self._cached_mesh_filename != fname and self._mesh_dataset is not None:
+ try:
+ self._mesh_dataset.close()
+ except Exception:
+ pass
+ self._mesh_dataset = None
+ # Clear geometry cache when connectivity file changes
+ self._clear_geometry_cache()
self.Modified()
def SetMiddleLayer(self, lev):
if self._lev != lev:
self._lev = lev
- self._lev_update = True
+ self._midpoint_update = True
self.Modified()
def SetInterfaceLayer(self, ilev):
if self._ilev != ilev:
self._ilev = ilev
- self._ilev_update = True
+ self._interface_update = True
self.Modified()
def SetCalculateAverages(self, calcavg):
@@ -697,17 +548,17 @@ def GetTimestepValues(self):
# load. To expose that in ParaView, simply use the
# smproperty.dataarrayselection().
# This method **must** return a `vtkDataArraySelection` instance.
- @smproperty.dataarrayselection(name="2D Variables")
- def Get2DDataArrays(self):
- return self._vars2Darr
+ @smproperty.dataarrayselection(name="Surface Variables")
+ def GetSurfaceVariables(self):
+ return self._surface_selection
- @smproperty.dataarrayselection(name="3D Middle Layer Variables")
- def Get3DmDataArrays(self):
- return self._vars3Dmarr
+ @smproperty.dataarrayselection(name="Midpoint Variables")
+ def GetMidpointVariables(self):
+ return self._midpoint_selection
- @smproperty.dataarrayselection(name="3D Interface Layer Variables")
- def Get3DiDataArrays(self):
- return self._vars3Diarr
+ @smproperty.dataarrayselection(name="Interface Variables")
+ def GetInterfaceVariables(self):
+ return self._interface_selection
def RequestInformation(self, request, inInfo, outInfo):
executive = self.GetExecutive()
@@ -760,96 +611,60 @@ def RequestData(self, request, inInfo, outInfo):
timeInd = self.get_time_index(outInfo, executive, from_port)
if self._time != timeInd:
self._time = timeInd
- self._2d_update = True
- self._lev_update = True
- self._ilev_update = True
+ self._surface_update = True
+ self._midpoint_update = True
+ self._interface_update = True
- meshdata = netCDF4.Dataset(self._ConnFileName, "r")
- vardata = netCDF4.Dataset(self._DataFileName, "r")
+ meshdata = self._get_mesh_dataset()
+ vardata = self._get_var_dataset()
+
+ # Build geometry if not cached
+ self._build_geometry(meshdata)
+
+ output_mesh = dsa.WrapDataObject(self._output)
- output2D = dsa.WrapDataObject(self._output)
- dims = meshdata.dimensions
- mdims = np.array(list(meshdata.dimensions.keys()))
- mvars = np.array(list(meshdata.variables.keys()))
- ncells2D = dims[
- mdims[
- np.where(
- (np.char.find(mdims, "grid_size") > -1)
- | (np.char.find(mdims, "ncol") > -1)
- )[0][0]
- ]
- ].size
if self._dirty:
self._output = vtkUnstructuredGrid()
- output2D = dsa.WrapDataObject(self._output)
-
- latdim = mvars[np.where(np.char.find(mvars, "corner_lat") > -1)][0]
- londim = mvars[np.where(np.char.find(mvars, "corner_lon") > -1)][0]
-
- lat = meshdata[latdim][:].data.flatten()
- lon = meshdata[londim][:].data.flatten()
-
- coords = np.empty((len(lat), 3), dtype=np.float64)
- coords[:, 0] = lon
- coords[:, 1] = lat
- coords[:, 2] = 0.0
- _coords = dsa.numpyTovtkDataArray(coords)
- vtk_coords = vtkPoints()
- vtk_coords.SetData(_coords)
- output2D.SetPoints(vtk_coords)
-
- cellTypes = np.empty(ncells2D, dtype=np.uint8)
- offsets = np.arange(0, (4 * ncells2D) + 1, 4, dtype=np.int64)
- cells = np.arange(ncells2D * 4, dtype=np.int64)
- cellTypes.fill(vtkConstants.VTK_QUAD)
- cellTypes = numpy_support.numpy_to_vtk(
- num_array=cellTypes.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_UNSIGNED_CHAR,
- )
- offsets = numpy_support.numpy_to_vtk(
- num_array=offsets.ravel(),
- deep=True,
- array_type=vtkConstants.VTK_ID_TYPE,
- )
- cells = numpy_support.numpy_to_vtk(
- num_array=cells.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE
- )
+ output_mesh = dsa.WrapDataObject(self._output)
+
+ # Use cached geometry
+ output_mesh.SetPoints(self._cached_points)
+
+ # Create cell array from cached data
cellArray = vtkCellArray()
- cellArray.SetData(offsets, cells)
- output2D.VTKObject.SetCells(cellTypes, cellArray)
+ cellArray.SetData(self._cached_offsets, self._cached_cells)
+ output_mesh.VTKObject.SetCells(self._cached_cell_types, cellArray)
self._dirty = False
+ # Use cached ncells2D
+ ncells2D = self._cached_ncells2D
+
# Needed to drop arrays from cached VTK Object
to_remove = set()
- last_num_arrays = output2D.CellData.GetNumberOfArrays()
+ last_num_arrays = output_mesh.CellData.GetNumberOfArrays()
for i in range(last_num_arrays):
- to_remove.add(output2D.CellData.GetArrayName(i))
+ to_remove.add(output_mesh.CellData.GetArrayName(i))
- for varmeta in self._vars2D:
- if self._vars2Darr.ArrayIsEnabled(varmeta.name):
- if output2D.CellData.HasArray(varmeta.name):
+ for varmeta in self._surface_vars:
+ if self._surface_selection.ArrayIsEnabled(varmeta.name):
+ if output_mesh.CellData.HasArray(varmeta.name):
to_remove.remove(varmeta.name)
- if not output2D.CellData.HasArray(varmeta.name) or self._2d_update:
- data = vardata[varmeta.name][:].data[timeInd].flatten()
- data = np.where(data == varmeta.fillval, np.nan, data)
- output2D.CellData.append(data, varmeta.name)
- self._2d_update = False
+ if (
+ not output_mesh.CellData.HasArray(varmeta.name)
+ or self._surface_update
+ ):
+ data = self._load_2d_variable(vardata, varmeta, timeInd)
+ output_mesh.CellData.append(data, varmeta.name)
+ self._surface_update = False
try:
lev_field_name = "lev"
- has_lev_field = output2D.FieldData.HasArray(lev_field_name)
- lev = (
- output2D.FieldData.GetArray(lev_field_name)
- if has_lev_field
- else FindSpecialVariable(
- vardata, EAMConstants.LEV, EAMConstants.HYAM, EAMConstants.HYBM
- )
- )
+ has_lev_field = output_mesh.FieldData.HasArray(lev_field_name)
+ lev = self._get_cached_lev(vardata)
if lev is not None:
if not has_lev_field:
- output2D.FieldData.append(lev, lev_field_name)
+ output_mesh.FieldData.append(lev, lev_field_name)
if self._lev >= vardata.dimensions[lev_field_name].size:
print_error(
f"User provided input for middle layer {self._lev} larger than actual data {len(lev) - 1}"
@@ -857,73 +672,49 @@ def RequestData(self, request, inInfo, outInfo):
lstart = self._lev * ncells2D
lend = lstart + ncells2D
- for varmeta in self._vars3Dm:
- if self._vars3Dmarr.ArrayIsEnabled(varmeta.name):
- if output2D.CellData.HasArray(varmeta.name):
+ for varmeta in self._midpoint_vars:
+ if self._midpoint_selection.ArrayIsEnabled(varmeta.name):
+ if output_mesh.CellData.HasArray(varmeta.name):
to_remove.remove(varmeta.name)
if (
- not output2D.CellData.HasArray(varmeta.name)
- or self._lev_update
+ not output_mesh.CellData.HasArray(varmeta.name)
+ or self._midpoint_update
):
- if not varmeta.transpose:
- data = (
- vardata[varmeta.name][:]
- .data[timeInd]
- .flatten()[lstart:lend]
- )
- else:
- data = (
- vardata[varmeta.name][:]
- .data[timeInd]
- .transpose()
- .flatten()[lstart:lend]
- )
- data = np.where(data == varmeta.fillval, np.nan, data)
- output2D.CellData.append(data, varmeta.name)
- self._lev_update = False
+ data = self._load_3d_slice(
+ vardata, varmeta, timeInd, lstart, lend
+ )
+ output_mesh.CellData.append(data, varmeta.name)
+ self._midpoint_update = False
except Exception as e:
print_error("Error occurred while processing middle layer variables :", e)
traceback.print_exc()
try:
ilev_field_name = "ilev"
- has_ilev_field = output2D.FieldData.HasArray(ilev_field_name)
- ilev = FindSpecialVariable(
- vardata, EAMConstants.ILEV, EAMConstants.HYAI, EAMConstants.HYBI
- )
+ has_ilev_field = output_mesh.FieldData.HasArray(ilev_field_name)
+ ilev = self._get_cached_ilev(vardata)
if ilev is not None:
if not has_ilev_field:
- output2D.FieldData.append(ilev, ilev_field_name)
+ output_mesh.FieldData.append(ilev, ilev_field_name)
if self._ilev >= vardata.dimensions[ilev_field_name].size:
print_error(
f"User provided input for middle layer {self._ilev} larger than actual data {len(ilev) - 1}"
)
ilstart = self._ilev * ncells2D
ilend = ilstart + ncells2D
- for varmeta in self._vars3Di:
- if self._vars3Diarr.ArrayIsEnabled(varmeta.name):
- if output2D.CellData.HasArray(varmeta.name):
+ for varmeta in self._interface_vars:
+ if self._interface_selection.ArrayIsEnabled(varmeta.name):
+ if output_mesh.CellData.HasArray(varmeta.name):
to_remove.remove(varmeta.name)
if (
- not output2D.CellData.HasArray(varmeta.name)
- or self._ilev_update
+ not output_mesh.CellData.HasArray(varmeta.name)
+ or self._interface_update
):
- if not varmeta.transpose:
- data = (
- vardata[varmeta.name][:]
- .data[timeInd]
- .flatten()[ilstart:ilend]
- )
- else:
- data = (
- vardata[varmeta.name][:]
- .data[timeInd]
- .transpose()
- .flatten()[ilstart:ilend]
- )
- data = np.where(data == varmeta.fillval, np.nan, data)
- output2D.CellData.append(data, varmeta.name)
- self._ilev_update = False
+ data = self._load_3d_slice(
+ vardata, varmeta, timeInd, ilstart, ilend
+ )
+ output_mesh.CellData.append(data, varmeta.name)
+ self._interface_update = False
except Exception as e:
print_error(
"Error occurred while processing interface layer variables :", e
@@ -931,15 +722,15 @@ def RequestData(self, request, inInfo, outInfo):
traceback.print_exc()
area_var_name = "area"
- if self._areavar and not output2D.CellData.HasArray(area_var_name):
- data = vardata[self._areavar.name][:].data.flatten()
- data = np.where(data == self._areavar.fillval, np.nan, data)
- output2D.CellData.append(data, area_var_name)
+ if self._areavar and not output_mesh.CellData.HasArray(area_var_name):
+ data = self._get_cached_area(vardata)
+ if data is not None:
+ output_mesh.CellData.append(data, area_var_name)
if area_var_name in to_remove:
to_remove.remove(area_var_name)
for var_name in to_remove:
- output2D.CellData.RemoveArray(var_name)
+ output_mesh.CellData.RemoveArray(var_name)
output = vtkUnstructuredGrid.GetData(outInfo, 0)
output.ShallowCopy(self._output)
diff --git a/quickview/utilities.py b/quickview/utilities.py
index f5914f5..f340b75 100644
--- a/quickview/utilities.py
+++ b/quickview/utilities.py
@@ -156,3 +156,29 @@ def build_colorbar_image(paraview_lut, log_scale=False, invert=False):
# Convert to image
return vtk_lut_to_image(vtk_lut)
+
+
+def get_cached_colorbar_image(colormap_name, inverted=False):
+ """
+ Get a cached colorbar image for a given colormap.
+
+ Parameters:
+ -----------
+ colormap_name : str
+ Name of the colormap (e.g., "Cool to Warm", "Rainbow Desaturated")
+ inverted : bool
+ Whether to get the inverted version
+
+ Returns:
+ --------
+ str
+ Base64-encoded PNG image as a data URI, or empty string if not found
+ """
+ # Import the cache (will be added after running generate_colorbar_cache.py)
+ from quickview.colorbar_cache import COLORBAR_CACHE
+
+ if colormap_name in COLORBAR_CACHE:
+ variant = "inverted" if inverted else "normal"
+ return COLORBAR_CACHE[colormap_name].get(variant, "")
+
+ return ""
diff --git a/quickview/view_manager.py b/quickview/view_manager.py
index dafd83e..1e6b90f 100644
--- a/quickview/view_manager.py
+++ b/quickview/view_manager.py
@@ -18,7 +18,7 @@
)
from quickview.pipeline import EAMVisSource
-from quickview.utilities import build_colorbar_image
+from quickview.utilities import get_cached_colorbar_image
from typing import Dict, List, Optional
@@ -335,8 +335,8 @@ def sync_color_config_to_state(self, index, context: ViewContext):
def generate_colorbar_image(self, 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.
+ This uses the cached colorbar images based on the colormap name
+ and invert status.
"""
if index >= len(self.state.variables):
return
@@ -346,24 +346,16 @@ def generate_colorbar_image(self, index):
if context is None:
return
- # Get the current ParaView color transfer function - already in correct state
- coltrfunc = GetColorTransferFunction(var)
-
- # Generate the colorbar image from current state
+ # Get cached colorbar image based on colormap and invert status
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,
- invert=False, # Inversion is already applied to coltrfunc
+ image_data = get_cached_colorbar_image(
+ context.config.colormap, context.config.invert_colors
)
- # Update state with the new image
+ # Update state with the cached 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}")
+ print(f"Error getting cached colorbar image for {var}: {e}")
def reset_camera(self, **kwargs):
for widget in self.widgets:
@@ -406,14 +398,29 @@ def compute_average(self, var, vtkdata=None):
data = self.source.views["atmosphere_data"]
vtkdata = sm.Fetch(data)
vardata = vtkdata.GetCellData().GetArray(var)
- area = np.array(vtkdata.GetCellData().GetArray("area"))
- if np.isnan(vardata).any():
- mask = ~np.isnan(vardata)
- if not np.any(mask):
- return np.nan # all values are NaN
- vardata = np.array(vardata)[mask]
- area = np.array(area)[mask]
- return np.average(vardata, weights=area)
+
+ # Check if area variable exists
+ area_array = vtkdata.GetCellData().GetArray("area")
+
+ if area_array is not None:
+ # Area-weighted averaging
+ area = np.array(area_array)
+ if np.isnan(vardata).any():
+ mask = ~np.isnan(vardata)
+ if not np.any(mask):
+ return np.nan # all values are NaN
+ vardata = np.array(vardata)[mask]
+ area = np.array(area)[mask]
+ return np.average(vardata, weights=area)
+ else:
+ # Simple arithmetic averaging
+ vardata = np.array(vardata)
+ if np.isnan(vardata).any():
+ mask = ~np.isnan(vardata)
+ if not np.any(mask):
+ return np.nan # all values are NaN
+ vardata = vardata[mask]
+ return np.mean(vardata)
def compute_range(self, var, vtkdata=None):
if vtkdata is None:
@@ -595,14 +602,14 @@ def update_colormap(self, index, value):
context: ViewContext = self.registry.get_view(var)
context.config.colormap = value
- # Generate new colorbar image BEFORE applying any transformations
- self.generate_colorbar_image(index)
- # Now apply the preset with current transformations
+ # Apply the preset
coltrfunc.ApplyPreset(context.config.colormap, True)
# Reapply inversion if it was enabled
if context.config.invert_colors:
coltrfunc.InvertTransferFunction()
+ # Generate new colorbar image with updated colormap
+ 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)
@@ -643,10 +650,10 @@ def update_invert_colors(self, index, value):
self.sync_color_config_to_state(index, context)
self.render_view_by_index(index)
- def update_scalar_bars(self, event):
+ def update_scalar_bars(self, event=None):
# 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():
+ for _, context in self.registry.items():
view = context.state.view_proxy
context.state.data_representation.SetScalarBarVisibility(view, False)
self.render_all_views()
@@ -662,8 +669,7 @@ def set_manual_color_range(self, index, min, max):
# 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)
+ # Note: colorbar image doesn't change with range, only data mapping changes
self.render_view_by_index(index)
def revert_to_auto_color_range(self, index):
@@ -680,8 +686,7 @@ def revert_to_auto_color_range(self, index):
context.state.data_representation.RescaleTransferFunctionToDataRange(
False, True
)
- # Generate new colorbar image with updated range
- self.generate_colorbar_image(index)
+ # Note: colorbar image doesn't change with range, only data mapping changes
self.render_all_views()
def zoom_in(self, index=0):
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..ffa92c7
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,23 @@
+# QuickView Scripts
+
+This directory contains utility scripts for maintaining and developing
+QuickView.
+
+## Scripts
+
+### generate_colorbar_cache.py
+
+Generates the colorbar image cache for all supported colormaps. This
+pre-generates base64-encoded PNG images for both normal and inverted versions of
+each colormap, improving runtime performance.
+
+**Usage:**
+
+```bash
+# Requires ParaView's pvpython
+export EAMPVIEW=/path/to/paraview/bin/pvpython
+$EAMPVIEW scripts/generate_colorbar_cache.py > quickview/colorbar_cache.py
+```
+
+This will update the `quickview/colorbar_cache.py` file with all colorbar
+images.
diff --git a/scripts/generate_colorbar_cache.py b/scripts/generate_colorbar_cache.py
new file mode 100755
index 0000000..66c8302
--- /dev/null
+++ b/scripts/generate_colorbar_cache.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+"""
+Standalone script to generate colorbar cache for QuickView.
+This script generates base64-encoded PNG images for all supported colormaps
+in both normal and inverted forms.
+
+Usage:
+ python generate_colorbar_cache.py > colorbar_cache_output.py
+"""
+
+import os
+import sys
+import xml.etree.ElementTree as ET
+
+# Add quickview to path
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from paraview.simple import GetColorTransferFunction, ImportPresets, GetLookupTableNames
+from quickview.utilities import build_colorbar_image
+
+# Define the colormaps to cache (matching interface.py)
+noncvd = [
+ {
+ "text": "Rainbow Desat.",
+ "value": "Rainbow Desaturated",
+ },
+ {
+ "text": "Cool to Warm",
+ "value": "Cool to Warm",
+ },
+ {
+ "text": "Jet",
+ "value": "Jet",
+ },
+ {
+ "text": "Yellow-Gray-Blue",
+ "value": "Yellow - Gray - Blue",
+ },
+]
+
+# CVD-friendly colormaps will be loaded from XML files
+cvd = []
+
+# Load CVD presets from XML files
+try:
+ existing = GetLookupTableNames()
+ presdir = os.path.join(os.path.dirname(__file__), "quickview", "presets")
+ presets = os.listdir(path=presdir)
+ for preset in presets:
+ prespath = os.path.abspath(os.path.join(presdir, preset))
+ if os.path.isfile(prespath):
+ name = ET.parse(prespath).getroot()[0].attrib["name"]
+ if name not in existing:
+ ImportPresets(prespath)
+ cvd.append({"text": name.title(), "value": name})
+except Exception as e:
+ print(f"# Error loading presets: {e}", file=sys.stderr)
+
+# Combine all colormaps
+all_colormaps = cvd + noncvd
+
+print("# Auto-generated colorbar cache")
+print("# Generated using generate_colorbar_cache.py")
+print()
+print("COLORBAR_CACHE = {")
+
+for colormap in all_colormaps:
+ colormap_name = colormap["value"]
+ print(f' "{colormap_name}": {{')
+
+ try:
+ # Get the color transfer function
+ lut = GetColorTransferFunction("dummy_var")
+ lut.ApplyPreset(colormap_name, True)
+
+ # Generate normal colorbar
+ normal_image = build_colorbar_image(lut, log_scale=False, invert=False)
+ print(f' "normal": "{normal_image}",')
+
+ # Invert the transfer function
+ lut.InvertTransferFunction()
+
+ # Generate inverted colorbar
+ inverted_image = build_colorbar_image(lut, log_scale=False, invert=False)
+ print(f' "inverted": "{inverted_image}",')
+
+ # Reset for next iteration
+ lut.InvertTransferFunction() # Revert back to normal
+
+ except Exception as e:
+ print(f"# Error processing {colormap_name}: {e}", file=sys.stderr)
+ print(' "normal": "",')
+ print(' "inverted": "",')
+
+ print(" },")
+
+print("}")
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 732e6f7..f53c1f7 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "app"
-version = "0.1.8"
+version = "0.1.9"
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 118e17d..2728d74 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -7,7 +7,7 @@
},
"package": {
"productName": "QuickView",
- "version": "0.1.8"
+ "version": "0.1.9"
},
"tauri": {
"allowlist": {