viewinline 0.2.3__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viewinline
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Quick look geospatial viewer for the terminal, with inline image previews
5
5
  Project-URL: Homepage, https://github.com/nkeikon/viewinline
6
6
  Project-URL: Repository, https://github.com/nkeikon/viewinline
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
  [![Python version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://pypi.org/project/viewinline/)
38
38
 
39
39
  **Quick-look geospatial viewer for compatible terminals.**
40
- Displays rasters, vectors, and tabular data directly in the terminal with no GUI and no temporary files.
40
+ Displays rasters, vectors, and tabular data directly in the terminal with no GUI.
41
41
 
42
42
  <p align="center">
43
43
  <a href="viewinline_gif1.gif"><img src="viewinline_gif1.gif" width="49%"></a>
@@ -64,6 +64,7 @@ pip install viewinline
64
64
  # Rasters
65
65
  viewinline path/to/file.tif
66
66
  viewinline R.tif G.tif B.tif # RGB composite (also works with --rgbfiles)
67
+ viewinline hyperspectral.nc --band 50
67
68
  viewinline path/to/multiband.tif --rgb 3 2 1
68
69
  viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
69
70
 
@@ -72,6 +73,7 @@ viewinline file.nc # list variables
72
73
  viewinline file.nc --subset 2 # display variable 2
73
74
  viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
74
75
  viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
76
+ viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
75
77
 
76
78
  # Vectors
77
79
  viewinline path/to/vector.geojson
@@ -113,6 +115,7 @@ Install chafa once (it's a system binary, available across all conda/virtualenv
113
115
  brew install chafa # macOS
114
116
  sudo apt install chafa # Debian/Ubuntu
115
117
  sudo dnf install chafa # Fedora
118
+ scoop install chafa # Windows
116
119
  ```
117
120
  Without chafa, terminals outside the native list above show an info message instead of an image.
118
121
  You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGINE=chafa`.
@@ -169,7 +172,9 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
169
172
 
170
173
  **NetCDF/HDF notes:**
171
174
  - viewinline lists only variables that can be displayed as 2D or 3D arrays
172
- - Variables with additional dimensions (e.g., vertical levels) may be listed but will fail to display with a clear error message
175
+ - 3D variables with time or known spatial dimensions are auto-handled (slices along the non-spatial axis)
176
+ - For 3D variables with non-standard dimensions (e.g., hyperspectral cubes like PICARD), viewinline auto-detects the band axis by smallest dimension. Use `--reduce DIM_NAME` to override.
177
+ - Variables with 4+ dimensions are not supported
173
178
  - For a complete variable list, use `ncdump -h file.nc` or `viewtif`
174
179
 
175
180
  ## Dependencies
@@ -198,7 +203,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
198
203
 
199
204
  **Note on HDF support:**
200
205
  - **HDF5** (.h5, .hdf5): Supported via rasterio if GDAL has HDF5 support (most installations)
201
- - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (common in MODIS data)
206
+ - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (the legacy format used by MODIS and older NASA products)
202
207
  - **NetCDF** (.nc): Supported via rasterio (uses GDAL's NetCDF driver)
203
208
 
204
209
  ## Available options
@@ -210,6 +215,7 @@ Raster:
210
215
  --band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
211
216
  --timestep INTEGER Alias for --band when working with NetCDF files.
212
217
  --subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
218
+ --reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
213
219
  --colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
214
220
  --rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
215
221
  --rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
@@ -4,7 +4,7 @@
4
4
  [![Python version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://pypi.org/project/viewinline/)
5
5
 
6
6
  **Quick-look geospatial viewer for compatible terminals.**
7
- Displays rasters, vectors, and tabular data directly in the terminal with no GUI and no temporary files.
7
+ Displays rasters, vectors, and tabular data directly in the terminal with no GUI.
8
8
 
9
9
  <p align="center">
10
10
  <a href="viewinline_gif1.gif"><img src="viewinline_gif1.gif" width="49%"></a>
@@ -31,6 +31,7 @@ pip install viewinline
31
31
  # Rasters
32
32
  viewinline path/to/file.tif
33
33
  viewinline R.tif G.tif B.tif # RGB composite (also works with --rgbfiles)
34
+ viewinline hyperspectral.nc --band 50
34
35
  viewinline path/to/multiband.tif --rgb 3 2 1
35
36
  viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
36
37
 
@@ -39,6 +40,7 @@ viewinline file.nc # list variables
39
40
  viewinline file.nc --subset 2 # display variable 2
40
41
  viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
41
42
  viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
43
+ viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
42
44
 
43
45
  # Vectors
44
46
  viewinline path/to/vector.geojson
@@ -80,6 +82,7 @@ Install chafa once (it's a system binary, available across all conda/virtualenv
80
82
  brew install chafa # macOS
81
83
  sudo apt install chafa # Debian/Ubuntu
82
84
  sudo dnf install chafa # Fedora
85
+ scoop install chafa # Windows
83
86
  ```
84
87
  Without chafa, terminals outside the native list above show an info message instead of an image.
85
88
  You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGINE=chafa`.
@@ -136,7 +139,9 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
136
139
 
137
140
  **NetCDF/HDF notes:**
138
141
  - viewinline lists only variables that can be displayed as 2D or 3D arrays
139
- - Variables with additional dimensions (e.g., vertical levels) may be listed but will fail to display with a clear error message
142
+ - 3D variables with time or known spatial dimensions are auto-handled (slices along the non-spatial axis)
143
+ - For 3D variables with non-standard dimensions (e.g., hyperspectral cubes like PICARD), viewinline auto-detects the band axis by smallest dimension. Use `--reduce DIM_NAME` to override.
144
+ - Variables with 4+ dimensions are not supported
140
145
  - For a complete variable list, use `ncdump -h file.nc` or `viewtif`
141
146
 
142
147
  ## Dependencies
@@ -165,7 +170,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
165
170
 
166
171
  **Note on HDF support:**
167
172
  - **HDF5** (.h5, .hdf5): Supported via rasterio if GDAL has HDF5 support (most installations)
168
- - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (common in MODIS data)
173
+ - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (the legacy format used by MODIS and older NASA products)
169
174
  - **NetCDF** (.nc): Supported via rasterio (uses GDAL's NetCDF driver)
170
175
 
171
176
  ## Available options
@@ -177,6 +182,7 @@ Raster:
177
182
  --band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
178
183
  --timestep INTEGER Alias for --band when working with NetCDF files.
179
184
  --subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
185
+ --reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
180
186
  --colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
181
187
  --rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
182
188
  --rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "viewinline"
7
- version = "0.2.3"
7
+ version = "0.3.0"
8
8
  description = "Quick look geospatial viewer for the terminal, with inline image previews"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -28,12 +28,19 @@ from matplotlib import colormaps
28
28
  import matplotlib as mpl
29
29
  import subprocess
30
30
 
31
+ try:
32
+ import netCDF4
33
+ HAS_NETCDF4 = True
34
+ except ImportError:
35
+ HAS_NETCDF4 = False
36
+
31
37
  import warnings
32
38
 
33
39
  warnings.filterwarnings("ignore", message="More than one layer found", category=UserWarning)
34
40
  warnings.filterwarnings("ignore", message="Dataset has no geotransform", category=UserWarning)
41
+ warnings.filterwarnings("ignore", message="invalid scale_factor or add_offset attribute", category=UserWarning)
35
42
 
36
- __version__ = "0.2.3"
43
+ __version__ = "0.3.0"
37
44
 
38
45
  AVAILABLE_COLORMAPS = [
39
46
  "viridis", "inferno", "magma", "plasma",
@@ -184,14 +191,7 @@ def show_inline_image(image_array: np.ndarray, display_scale = None, is_vector:
184
191
 
185
192
  if _TERMINAL_SUPPORTS_IMAGES:
186
193
  sys.stdout.write(f"\033]1337;File=inline=1;width={width_pct}%:{encoded}\a\n")
187
- else:
188
- # if is_chafa_available():
189
- # chafa_output = subprocess.check_output(
190
- # ["chafa", "-"],
191
- # input=image_bytes
192
- # ).decode()
193
-
194
- # sys.stdout.write(f"\n{chafa_output}\a\n")
194
+ else:
195
195
  if is_chafa_available():
196
196
  # Inside tmux, force chafa to use block-art symbols instead of
197
197
  # graphics protocols. Tmux mangles kitty graphics and sixel
@@ -227,9 +227,9 @@ def show_image_auto(img: np.ndarray, display_scale=None, is_vector: bool = False
227
227
  try:
228
228
  show_inline_image(img, display_scale, is_vector)
229
229
  if _TERMINAL_SUPPORTS_IMAGES:
230
- print("[VIEW] Image rendered")
230
+ print("[VIEW] Inline render complete")
231
231
  elif is_chafa_available():
232
- print("[VIEW] Rendered via chafa")
232
+ print("[VIEW] Inline render complete via chafa")
233
233
  # If neither path applies, show_inline_image already printed the info message
234
234
  except Exception as e:
235
235
  print(f"[ERROR] Failed to render image: {e}")
@@ -262,7 +262,6 @@ def load_csv_to_df(path: str) -> pd.DataFrame:
262
262
  print(f"[ERROR] Failed to read CSV: {e}")
263
263
  return pd.DataFrame()
264
264
 
265
-
266
265
  # =============================================================
267
266
  # Preview
268
267
  # =============================================================
@@ -643,6 +642,177 @@ def render_simple_image(filepath: str, args) -> None:
643
642
  except Exception as e:
644
643
  print(f"[ERROR] Failed to load image: {e}")
645
644
 
645
+ def render_netcdf_via_netcdf4(path, args):
646
+ """Read a NetCDF file via netCDF4 (bypassing GDAL). Handles hierarchical
647
+ groups and hyperspectral cubes where GDAL aborts or interprets axes wrong.
648
+ """
649
+ if not HAS_NETCDF4:
650
+ print("[ERROR] netCDF4 not installed. Install with:")
651
+ print(" pip install netCDF4")
652
+ print(" or: pip install viewinline[netcdf]")
653
+ return
654
+
655
+ try:
656
+ nc = netCDF4.Dataset(path)
657
+ except Exception as e:
658
+ print(f"[ERROR] Could not open NetCDF file: {e}")
659
+ return
660
+
661
+ # Recursively collect (path, variable) pairs across all groups
662
+ def collect_vars(group, prefix=""):
663
+ out = []
664
+ for name, var in group.variables.items():
665
+ full_name = f"{prefix}{name}"
666
+ out.append((full_name, var))
667
+ for sub_name, sub in group.groups.items():
668
+ out.extend(collect_vars(sub, f"{prefix}{sub_name}/"))
669
+ return out
670
+
671
+ all_vars = collect_vars(nc)
672
+
673
+ if not all_vars:
674
+ print("[ERROR] No variables found in file.")
675
+ nc.close()
676
+ return
677
+
678
+ # If no --subset, list all variables and exit
679
+ if not args.subset:
680
+ print(f"Found {len(all_vars)} variables in {os.path.basename(path)}:")
681
+ for i, (name, var) in enumerate(all_vars, 1):
682
+ shape_str = "x".join(str(s) for s in var.shape)
683
+ print(f" [{i}] {name} ({shape_str}, {var.dtype})")
684
+ print(f"\nUse --subset <N> to display a specific variable.")
685
+ nc.close()
686
+ return
687
+
688
+ # Validate --subset
689
+ if args.subset < 1 or args.subset > len(all_vars):
690
+ print(f"[ERROR] --subset must be between 1 and {len(all_vars)}")
691
+ nc.close()
692
+ return
693
+
694
+ var_name, var = all_vars[args.subset - 1]
695
+ print(f"[INFO] Displaying variable {args.subset}: {var_name}")
696
+ print(f"[DATA] Shape: {var.shape} dtype: {var.dtype} dims: {var.dimensions}")
697
+
698
+ # Detect dimensionality and read the right slice
699
+ if var.ndim == 2:
700
+ data = np.asarray(var[:, :], dtype=np.float64)
701
+ slice_info = "2D variable"
702
+
703
+ elif var.ndim == 3:
704
+ spatial_dims = {'lat', 'lon', 'latitude', 'longitude', 'y', 'x'}
705
+
706
+ spectral_axis = None
707
+
708
+ # 1. User override via --reduce
709
+ if args.reduce_dim is not None:
710
+ if args.reduce_dim in var.dimensions:
711
+ spectral_axis = list(var.dimensions).index(args.reduce_dim)
712
+ print(f"[INFO] Using user-specified --reduce '{args.reduce_dim}'")
713
+ else:
714
+ print(f"[ERROR] --reduce '{args.reduce_dim}' is not a dimension of this variable.")
715
+ print(f"[INFO] Available dimensions: {list(var.dimensions)}")
716
+ nc.close()
717
+ return
718
+
719
+ # 2. Standard convention: reduce along the non-spatial dim
720
+ if spectral_axis is None:
721
+ has_standard_spatial = any(d in spatial_dims for d in var.dimensions)
722
+ if has_standard_spatial:
723
+ for i, dim_name in enumerate(var.dimensions):
724
+ if dim_name not in spatial_dims:
725
+ spectral_axis = i
726
+ break
727
+
728
+ # 3. Fallback heuristic: smallest dim is typically the band axis
729
+ if spectral_axis is None:
730
+ sizes = [(i, var.shape[i]) for i in range(3)]
731
+ spectral_axis = min(sizes, key=lambda x: x[1])[0]
732
+ print(f"[INFO] Non-standard dimensions detected: {list(var.dimensions)}")
733
+ print(f"[INFO] Reducing along '{var.dimensions[spectral_axis]}' (size {var.shape[spectral_axis]}, assumed band/spectral axis)")
734
+ print(f"[INFO] If this is not correct, use --reduce DIM_NAME to override.")
735
+
736
+ # Slice along chosen axis
737
+ band_count = var.shape[spectral_axis]
738
+ band_num = args.band if args.band is not None else 1
739
+ band_idx = max(0, min(band_num - 1, band_count - 1))
740
+ slicer = [slice(None)] * 3
741
+ slicer[spectral_axis] = band_idx
742
+ data = np.asarray(var[tuple(slicer)], dtype=np.float64)
743
+ slice_info = f"slice along axis {spectral_axis} ({var.dimensions[spectral_axis]}), band {band_idx + 1} of {band_count}"
744
+
745
+ else:
746
+ print(f"[ERROR] viewinline only supports 2D or 3D variables. This one is {var.ndim}D.")
747
+ nc.close()
748
+ return
749
+
750
+ print(f"[DATA] {slice_info}")
751
+ # Apply fill value
752
+ fill = getattr(var, '_FillValue', None)
753
+ if fill is not None:
754
+ data = np.where(data == fill, np.nan, data)
755
+ # Flip vertically if data is stored south-to-north so north appears at top.
756
+ # Determine which dims remain after slicing — for the 2D result, figure out
757
+ # which axis (0 or 1) corresponds to latitude, and check that dim's coord values.
758
+ if var.ndim == 2:
759
+ remaining_dims = list(var.dimensions)
760
+ elif var.ndim == 3:
761
+ if spectral_axis is not None:
762
+ remaining_dims = [d for i, d in enumerate(var.dimensions) if i != spectral_axis]
763
+ else:
764
+ remaining_dims = list(var.dimensions[1:]) # axis 0 was reduced
765
+ else:
766
+ remaining_dims = []
767
+ lat_names = {'lat', 'latitude', 'y'}
768
+ for axis_in_2d, dim_name in enumerate(remaining_dims):
769
+ if dim_name in lat_names and dim_name in nc.variables:
770
+ lat_vals = nc[dim_name][:]
771
+ if len(lat_vals) > 1 and lat_vals[0] < lat_vals[-1]:
772
+ data = np.flip(data, axis=axis_in_2d)
773
+ print(f"[INFO] Flipped along '{dim_name}' for display (data stored south-to-north).")
774
+ break
775
+ nc.close()
776
+
777
+ # Normalize and display
778
+ band_u8 = normalize_to_uint8(data, vmin=args.vmin, vmax=args.vmax,
779
+ nodata=args.nodata)
780
+
781
+ if args.colormap:
782
+ cmap = colormaps[args.colormap]
783
+ colored = cmap(band_u8 / 255.0)
784
+ img = (colored[:, :, :3] * 255).astype(np.uint8)
785
+ print(f"[INFO] Applying colormap: {args.colormap}")
786
+ else:
787
+ img = np.stack([band_u8] * 3, axis=-1)
788
+ print("[INFO] Displaying grayscale")
789
+
790
+ # Resize to terminal
791
+ H, W = img.shape[:2]
792
+ if args.display:
793
+ new_w, new_h = max(1, int(W * args.display)), max(1, int(H * args.display))
794
+ img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
795
+ print(f"[VIEW] Manual resize ×{args.display:.2f} → {new_w}×{new_h}px")
796
+ # else:
797
+ # img, scale = resize_to_terminal(img)
798
+ # print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={scale:.2f})")
799
+ else:
800
+ max_dim = 2000
801
+ if max(img.shape[:2]) > max_dim:
802
+ scale = max_dim / max(img.shape[:2])
803
+ new_w = int(img.shape[1] * scale)
804
+ new_h = int(img.shape[0] * scale)
805
+ img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
806
+ print(f"[VIEW] Downsampled from {W}×{H}px to {new_w}×{new_h}px (scale={scale:.2f})")
807
+ print(f"[INFO] Use --display 1 for full resolution.")
808
+ else:
809
+ # (matches the width_pct logic in show_inline_image)
810
+ display_pct = args.display if args.display is not None else 0.33
811
+
812
+ print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={display_pct:.2f})")
813
+
814
+ show_image_auto(img, getattr(args, "display", None), is_vector=False)
815
+
646
816
  def render_raster(paths: list[str], args) -> None:
647
817
  try:
648
818
  import rasterio
@@ -655,8 +825,13 @@ def render_raster(paths: list[str], args) -> None:
655
825
 
656
826
  if len(paths) == 1:
657
827
  path = paths[0]
828
+
829
+ if path.lower().endswith('.nc'):
830
+ render_netcdf_via_netcdf4(path, args)
831
+ return
658
832
 
659
833
  # Handle NetCDF/HDF with subdatasets
834
+
660
835
  if path.lower().endswith(('.nc', '.hdf', '.hdf5', '.h5')):
661
836
  try:
662
837
 
@@ -732,7 +907,7 @@ def render_raster(paths: list[str], args) -> None:
732
907
  resampling=rasterio.enums.Resampling.bilinear
733
908
  )
734
909
 
735
- print(f"[PROC] Downsampled for preview → {out_w}×{out_h}px (scale={scale:.3f})")
910
+ print(f"[VIEW] Downsampled for preview → {out_w}×{out_h}px (scale={scale:.3f})")
736
911
  else:
737
912
  data = ds.read()
738
913
 
@@ -744,8 +919,11 @@ def render_raster(paths: list[str], args) -> None:
744
919
  print(f"[INFO] Multi-band raster detected ({band_count} bands)")
745
920
 
746
921
  # MULTI BAND RGB (skip for NetCDF - treat as slices/timesteps, not RGB)
747
- if band_count >= 3 and not paths[0].lower().endswith('.nc'):
748
-
922
+ # if band_count >= 3 and not paths[0].lower().endswith('.nc'):
923
+ # Auto-composite to RGB only when user didn't explicitly ask for a single band
924
+ # user_specified_band = args.band is not None and args.band != 1
925
+ user_specified_band = args.band is not None
926
+ if band_count >= 3 and not paths[0].lower().endswith('.nc') and not user_specified_band:
749
927
 
750
928
  if getattr(args, "rgb", None):
751
929
  try:
@@ -774,7 +952,8 @@ def render_raster(paths: list[str], args) -> None:
774
952
  # SINGLE BAND
775
953
  else:
776
954
 
777
- band_idx = max(0, min(args.band - 1, band_count - 1))
955
+ band_num = args.band if args.band is not None else 1
956
+ band_idx = max(0, min(band_num - 1, band_count - 1))
778
957
  # print(f"[INFO] Displaying band {band_idx + 1} of {band_count}")
779
958
  raw_band = data[band_idx].astype(float)
780
959
 
@@ -851,7 +1030,7 @@ def render_raster(paths: list[str], args) -> None:
851
1030
  print(f"[ERROR] Cannot display this variable.")
852
1031
  print("[INFO] viewinline only supports 2D or 3D NetCDF variables")
853
1032
  else:
854
- print(f"[ERROR] Raster rendering failed: {e}")
1033
+ print(f"[ERROR] Inline render failed: {e}")
855
1034
 
856
1035
 
857
1036
  def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector=False) -> None:
@@ -956,13 +1135,6 @@ def render_vector(path, args):
956
1135
  except Exception as e:
957
1136
  print(f"[WARN] Could not list layers: {e}")
958
1137
 
959
- # try:
960
- # gdf = gpd.read_file(path, layer=getattr(args, "layer", None))
961
- # print(f"[DATA] Vector loaded: {os.path.basename(path)} ({len(gdf)} features)")
962
- # except Exception as e:
963
- # print(f"[ERROR] Failed to read vector: {e}")
964
- # return
965
-
966
1138
  try:
967
1139
  # Use read_parquet for parquet/geoparquet files
968
1140
  if path.lower().endswith(('.parquet', '.geoparquet')):
@@ -1304,7 +1476,7 @@ def main() -> None:
1304
1476
 
1305
1477
  # Raster options
1306
1478
  parser.add_argument(
1307
- "--band", type=int, default=1,
1479
+ "--band", type=int, default=None,
1308
1480
  help="Band number to display (single raster case), or slice number for NetCDF."
1309
1481
  )
1310
1482
  parser.add_argument(
@@ -1320,6 +1492,10 @@ def main() -> None:
1320
1492
  "--rgb", nargs=3, type=int, metavar=('R', 'G', 'B'), default=None,
1321
1493
  help="Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3."
1322
1494
  )
1495
+ parser.add_argument(
1496
+ "--rgbfiles", nargs=3, type=str, metavar=('R', 'G', 'B'),
1497
+ help="Three single-band rasters for RGB composite (e.g., --rgbfiles R.tif G.tif B.tif). Can also provide as positional arguments without the flag."
1498
+ )
1323
1499
  parser.add_argument(
1324
1500
  "--vmin", type=float, default=None,
1325
1501
  help="Minimum pixel value for raster display scaling."
@@ -1341,8 +1517,9 @@ def main() -> None:
1341
1517
  help="Variable index for NetCDF files (e.g. --subset 1)."
1342
1518
  )
1343
1519
  parser.add_argument(
1344
- "--rgbfiles", nargs=3, type=str, metavar=('R', 'G', 'B'),
1345
- help="Three single-band rasters for RGB composite (e.g., --rgbfiles R.tif G.tif B.tif). Can also provide as positional arguments without the flag."
1520
+ "--reduce", dest="reduce_dim", type=str, default=None,
1521
+ metavar="DIM_NAME",
1522
+ help="For 3D NetCDF variables, specify which dimension to use as the band axis (auto-detected if omitted)."
1346
1523
  )
1347
1524
 
1348
1525
  # CSV options
@@ -1426,7 +1603,7 @@ def main() -> None:
1426
1603
  parser.add_argument(
1427
1604
  "--table", action="store_true",
1428
1605
  help="Display vector/parquet file as tabular data instead of rendering geometry."
1429
- )
1606
+ )
1430
1607
 
1431
1608
  parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
1432
1609
 
File without changes
File without changes