climplot 0.1.0__py3-none-any.whl
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.
- climplot/__init__.py +118 -0
- climplot/colormaps.py +254 -0
- climplot/data/presentation.mplstyle +37 -0
- climplot/data/publication.mplstyle +37 -0
- climplot/maps.py +230 -0
- climplot/metrics.py +404 -0
- climplot/panels.py +295 -0
- climplot/style.py +201 -0
- climplot/timeseries.py +110 -0
- climplot-0.1.0.dist-info/METADATA +198 -0
- climplot-0.1.0.dist-info/RECORD +14 -0
- climplot-0.1.0.dist-info/WHEEL +5 -0
- climplot-0.1.0.dist-info/licenses/LICENSE +21 -0
- climplot-0.1.0.dist-info/top_level.txt +1 -0
climplot/__init__.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
climplot - Publication-quality climate science plotting utilities.
|
|
3
|
+
|
|
4
|
+
A Python package for creating beautiful, publication-ready climate and weather
|
|
5
|
+
science figures with minimal effort. Designed for climate science beginners
|
|
6
|
+
and experts alike.
|
|
7
|
+
|
|
8
|
+
Quick Start
|
|
9
|
+
-----------
|
|
10
|
+
>>> import climplot
|
|
11
|
+
>>> climplot.publication() # Set up publication-quality defaults
|
|
12
|
+
>>> fig, ax = climplot.map_figure() # Create a map with Robinson projection
|
|
13
|
+
|
|
14
|
+
Style Modes
|
|
15
|
+
-----------
|
|
16
|
+
- climplot.publication(): 3.5" width, 8-11pt fonts, 300 DPI
|
|
17
|
+
- climplot.presentation(): 7" width, 12-16pt fonts, 150 DPI
|
|
18
|
+
|
|
19
|
+
See Also
|
|
20
|
+
--------
|
|
21
|
+
- Documentation: https://climplot.readthedocs.io
|
|
22
|
+
- Source: https://github.com/jkrasting/climplot
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
# Style functions
|
|
28
|
+
from .style import (
|
|
29
|
+
publication,
|
|
30
|
+
presentation,
|
|
31
|
+
reset_style,
|
|
32
|
+
get_current_mode,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Colormap functions
|
|
36
|
+
from .colormaps import (
|
|
37
|
+
anomaly_cmap,
|
|
38
|
+
sequential_cmap,
|
|
39
|
+
categorical_cmap,
|
|
40
|
+
discrete_cmap,
|
|
41
|
+
list_colormaps,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Map functions
|
|
45
|
+
from .maps import (
|
|
46
|
+
map_figure,
|
|
47
|
+
add_land_overlay,
|
|
48
|
+
add_coastlines,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Time series functions
|
|
52
|
+
from .timeseries import (
|
|
53
|
+
timeseries_figure,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Panel/multi-figure functions
|
|
57
|
+
from .panels import (
|
|
58
|
+
panel_figure,
|
|
59
|
+
add_panel_labels,
|
|
60
|
+
add_colorbar,
|
|
61
|
+
bottom_colorbar,
|
|
62
|
+
save_figure,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Metrics functions
|
|
66
|
+
from .metrics import (
|
|
67
|
+
area_weighted_mean,
|
|
68
|
+
area_weighted_bias,
|
|
69
|
+
area_weighted_rmse,
|
|
70
|
+
area_weighted_corr,
|
|
71
|
+
area_weighted_std,
|
|
72
|
+
timeseries_bias,
|
|
73
|
+
timeseries_rmse,
|
|
74
|
+
timeseries_corr,
|
|
75
|
+
timeseries_std,
|
|
76
|
+
metrics_summary,
|
|
77
|
+
print_metrics_summary,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
# Version
|
|
82
|
+
"__version__",
|
|
83
|
+
# Style
|
|
84
|
+
"publication",
|
|
85
|
+
"presentation",
|
|
86
|
+
"reset_style",
|
|
87
|
+
"get_current_mode",
|
|
88
|
+
# Colormaps
|
|
89
|
+
"anomaly_cmap",
|
|
90
|
+
"sequential_cmap",
|
|
91
|
+
"categorical_cmap",
|
|
92
|
+
"discrete_cmap",
|
|
93
|
+
"list_colormaps",
|
|
94
|
+
# Maps
|
|
95
|
+
"map_figure",
|
|
96
|
+
"add_land_overlay",
|
|
97
|
+
"add_coastlines",
|
|
98
|
+
# Time series
|
|
99
|
+
"timeseries_figure",
|
|
100
|
+
# Panels
|
|
101
|
+
"panel_figure",
|
|
102
|
+
"add_panel_labels",
|
|
103
|
+
"add_colorbar",
|
|
104
|
+
"bottom_colorbar",
|
|
105
|
+
"save_figure",
|
|
106
|
+
# Metrics
|
|
107
|
+
"area_weighted_mean",
|
|
108
|
+
"area_weighted_bias",
|
|
109
|
+
"area_weighted_rmse",
|
|
110
|
+
"area_weighted_corr",
|
|
111
|
+
"area_weighted_std",
|
|
112
|
+
"timeseries_bias",
|
|
113
|
+
"timeseries_rmse",
|
|
114
|
+
"timeseries_corr",
|
|
115
|
+
"timeseries_std",
|
|
116
|
+
"metrics_summary",
|
|
117
|
+
"print_metrics_summary",
|
|
118
|
+
]
|
climplot/colormaps.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Climate-specific colormap utilities.
|
|
3
|
+
|
|
4
|
+
This module provides colormaps optimized for climate data visualization,
|
|
5
|
+
including diverging colormaps for anomalies and sequential colormaps
|
|
6
|
+
for positive-only data.
|
|
7
|
+
|
|
8
|
+
Key Features
|
|
9
|
+
------------
|
|
10
|
+
- Discrete levels with "round" intervals (0, 1, 2, or 5 endings)
|
|
11
|
+
- Center-on-white option for difference plots
|
|
12
|
+
- Pre-defined colormaps for common climate variables
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
--------
|
|
16
|
+
>>> import climplot
|
|
17
|
+
>>> cmap, norm, levels = climplot.anomaly_cmap(-0.3, 0.3, 0.05)
|
|
18
|
+
>>> ax.pcolormesh(lon, lat, data, cmap=cmap, norm=norm)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
import matplotlib.colors as mcolors
|
|
23
|
+
from matplotlib.colors import ListedColormap, BoundaryNorm
|
|
24
|
+
import numpy as np
|
|
25
|
+
from typing import Tuple, List, Optional
|
|
26
|
+
|
|
27
|
+
# Standard climate colormaps
|
|
28
|
+
CLIMATE_COLORMAPS = {
|
|
29
|
+
"anomaly": "RdBu_r", # Red-blue diverging (red=high, blue=low)
|
|
30
|
+
"temperature": "RdBu_r",
|
|
31
|
+
"precipitation": "BrBG", # Brown-teal diverging
|
|
32
|
+
"ssh": "RdBu_r", # Sea surface height
|
|
33
|
+
"wind": "PuOr_r", # Purple-orange diverging
|
|
34
|
+
"sequential": "viridis", # Sequential
|
|
35
|
+
"ice": "Blues_r", # Ice concentration
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def anomaly_cmap(
|
|
40
|
+
vmin: float = -1.0,
|
|
41
|
+
vmax: float = 1.0,
|
|
42
|
+
interval: float = 0.1,
|
|
43
|
+
center_on_white: bool = False,
|
|
44
|
+
cmap_name: str = "RdBu_r",
|
|
45
|
+
) -> Tuple[mcolors.Colormap, mcolors.BoundaryNorm, np.ndarray]:
|
|
46
|
+
"""
|
|
47
|
+
Create a diverging colormap for anomaly fields.
|
|
48
|
+
|
|
49
|
+
Generates discrete color levels centered on zero with the specified
|
|
50
|
+
interval. By default uses RdBu_r where red=positive, blue=negative.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
vmin : float, optional
|
|
55
|
+
Minimum value for color scale. Default is -1.0.
|
|
56
|
+
vmax : float, optional
|
|
57
|
+
Maximum value for color scale. Default is 1.0.
|
|
58
|
+
interval : float, optional
|
|
59
|
+
Interval between color levels. Should end in 0, 1, 2, or 5.
|
|
60
|
+
Default is 0.1.
|
|
61
|
+
center_on_white : bool, optional
|
|
62
|
+
If True, creates a white band centered on zero.
|
|
63
|
+
White band width is ±interval/2. Default is False.
|
|
64
|
+
Recommended for difference plots.
|
|
65
|
+
cmap_name : str, optional
|
|
66
|
+
Base colormap name. Default is 'RdBu_r'.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
cmap : Colormap
|
|
71
|
+
The colormap object
|
|
72
|
+
norm : BoundaryNorm
|
|
73
|
+
Normalization for discrete levels
|
|
74
|
+
levels : ndarray
|
|
75
|
+
The discrete color levels
|
|
76
|
+
|
|
77
|
+
Examples
|
|
78
|
+
--------
|
|
79
|
+
>>> # Standard anomaly colormap
|
|
80
|
+
>>> cmap, norm, levels = climplot.anomaly_cmap(-0.3, 0.3, 0.05)
|
|
81
|
+
|
|
82
|
+
>>> # Difference plot with white band for near-zero values
|
|
83
|
+
>>> cmap, norm, levels = climplot.anomaly_cmap(-0.3, 0.3, 0.05, center_on_white=True)
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
-----
|
|
87
|
+
Common interval choices:
|
|
88
|
+
- 0.01, 0.02, 0.05, 0.10, 0.20, 0.50, 1.0, 2.0, 5.0
|
|
89
|
+
"""
|
|
90
|
+
return discrete_cmap(
|
|
91
|
+
vmin, vmax, interval, cmap_name=cmap_name, center_on_white=center_on_white
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sequential_cmap(
|
|
96
|
+
vmin: float = 0.0,
|
|
97
|
+
vmax: float = 1.0,
|
|
98
|
+
interval: float = 0.1,
|
|
99
|
+
cmap_name: str = "viridis",
|
|
100
|
+
) -> Tuple[mcolors.Colormap, mcolors.BoundaryNorm, np.ndarray]:
|
|
101
|
+
"""
|
|
102
|
+
Create a sequential colormap for positive-only data.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
vmin : float, optional
|
|
107
|
+
Minimum value. Default is 0.0.
|
|
108
|
+
vmax : float, optional
|
|
109
|
+
Maximum value. Default is 1.0.
|
|
110
|
+
interval : float, optional
|
|
111
|
+
Interval between levels. Default is 0.1.
|
|
112
|
+
cmap_name : str, optional
|
|
113
|
+
Colormap name. Default is 'viridis'.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
cmap : Colormap
|
|
118
|
+
norm : BoundaryNorm
|
|
119
|
+
levels : ndarray
|
|
120
|
+
|
|
121
|
+
Examples
|
|
122
|
+
--------
|
|
123
|
+
>>> cmap, norm, levels = climplot.sequential_cmap(0, 100, 10)
|
|
124
|
+
"""
|
|
125
|
+
return discrete_cmap(vmin, vmax, interval, cmap_name=cmap_name, extend="max")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def categorical_cmap(
|
|
129
|
+
n_categories: int,
|
|
130
|
+
cmap_name: str = "tab10",
|
|
131
|
+
) -> ListedColormap:
|
|
132
|
+
"""
|
|
133
|
+
Create a colormap for discrete categories.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
n_categories : int
|
|
138
|
+
Number of categories
|
|
139
|
+
cmap_name : str, optional
|
|
140
|
+
Base colormap. Default is 'tab10'.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
ListedColormap
|
|
145
|
+
Colormap with n_categories discrete colors
|
|
146
|
+
|
|
147
|
+
Examples
|
|
148
|
+
--------
|
|
149
|
+
>>> cmap = climplot.categorical_cmap(5)
|
|
150
|
+
"""
|
|
151
|
+
base_cmap = plt.get_cmap(cmap_name)
|
|
152
|
+
colors = [base_cmap(i) for i in range(n_categories)]
|
|
153
|
+
return ListedColormap(colors)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def discrete_cmap(
|
|
157
|
+
vmin: float,
|
|
158
|
+
vmax: float,
|
|
159
|
+
interval: float,
|
|
160
|
+
cmap_name: str = "RdBu_r",
|
|
161
|
+
extend: str = "both",
|
|
162
|
+
center_on_white: bool = False,
|
|
163
|
+
) -> Tuple[mcolors.Colormap, mcolors.BoundaryNorm, np.ndarray]:
|
|
164
|
+
"""
|
|
165
|
+
Create a discrete colormap with BoundaryNorm.
|
|
166
|
+
|
|
167
|
+
Generates color levels with "round" intervals for clean, readable
|
|
168
|
+
colorbars. Optionally centers colormap on white for difference plots.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
vmin : float
|
|
173
|
+
Minimum value for color scale
|
|
174
|
+
vmax : float
|
|
175
|
+
Maximum value for color scale
|
|
176
|
+
interval : float
|
|
177
|
+
Interval between color levels. Should end in 0, 1, 2, or 5.
|
|
178
|
+
cmap_name : str, optional
|
|
179
|
+
Name of matplotlib colormap. Default is 'RdBu_r'.
|
|
180
|
+
extend : str, optional
|
|
181
|
+
Out-of-range handling: 'neither', 'min', 'max', or 'both'.
|
|
182
|
+
Default is 'both'.
|
|
183
|
+
center_on_white : bool, optional
|
|
184
|
+
If True, creates a white band centered on zero.
|
|
185
|
+
White band width is ±interval/2. Default is False.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
cmap : Colormap
|
|
190
|
+
The colormap object
|
|
191
|
+
norm : BoundaryNorm
|
|
192
|
+
Normalization for discrete levels
|
|
193
|
+
levels : ndarray
|
|
194
|
+
The discrete color levels
|
|
195
|
+
|
|
196
|
+
Examples
|
|
197
|
+
--------
|
|
198
|
+
>>> # Standard discrete colormap
|
|
199
|
+
>>> cmap, norm, levels = discrete_cmap(-0.3, 0.3, 0.05)
|
|
200
|
+
|
|
201
|
+
>>> # With white band at center
|
|
202
|
+
>>> cmap, norm, levels = discrete_cmap(-0.3, 0.3, 0.05, center_on_white=True)
|
|
203
|
+
|
|
204
|
+
Notes
|
|
205
|
+
-----
|
|
206
|
+
When center_on_white=True:
|
|
207
|
+
- White band width is ±interval/2
|
|
208
|
+
- For interval=0.05, white covers -0.025 to +0.025
|
|
209
|
+
- Best for difference plots where near-zero values are ambiguous
|
|
210
|
+
"""
|
|
211
|
+
if center_on_white:
|
|
212
|
+
# Create levels with white band centered on zero
|
|
213
|
+
neg_levels = np.arange(vmin, -interval / 10, interval)
|
|
214
|
+
white_band = np.array([-interval / 2, interval / 2])
|
|
215
|
+
pos_levels = np.arange(interval, vmax + interval / 2, interval)
|
|
216
|
+
levels = np.sort(np.concatenate([neg_levels, white_band, pos_levels]))
|
|
217
|
+
else:
|
|
218
|
+
# Standard levels
|
|
219
|
+
levels = np.arange(vmin, vmax + interval / 2, interval)
|
|
220
|
+
|
|
221
|
+
# Get base colormap
|
|
222
|
+
base_cmap = plt.get_cmap(cmap_name)
|
|
223
|
+
|
|
224
|
+
# Modify for center-on-white
|
|
225
|
+
if center_on_white:
|
|
226
|
+
n_colors = 256
|
|
227
|
+
colors = base_cmap(np.linspace(0, 1, n_colors))
|
|
228
|
+
center_idx = n_colors // 2
|
|
229
|
+
colors[center_idx] = [1.0, 1.0, 1.0, 1.0] # Pure white
|
|
230
|
+
cmap = ListedColormap(colors, name=f"{cmap_name}_white_center")
|
|
231
|
+
else:
|
|
232
|
+
cmap = base_cmap
|
|
233
|
+
|
|
234
|
+
# Create discrete normalization
|
|
235
|
+
norm = BoundaryNorm(levels, cmap.N, extend=extend)
|
|
236
|
+
|
|
237
|
+
return cmap, norm, levels
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def list_colormaps() -> dict:
|
|
241
|
+
"""
|
|
242
|
+
List recommended colormaps for climate variables.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
dict
|
|
247
|
+
Mapping of variable types to recommended colormap names
|
|
248
|
+
|
|
249
|
+
Examples
|
|
250
|
+
--------
|
|
251
|
+
>>> climplot.list_colormaps()
|
|
252
|
+
{'anomaly': 'RdBu_r', 'temperature': 'RdBu_r', ...}
|
|
253
|
+
"""
|
|
254
|
+
return CLIMATE_COLORMAPS.copy()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# climplot presentation style
|
|
2
|
+
# For slides/posters: 7" width, 12-16pt fonts, 150 DPI
|
|
3
|
+
# Usage: plt.style.use('climplot/data/presentation.mplstyle')
|
|
4
|
+
|
|
5
|
+
# Font sizes (larger for readability)
|
|
6
|
+
font.size: 14
|
|
7
|
+
axes.labelsize: 14
|
|
8
|
+
axes.titlesize: 16
|
|
9
|
+
xtick.labelsize: 14
|
|
10
|
+
ytick.labelsize: 14
|
|
11
|
+
legend.fontsize: 12
|
|
12
|
+
|
|
13
|
+
# Figure settings
|
|
14
|
+
figure.figsize: 7.0, 4.2
|
|
15
|
+
figure.dpi: 150
|
|
16
|
+
savefig.dpi: 150
|
|
17
|
+
savefig.bbox: tight
|
|
18
|
+
savefig.facecolor: white
|
|
19
|
+
|
|
20
|
+
# Line widths (thicker for visibility)
|
|
21
|
+
axes.linewidth: 1.2
|
|
22
|
+
grid.linewidth: 0.5
|
|
23
|
+
lines.linewidth: 2.5
|
|
24
|
+
|
|
25
|
+
# Grid
|
|
26
|
+
axes.grid: False
|
|
27
|
+
grid.alpha: 0.3
|
|
28
|
+
|
|
29
|
+
# Legend
|
|
30
|
+
legend.frameon: False
|
|
31
|
+
legend.loc: best
|
|
32
|
+
|
|
33
|
+
# PDF output
|
|
34
|
+
pdf.fonttype: 42
|
|
35
|
+
|
|
36
|
+
# Colors
|
|
37
|
+
axes.prop_cycle: cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2', '7f7f7f', 'bcbd22', '17becf'])
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# climplot publication style
|
|
2
|
+
# For journal figures: 3.5" width, 8-11pt fonts, 300 DPI
|
|
3
|
+
# Usage: plt.style.use('climplot/data/publication.mplstyle')
|
|
4
|
+
|
|
5
|
+
# Font sizes
|
|
6
|
+
font.size: 10
|
|
7
|
+
axes.labelsize: 10
|
|
8
|
+
axes.titlesize: 11
|
|
9
|
+
xtick.labelsize: 8
|
|
10
|
+
ytick.labelsize: 8
|
|
11
|
+
legend.fontsize: 8
|
|
12
|
+
|
|
13
|
+
# Figure settings
|
|
14
|
+
figure.figsize: 3.5, 2.625
|
|
15
|
+
figure.dpi: 300
|
|
16
|
+
savefig.dpi: 300
|
|
17
|
+
savefig.bbox: tight
|
|
18
|
+
savefig.facecolor: white
|
|
19
|
+
|
|
20
|
+
# Line widths
|
|
21
|
+
axes.linewidth: 0.8
|
|
22
|
+
grid.linewidth: 0.3
|
|
23
|
+
lines.linewidth: 1.5
|
|
24
|
+
|
|
25
|
+
# Grid
|
|
26
|
+
axes.grid: False
|
|
27
|
+
grid.alpha: 0.3
|
|
28
|
+
|
|
29
|
+
# Legend
|
|
30
|
+
legend.frameon: False
|
|
31
|
+
legend.loc: best
|
|
32
|
+
|
|
33
|
+
# PDF output
|
|
34
|
+
pdf.fonttype: 42
|
|
35
|
+
|
|
36
|
+
# Colors (use colorblind-friendly defaults)
|
|
37
|
+
axes.prop_cycle: cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2', '7f7f7f', 'bcbd22', '17becf'])
|
climplot/maps.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Map plotting utilities with Cartopy projections.
|
|
3
|
+
|
|
4
|
+
This module provides functions for creating publication-quality maps
|
|
5
|
+
with appropriate projections, coastlines, and land overlays.
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
>>> import climplot
|
|
10
|
+
>>> climplot.publication()
|
|
11
|
+
>>> fig, ax = climplot.map_figure()
|
|
12
|
+
>>> cs = ax.pcolormesh(lon, lat, data, transform=ccrs.PlateCarree())
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
import numpy as np
|
|
17
|
+
from typing import Tuple, Optional
|
|
18
|
+
|
|
19
|
+
# Cartopy imports (required dependency)
|
|
20
|
+
import cartopy.crs as ccrs
|
|
21
|
+
import cartopy.feature as cfeature
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def map_figure(
|
|
25
|
+
projection: str = "robinson",
|
|
26
|
+
central_longitude: float = 180,
|
|
27
|
+
figsize: Optional[Tuple[float, float]] = None,
|
|
28
|
+
**kwargs,
|
|
29
|
+
) -> Tuple[plt.Figure, plt.Axes]:
|
|
30
|
+
"""
|
|
31
|
+
Create a map figure with specified projection.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
projection : str, optional
|
|
36
|
+
Map projection name. Options:
|
|
37
|
+
- 'robinson' (default): Good for global maps
|
|
38
|
+
- 'platecarree': Equirectangular, simple lat/lon
|
|
39
|
+
- 'mollweide': Equal-area, good for global
|
|
40
|
+
- 'orthographic': Globe view
|
|
41
|
+
- 'mercator': Standard navigation projection
|
|
42
|
+
- 'northpolarstereo': Arctic view
|
|
43
|
+
- 'southpolarstereo': Antarctic view
|
|
44
|
+
central_longitude : float, optional
|
|
45
|
+
Central longitude for projection. Default is 180 (Pacific-centered).
|
|
46
|
+
figsize : tuple, optional
|
|
47
|
+
Figure size (width, height) in inches. If None, uses matplotlib defaults.
|
|
48
|
+
**kwargs
|
|
49
|
+
Additional arguments passed to plt.figure()
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
fig : Figure
|
|
54
|
+
The figure object
|
|
55
|
+
ax : GeoAxes
|
|
56
|
+
The map axes with specified projection
|
|
57
|
+
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
>>> # Global Robinson projection (Pacific-centered)
|
|
61
|
+
>>> fig, ax = climplot.map_figure()
|
|
62
|
+
|
|
63
|
+
>>> # Atlantic-centered
|
|
64
|
+
>>> fig, ax = climplot.map_figure(central_longitude=0)
|
|
65
|
+
|
|
66
|
+
>>> # Plate Carree for regional maps
|
|
67
|
+
>>> fig, ax = climplot.map_figure(projection='platecarree')
|
|
68
|
+
"""
|
|
69
|
+
# Get projection
|
|
70
|
+
proj = _get_projection(projection, central_longitude)
|
|
71
|
+
|
|
72
|
+
# Create figure
|
|
73
|
+
if figsize is not None:
|
|
74
|
+
kwargs["figsize"] = figsize
|
|
75
|
+
fig = plt.figure(**kwargs)
|
|
76
|
+
ax = fig.add_subplot(111, projection=proj)
|
|
77
|
+
|
|
78
|
+
return fig, ax
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_projection(name: str, central_longitude: float = 180):
|
|
82
|
+
"""Get a cartopy projection by name."""
|
|
83
|
+
projections = {
|
|
84
|
+
"robinson": ccrs.Robinson(central_longitude=central_longitude),
|
|
85
|
+
"platecarree": ccrs.PlateCarree(central_longitude=central_longitude),
|
|
86
|
+
"mollweide": ccrs.Mollweide(central_longitude=central_longitude),
|
|
87
|
+
"orthographic": ccrs.Orthographic(
|
|
88
|
+
central_longitude=central_longitude, central_latitude=0
|
|
89
|
+
),
|
|
90
|
+
"mercator": ccrs.Mercator(central_longitude=central_longitude),
|
|
91
|
+
"northpolarstereo": ccrs.NorthPolarStereo(),
|
|
92
|
+
"southpolarstereo": ccrs.SouthPolarStereo(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
name_lower = name.lower().replace("_", "").replace("-", "")
|
|
96
|
+
if name_lower not in projections:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Unknown projection: {name}. "
|
|
99
|
+
f"Available: {list(projections.keys())}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return projections[name_lower]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def add_land_overlay(
|
|
106
|
+
ax: plt.Axes,
|
|
107
|
+
lon,
|
|
108
|
+
lat,
|
|
109
|
+
wet_mask,
|
|
110
|
+
land_color: str = "#808080",
|
|
111
|
+
zorder: int = 10,
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Add gray overlay to render land areas.
|
|
115
|
+
|
|
116
|
+
Draws a solid color layer over land points (wet=0), ensuring
|
|
117
|
+
continents appear uniformly regardless of underlying data values.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
ax : GeoAxes
|
|
122
|
+
Map axes to add overlay to
|
|
123
|
+
lon : array-like
|
|
124
|
+
2D longitude array
|
|
125
|
+
lat : array-like
|
|
126
|
+
2D latitude array
|
|
127
|
+
wet_mask : array-like
|
|
128
|
+
Wet mask where 1 = ocean, 0 = land
|
|
129
|
+
land_color : str, optional
|
|
130
|
+
Color for land. Default is '#808080' (medium gray).
|
|
131
|
+
zorder : int, optional
|
|
132
|
+
Drawing order. Default is 10 (on top of most elements).
|
|
133
|
+
|
|
134
|
+
Examples
|
|
135
|
+
--------
|
|
136
|
+
>>> fig, ax = climplot.map_figure()
|
|
137
|
+
>>> cs = ax.pcolormesh(lon, lat, data, transform=ccrs.PlateCarree())
|
|
138
|
+
>>> climplot.add_land_overlay(ax, lon, lat, wet_mask)
|
|
139
|
+
"""
|
|
140
|
+
import xarray as xr
|
|
141
|
+
from matplotlib.colors import ListedColormap
|
|
142
|
+
|
|
143
|
+
# Create land overlay: 1 where land (wet=0), NaN where ocean
|
|
144
|
+
land_overlay = xr.where(wet_mask == 0, 1.0, np.nan)
|
|
145
|
+
|
|
146
|
+
# Single-color colormap
|
|
147
|
+
gray_cmap = ListedColormap([land_color])
|
|
148
|
+
|
|
149
|
+
# Draw over land
|
|
150
|
+
ax.pcolormesh(
|
|
151
|
+
lon,
|
|
152
|
+
lat,
|
|
153
|
+
land_overlay,
|
|
154
|
+
transform=ccrs.PlateCarree(),
|
|
155
|
+
cmap=gray_cmap,
|
|
156
|
+
vmin=0,
|
|
157
|
+
vmax=1,
|
|
158
|
+
zorder=zorder,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def add_coastlines(
|
|
163
|
+
ax: plt.Axes,
|
|
164
|
+
resolution: str = "110m",
|
|
165
|
+
linewidth: float = 0.5,
|
|
166
|
+
color: str = "black",
|
|
167
|
+
**kwargs,
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Add coastlines to a map.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
ax : GeoAxes
|
|
175
|
+
Map axes
|
|
176
|
+
resolution : str, optional
|
|
177
|
+
Resolution: '110m' (default), '50m', or '10m'.
|
|
178
|
+
Higher resolution = more detail but slower.
|
|
179
|
+
linewidth : float, optional
|
|
180
|
+
Line width. Default is 0.5.
|
|
181
|
+
color : str, optional
|
|
182
|
+
Line color. Default is 'black'.
|
|
183
|
+
**kwargs
|
|
184
|
+
Additional arguments passed to ax.coastlines()
|
|
185
|
+
|
|
186
|
+
Examples
|
|
187
|
+
--------
|
|
188
|
+
>>> fig, ax = climplot.map_figure()
|
|
189
|
+
>>> climplot.add_coastlines(ax)
|
|
190
|
+
|
|
191
|
+
Notes
|
|
192
|
+
-----
|
|
193
|
+
For model data on native grids, consider using add_land_overlay()
|
|
194
|
+
instead of coastlines, as coastlines may not align with the model grid.
|
|
195
|
+
"""
|
|
196
|
+
ax.coastlines(resolution=resolution, linewidth=linewidth, color=color, **kwargs)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def add_land_feature(
|
|
200
|
+
ax: plt.Axes,
|
|
201
|
+
resolution: str = "110m",
|
|
202
|
+
facecolor: str = "#808080",
|
|
203
|
+
edgecolor: str = "none",
|
|
204
|
+
**kwargs,
|
|
205
|
+
):
|
|
206
|
+
"""
|
|
207
|
+
Add land feature (filled continents) to a map.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
ax : GeoAxes
|
|
212
|
+
Map axes
|
|
213
|
+
resolution : str, optional
|
|
214
|
+
Resolution: '110m' (default), '50m', or '10m'.
|
|
215
|
+
facecolor : str, optional
|
|
216
|
+
Fill color. Default is '#808080' (gray).
|
|
217
|
+
edgecolor : str, optional
|
|
218
|
+
Edge color. Default is 'none'.
|
|
219
|
+
**kwargs
|
|
220
|
+
Additional arguments passed to ax.add_feature()
|
|
221
|
+
|
|
222
|
+
Examples
|
|
223
|
+
--------
|
|
224
|
+
>>> fig, ax = climplot.map_figure()
|
|
225
|
+
>>> climplot.add_land_feature(ax)
|
|
226
|
+
"""
|
|
227
|
+
land = cfeature.NaturalEarthFeature(
|
|
228
|
+
"physical", "land", resolution, facecolor=facecolor, edgecolor=edgecolor
|
|
229
|
+
)
|
|
230
|
+
ax.add_feature(land, **kwargs)
|