rsplot 0.2.4__tar.gz → 0.2.5__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.
Files changed (33) hide show
  1. {rsplot-0.2.4 → rsplot-0.2.5}/PKG-INFO +49 -4
  2. {rsplot-0.2.4 → rsplot-0.2.5}/README.md +48 -3
  3. {rsplot-0.2.4 → rsplot-0.2.5}/pyproject.toml +1 -1
  4. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/cli.py +5 -3
  5. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/config.py +100 -32
  6. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/geo/boundaries.py +6 -6
  7. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/plotting/colormaps.py +9 -10
  8. rsplot-0.2.5/src/rsplot/plotting/meteo.py +422 -0
  9. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/plotting/overlay.py +23 -8
  10. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/plotting/raster.py +2 -1
  11. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/plotting/styles.py +9 -2
  12. rsplot-0.2.5/src/rsplot/readers/era5.py +477 -0
  13. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/readers/guokong.py +7 -7
  14. {rsplot-0.2.4 → rsplot-0.2.5}/.gitignore +0 -0
  15. {rsplot-0.2.4 → rsplot-0.2.5}/LICENSE +0 -0
  16. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/__init__.py +0 -0
  17. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/__main__.py +0 -0
  18. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/fnr.py +0 -0
  19. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/geo/__init__.py +0 -0
  20. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/geo/gridding.py +0 -0
  21. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/plotting/__init__.py +0 -0
  22. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/plotting/fnr.py +0 -0
  23. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/plotting/station.py +0 -0
  24. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/readers/__init__.py +0 -0
  25. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/readers/base.py +0 -0
  26. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/readers/tropomi_hcho.py +0 -0
  27. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/readers/tropomi_no2.py +0 -0
  28. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/readers/tropomi_o3.py +0 -0
  29. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/readers/tropomi_o3pr.py +0 -0
  30. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/recent.py +0 -0
  31. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/results.py +0 -0
  32. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/tiles/__init__.py +0 -0
  33. {rsplot-0.2.4 → rsplot-0.2.5}/src/rsplot/tiles/tianditu.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rsplot
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: CLI tool for plotting Remote Sensing data, designed for Agent.
5
5
  Project-URL: Repository, https://git.lug.ustc.edu.cn/yaoyhu/rsplot
6
6
  Project-URL: GitHub, https://github.com/yaoyhu/rsplot
@@ -258,16 +258,61 @@ uv run rsplot --help
258
258
 
259
259
  ## Configuration
260
260
 
261
- For satellite basemaps, set a TianDiTu API key:
261
+ `rsplot` does not ship site-specific data paths. Configure local satellite,
262
+ station, boundary, and font paths with a TOML file:
263
+
264
+ ```bash
265
+ mkdir -p ~/.config/rsplot
266
+ cp config.example.toml ~/.config/rsplot/config.toml
267
+ ```
268
+
269
+ Then edit `~/.config/rsplot/config.toml`:
270
+
271
+ ```toml
272
+ [paths]
273
+ geojson_dir = "/path/to/geojson"
274
+ font_dir = "/path/to/fonts"
275
+ # hcho_cmap_file = "/path/to/256x3_rgb_table.txt"
276
+
277
+ [paths.data_dirs]
278
+ no2 = "/path/to/NO2"
279
+ o3 = "/path/to/O3"
280
+ o3pr = "/path/to/O3PR"
281
+ hcho = "/path/to/HCHO"
282
+ guokong = "/path/to/guokong_combine"
283
+
284
+ # Optional satellite basemap key. Leave commented unless you have a real key.
285
+ # [tianditu]
286
+ # api_key = "your-real-key"
287
+ ```
288
+
289
+ You can also point to a different config file:
290
+
291
+ ```bash
292
+ export RSPLOT_CONFIG="/path/to/rsplot.toml"
293
+ ```
294
+
295
+ Environment variables override TOML values:
262
296
 
263
297
  ```bash
264
298
  export TIANDITU_API_KEY="your-key"
299
+ export RSPLOT_GEOJSON_DIR="/path/to/geojson"
300
+ export RSPLOT_FONT_DIR="/path/to/fonts"
301
+ export RSPLOT_DATA_DIR_NO2="/path/to/NO2"
302
+ export RSPLOT_DATA_DIR_O3="/path/to/O3"
303
+ export RSPLOT_DATA_DIR_O3PR="/path/to/O3PR"
304
+ export RSPLOT_DATA_DIR_HCHO="/path/to/HCHO"
305
+ export RSPLOT_DATA_DIR_GUOKONG="/path/to/guokong_combine"
306
+ export RSPLOT_HCHO_CMAP_FILE="/path/to/256x3_rgb_table.txt"
265
307
  ```
266
308
 
309
+ Command-line directory options, such as `--data-dir`, take precedence for the
310
+ command being run. If no TianDiTu API key is configured, use `--basemap none`
311
+ to avoid requesting satellite tiles.
312
+
267
313
  ## Usage
268
314
 
269
- The examples below assume the command is running on a USTC server or in an
270
- environment with equivalent data paths configured.
315
+ The examples below assume data paths are configured and readable.
271
316
 
272
317
  ```bash
273
318
  # Show available commands and options.
@@ -31,16 +31,61 @@ uv run rsplot --help
31
31
 
32
32
  ## Configuration
33
33
 
34
- For satellite basemaps, set a TianDiTu API key:
34
+ `rsplot` does not ship site-specific data paths. Configure local satellite,
35
+ station, boundary, and font paths with a TOML file:
36
+
37
+ ```bash
38
+ mkdir -p ~/.config/rsplot
39
+ cp config.example.toml ~/.config/rsplot/config.toml
40
+ ```
41
+
42
+ Then edit `~/.config/rsplot/config.toml`:
43
+
44
+ ```toml
45
+ [paths]
46
+ geojson_dir = "/path/to/geojson"
47
+ font_dir = "/path/to/fonts"
48
+ # hcho_cmap_file = "/path/to/256x3_rgb_table.txt"
49
+
50
+ [paths.data_dirs]
51
+ no2 = "/path/to/NO2"
52
+ o3 = "/path/to/O3"
53
+ o3pr = "/path/to/O3PR"
54
+ hcho = "/path/to/HCHO"
55
+ guokong = "/path/to/guokong_combine"
56
+
57
+ # Optional satellite basemap key. Leave commented unless you have a real key.
58
+ # [tianditu]
59
+ # api_key = "your-real-key"
60
+ ```
61
+
62
+ You can also point to a different config file:
63
+
64
+ ```bash
65
+ export RSPLOT_CONFIG="/path/to/rsplot.toml"
66
+ ```
67
+
68
+ Environment variables override TOML values:
35
69
 
36
70
  ```bash
37
71
  export TIANDITU_API_KEY="your-key"
72
+ export RSPLOT_GEOJSON_DIR="/path/to/geojson"
73
+ export RSPLOT_FONT_DIR="/path/to/fonts"
74
+ export RSPLOT_DATA_DIR_NO2="/path/to/NO2"
75
+ export RSPLOT_DATA_DIR_O3="/path/to/O3"
76
+ export RSPLOT_DATA_DIR_O3PR="/path/to/O3PR"
77
+ export RSPLOT_DATA_DIR_HCHO="/path/to/HCHO"
78
+ export RSPLOT_DATA_DIR_GUOKONG="/path/to/guokong_combine"
79
+ export RSPLOT_HCHO_CMAP_FILE="/path/to/256x3_rgb_table.txt"
38
80
  ```
39
81
 
82
+ Command-line directory options, such as `--data-dir`, take precedence for the
83
+ command being run. If no TianDiTu API key is configured, use `--basemap none`
84
+ to avoid requesting satellite tiles.
85
+
40
86
  ## Usage
41
87
 
42
- The examples below assume the command is running on a USTC server or in an
43
- environment with equivalent data paths configured.
88
+ The examples below assume data paths are configured and readable.
44
89
 
45
90
  ```bash
46
91
  # Show available commands and options.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rsplot"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "CLI tool for plotting Remote Sensing data, designed for Agent."
5
5
  authors = [
6
6
  { name = "Yaoyao Hu", email = "yaoyhu@mail.ustc.edu.cn" },
@@ -413,6 +413,7 @@ def raster(
413
413
  vmax=use_vmax,
414
414
  basemap=use_basemap,
415
415
  tianditu_key=cfg.tianditu_key,
416
+ hcho_cmap_file=cfg.hcho_cmap_file,
416
417
  title=title,
417
418
  output=output,
418
419
  colorbar_label=colorbar_label,
@@ -589,7 +590,7 @@ def station(
589
590
  datetime_str=resolved_dt,
590
591
  threshold=threshold,
591
592
  unit=unit,
592
- cities_gdf=_load_city(cfg.geojson_dir),
593
+ cities_gdf=_load_city(cfg.require_geojson_dir()),
593
594
  output=output,
594
595
  )
595
596
  emit_result(result, output)
@@ -867,6 +868,7 @@ def overlay(
867
868
  dpi=use_dpi,
868
869
  basemap=use_basemap,
869
870
  tianditu_key=cfg.tianditu_key,
871
+ hcho_cmap_file=cfg.hcho_cmap_file,
870
872
  title=title,
871
873
  output=output,
872
874
  )
@@ -895,7 +897,7 @@ def overlay(
895
897
  datetime_str=resolved_dt,
896
898
  threshold=threshold,
897
899
  unit=unit,
898
- cities_gdf=_load_city(cfg.geojson_dir),
900
+ cities_gdf=_load_city(cfg.require_geojson_dir()),
899
901
  output=output,
900
902
  )
901
903
  emit_result(result, output)
@@ -1255,7 +1257,7 @@ def fnr(
1255
1257
  if raster_dir_hcho is not None
1256
1258
  else cfg.get_data_dir("hcho")
1257
1259
  )
1258
- console.print(f"\n[bold]读取数据:[/bold]")
1260
+ console.print("\n[bold]读取数据:[/bold]")
1259
1261
  console.print(f" NO2 : {use_no2_dir}")
1260
1262
  console.print(f" HCHO: {use_hcho_dir}")
1261
1263
 
@@ -1,7 +1,8 @@
1
1
  """Configuration management for rsplot.
2
2
 
3
- # TODO: Loads defaults from ~/.config/rsplot/config.toml if present,
4
- CLI arguments override everything.
3
+ Runtime paths are intentionally configured outside the package. The loader
4
+ merges safe built-in defaults, TOML config files, environment variables, and
5
+ finally CLI overrides handled by command functions.
5
6
  """
6
7
 
7
8
  from __future__ import annotations
@@ -19,21 +20,20 @@ else:
19
20
 
20
21
 
21
22
  # ---------------------------------------------------------------------------
22
- # Default paths
23
+ # Safe built-in path defaults
23
24
  # ---------------------------------------------------------------------------
24
- DEFAULT_DATA_DIRS: dict[str, str] = {
25
- "no2": "/satellite/exports2/yaoyhu/NO2",
26
- "o3": "/satellite/exports2/yaoyhu/O3",
27
- "o3pr": "/satellite/exports2/yaoyhu/O3PR",
28
- "hcho": "/satellite/exports2/yaoyhu/HCHO",
29
- "guokong": "/home/zzgsg/data/guokong_combine",
30
- }
31
- DEFAULT_GEOJSON_DIR = "/satellite/d4/yaoyhu/tiandi"
32
- DEFAULT_FONT_DIR = "/satellite/d4/yaoyhu/fonts"
25
+ KNOWN_DATA_PRODUCTS = ("no2", "o3", "o3pr", "hcho", "guokong")
26
+
27
+ DEFAULT_DATA_DIRS: dict[str, str] = {}
28
+ DEFAULT_GEOJSON_DIR = None
29
+ DEFAULT_FONT_DIR = None
30
+ DEFAULT_HCHO_CMAP_FILE = None
33
31
  DEFAULT_TIANDITU_KEY = None
34
32
 
35
33
 
36
34
  CONFIG_PATH = Path.home() / ".config" / "rsplot" / "config.toml"
35
+ CONFIG_ENV_VAR = "RSPLOT_CONFIG"
36
+ HCHO_CMAP_FILE_ENV = "RSPLOT_HCHO_CMAP_FILE"
37
37
 
38
38
 
39
39
  # ---------------------------------------------------------------------------
@@ -146,15 +146,17 @@ DEFAULT_KEY_REGIONS: dict[str, list[str]] = {
146
146
  # ---------------------------------------------------------------------------
147
147
  @dataclass
148
148
  class AppConfig:
149
- """Merged configuration (file defaults + CLI overrides)."""
149
+ """Merged configuration from files and environment variables."""
150
150
 
151
151
  # paths (per-product data directories)
152
152
  data_dirs: dict[str, str] = field(
153
153
  default_factory=lambda: dict(DEFAULT_DATA_DIRS)
154
154
  )
155
- geojson_dir: str = DEFAULT_GEOJSON_DIR
156
- font_dir: str = DEFAULT_FONT_DIR
155
+ geojson_dir: str | None = DEFAULT_GEOJSON_DIR
156
+ font_dir: str | None = DEFAULT_FONT_DIR
157
+ hcho_cmap_file: str | None = DEFAULT_HCHO_CMAP_FILE
157
158
  tianditu_key: str | None = DEFAULT_TIANDITU_KEY
159
+ config_files: list[str] = field(default_factory=list)
158
160
 
159
161
  # plotting defaults
160
162
  dpi: int = 300
@@ -167,43 +169,82 @@ class AppConfig:
167
169
 
168
170
  @classmethod
169
171
  def load(cls) -> AppConfig:
170
- """Load config from TOML file, falling back to defaults."""
172
+ """Load config from TOML files and environment variables.
173
+
174
+ Merge order:
175
+ 1. safe built-in defaults
176
+ 2. ~/.config/rsplot/config.toml, if present
177
+ 3. RSPLOT_CONFIG, if set
178
+ 4. environment variable path/API-key overrides
179
+ """
171
180
  cfg = cls()
172
- if CONFIG_PATH.exists():
173
- with open(CONFIG_PATH, "rb") as f:
181
+ for path in _config_paths():
182
+ with open(path, "rb") as f:
174
183
  data = tomllib.load(f)
175
184
  cfg._apply_toml(data)
176
- # Environment variable overrides (per-product: RSPLOT_DATA_DIR_NO2, etc.)
177
- for product in list(cfg.data_dirs):
185
+ cfg.config_files.append(str(path))
186
+
187
+ cfg._apply_env()
188
+ return cfg
189
+
190
+ def _apply_env(self) -> None:
191
+ """Apply environment variable overrides."""
192
+ products = set(KNOWN_DATA_PRODUCTS) | set(self.data_dirs)
193
+ for product in sorted(products):
178
194
  env_key = f"RSPLOT_DATA_DIR_{product.upper()}"
179
195
  if v := os.environ.get(env_key):
180
- cfg.data_dirs[product] = v
196
+ self.data_dirs[product] = _normalize_path(v)
181
197
  if v := os.environ.get("RSPLOT_GEOJSON_DIR"):
182
- cfg.geojson_dir = v
198
+ self.geojson_dir = _normalize_path(v)
199
+ if v := os.environ.get("RSPLOT_FONT_DIR"):
200
+ self.font_dir = _normalize_path(v)
201
+ if v := os.environ.get(HCHO_CMAP_FILE_ENV):
202
+ self.hcho_cmap_file = _normalize_path(v)
183
203
  if v := os.environ.get("TIANDITU_API_KEY"):
184
- cfg.tianditu_key = v
185
- return cfg
204
+ self.tianditu_key = v
186
205
 
187
206
  def get_data_dir(self, product: str) -> str:
188
207
  """Return data directory for a product, or raise if not configured."""
189
208
  key = product.lower()
190
- if key in self.data_dirs:
191
- return self.data_dirs[key]
192
- available = ", ".join(self.data_dirs)
209
+ dir_path = self.data_dirs.get(key)
210
+ if dir_path:
211
+ return dir_path
212
+
213
+ configured = (
214
+ ", ".join(sorted(k for k, v in self.data_dirs.items() if v))
215
+ or "none"
216
+ )
193
217
  raise ValueError(
194
218
  f"No data directory configured for product '{product}'. "
195
- f"Available: {available}"
219
+ f"Set [paths.data_dirs].{key} in {CONFIG_PATH}, set "
220
+ f"RSPLOT_DATA_DIR_{key.upper()}, point {CONFIG_ENV_VAR} to a "
221
+ "TOML config file, or pass the command-specific data directory "
222
+ f"option. Known products: {', '.join(KNOWN_DATA_PRODUCTS)}. "
223
+ f"Configured products: {configured}."
224
+ )
225
+
226
+ def require_geojson_dir(self) -> str:
227
+ """Return the boundary GeoJSON directory, or raise with guidance."""
228
+ if self.geojson_dir:
229
+ return self.geojson_dir
230
+ raise ValueError(
231
+ "GeoJSON boundary directory is not configured. Set "
232
+ f"[paths].geojson_dir in {CONFIG_PATH}, set RSPLOT_GEOJSON_DIR, "
233
+ f"or point {CONFIG_ENV_VAR} to a TOML config file."
196
234
  )
197
235
 
198
236
  def _apply_toml(self, data: dict[str, Any]) -> None:
199
237
  paths = data.get("paths", {})
200
238
  data_dirs = paths.get("data_dirs", {})
201
239
  for product, dir_path in data_dirs.items():
202
- self.data_dirs[product.lower()] = dir_path
240
+ if dir_path:
241
+ self.data_dirs[product.lower()] = _normalize_path(dir_path)
203
242
  if v := paths.get("geojson_dir"):
204
- self.geojson_dir = v
243
+ self.geojson_dir = _normalize_path(v)
205
244
  if v := paths.get("font_dir"):
206
- self.font_dir = v
245
+ self.font_dir = _normalize_path(v)
246
+ if v := paths.get("hcho_cmap_file"):
247
+ self.hcho_cmap_file = _normalize_path(v)
207
248
 
208
249
  td = data.get("tianditu", {})
209
250
  if v := td.get("api_key"):
@@ -220,6 +261,33 @@ class AppConfig:
220
261
  self.key_regions[name] = provinces
221
262
 
222
263
 
264
+ def _config_paths() -> list[Path]:
265
+ """Return config files to load in priority order."""
266
+ paths: list[Path] = []
267
+ if CONFIG_PATH.exists():
268
+ paths.append(CONFIG_PATH)
269
+
270
+ if raw := os.environ.get(CONFIG_ENV_VAR):
271
+ path = Path(_normalize_path(raw))
272
+ if not path.exists():
273
+ raise FileNotFoundError(
274
+ f"{CONFIG_ENV_VAR} points to a missing config file: {path}"
275
+ )
276
+ try:
277
+ already_loaded = path.resolve() in {p.resolve() for p in paths}
278
+ except OSError:
279
+ already_loaded = path in paths
280
+ if not already_loaded:
281
+ paths.append(path)
282
+
283
+ return paths
284
+
285
+
286
+ def _normalize_path(value: Any) -> str:
287
+ """Expand ~ and environment variables in configured path values."""
288
+ return os.path.expanduser(os.path.expandvars(str(value)))
289
+
290
+
223
291
  # ---------------------------------------------------------------------------
224
292
  # GB 3095-2026 Air Quality Limits (transitional phase, grade II)
225
293
  # ---------------------------------------------------------------------------
@@ -241,7 +309,7 @@ AQ_LIMITS_2026: dict[str, dict[str, float]] = {
241
309
  # ---------------------------------------------------------------------------
242
310
  AQI_COLORS: list[tuple[int, str]] = [
243
311
  # (upper bound, hex color)
244
- (50, "#00e400"), # 优 / green
312
+ (50, "#00e400"), # 优 / green
245
313
  (100, "#ffff00"), # 良 / yellow
246
314
  (150, "#ff7e00"), # 轻度污染 / orange
247
315
  (200, "#ff0000"), # 中度污染 / red
@@ -47,7 +47,9 @@ class RegionInfo:
47
47
  # sub-boundary GeoDataFrames for overlay
48
48
  main_boundary_gdf: gpd.GeoDataFrame | None = None # red border
49
49
  sub_boundary_gdf: gpd.GeoDataFrame | None = None # grey sub-divisions
50
- detail_boundary_gdf: gpd.GeoDataFrame | None = None # finer sub-divisions (counties in province view)
50
+ detail_boundary_gdf: gpd.GeoDataFrame | None = (
51
+ None # finer sub-divisions (counties in province view)
52
+ )
51
53
  sub_name_field: str = NAME_FIELD
52
54
 
53
55
 
@@ -108,7 +110,7 @@ def resolve_region(
108
110
  Cascade order: key_region → province → city → county.
109
111
  If level_override is given, skip cascade and match only that level.
110
112
  """
111
- gj = cfg.geojson_dir
113
+ gj = cfg.require_geojson_dir()
112
114
  gdf_prov = _load_province(gj)
113
115
  gdf_city = _load_city(gj)
114
116
  gdf_county = _load_county(gj)
@@ -209,9 +211,7 @@ def resolve_region(
209
211
  )
210
212
  else:
211
213
  # Regular province: cities + counties within each city
212
- sub_cities = gdf_city[
213
- gdf_city.geometry.intersects(union_geom)
214
- ]
214
+ sub_cities = gdf_city[gdf_city.geometry.intersects(union_geom)]
215
215
  detail_counties = gdf_county[
216
216
  gdf_county.geometry.intersects(union_geom)
217
217
  ]
@@ -244,7 +244,7 @@ def resolve_region(
244
244
  if level_override == "county":
245
245
  # "/" case already handled above; bare name with --level county is ambiguous
246
246
  raise ValueError(
247
- f"县/区级别需要指定所属城市,格式: 城市/县区,如 合肥/蜀山、成都/高新"
247
+ "县/区级别需要指定所属城市,格式: 城市/县区,如 合肥/蜀山、成都/高新"
248
248
  )
249
249
 
250
250
  raise ValueError(
@@ -12,15 +12,10 @@ import numpy as np
12
12
  HCHO_CMAP = "rsplot_hcho"
13
13
  STATION_CONCENTRATION_CMAP = "rsplot_station_concentration"
14
14
 
15
- DEFAULT_HCHO_CMAP_FILE = (
16
- "/exports/XCZ/python/tmp/pycharm_project_24/colormap_22.txt"
17
- )
18
- HCHO_CMAP_FILE_ENV = "RSPLOT_HCHO_CMAP_FILE"
19
15
 
20
-
21
- def register_rsplot_colormaps() -> None:
16
+ def register_rsplot_colormaps(hcho_cmap_file: str | None = None) -> None:
22
17
  """Register custom colormaps used by rsplot plots."""
23
- _register(_build_hcho_cmap())
18
+ _register(_build_hcho_cmap(hcho_cmap_file))
24
19
  _register(
25
20
  mcolors.LinearSegmentedColormap.from_list(
26
21
  STATION_CONCENTRATION_CMAP,
@@ -36,9 +31,13 @@ def register_rsplot_colormaps() -> None:
36
31
  )
37
32
 
38
33
 
39
- def _build_hcho_cmap() -> mcolors.Colormap:
40
- cmap_file = Path(os.environ.get(HCHO_CMAP_FILE_ENV, DEFAULT_HCHO_CMAP_FILE))
41
- if cmap_file.exists():
34
+ def _build_hcho_cmap(hcho_cmap_file: str | None) -> mcolors.Colormap:
35
+ if hcho_cmap_file:
36
+ cmap_file = Path(os.path.expandvars(hcho_cmap_file)).expanduser()
37
+ if not cmap_file.exists():
38
+ raise FileNotFoundError(
39
+ f"Configured HCHO colormap file does not exist: {cmap_file}"
40
+ )
42
41
  data = np.loadtxt(cmap_file)
43
42
  if data.shape != (256, 3):
44
43
  raise ValueError(