Skip to content

Commit 2736e62

Browse files
Plot NaNs in gray color and use log color bar when appropriate (#929)
* In the plot function geo_im_from_array, NaN values in the data will be plotted in gray. Before, NaN value were not plotted (i.e. transparent), making them indistinguishable from plot regions for which there is no data (no centroids). * In the plot function plot_from_gdf, the colorbar with will be shown on a logarithmic scale if a) the gdf is about return periods or impacts, b) there are no zeros in the data, c) the span of the data's values are at least two orders of magnitude. * Rename 'subplots_from_gdf' to 'plots_from_gdf'. --------- Co-authored-by: Lukas Riedel <[email protected]>
1 parent 994fd6d commit 2736e62

File tree

4 files changed

+84
-36
lines changed

4 files changed

+84
-36
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Code freeze date: YYYY-MM-DD
1414

1515
### Changed
1616

17+
- In `climada.util.plot.geo_im_from_array`, NaNs are plotted in gray while cells with no centroid are not plotted [#929](https://github.com/CLIMADA-project/climada_python/pull/929)
18+
- Renamed `climada.util.plot.subplots_from_gdf` to `climada.util.plot.plot_from_gdf` [#929](https://github.com/CLIMADA-project/climada_python/pull/929)
19+
1720
### Fixed
1821

1922
### Deprecated
@@ -89,6 +92,8 @@ CLIMADA tutorials. [#872](https://github.com/CLIMADA-project/climada_python/pull
8992
- climada.hazard.centroids.centr.Centroids.to_default_crs
9093
- climada.hazard.centroids.centr.Centroids.write_csv
9194
- climada.hazard.centroids.centr.Centroids.write_excel
95+
- climada.hazard.local_return_period [#898](https://github.com/CLIMADA-project/climada_python/pull/898)
96+
- climada.util.plot.subplots_from_gdf [#898](https://github.com/CLIMADA-project/climada_python/pull/898)
9297

9398
### Deprecated
9499

climada/util/plot.py

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,15 @@ def geo_im_from_array(array_sub, coord, var_name, title,
309309
if not proj:
310310
mid_lon = 0.5 * sum(extent[:2])
311311
proj = ccrs.PlateCarree(central_longitude=mid_lon)
312-
if 'vmin' not in kwargs:
313-
kwargs['vmin'] = np.nanmin(array_sub)
314-
if 'vmax' not in kwargs:
315-
kwargs['vmax'] = np.nanmax(array_sub)
312+
313+
if "norm" in kwargs:
314+
min_value = kwargs["norm"].vmin
315+
else:
316+
kwargs['vmin'] = kwargs.get("vmin", np.nanmin(array_sub))
317+
min_value = kwargs['vmin']
318+
kwargs['vmax'] = kwargs.get("vmax", np.nanmax(array_sub))
319+
min_value = min_value/2 if min_value > 0 else min_value-1
320+
316321
if axes is None:
317322
proj_plot = proj
318323
if isinstance(proj, ccrs.PlateCarree):
@@ -328,8 +333,10 @@ def geo_im_from_array(array_sub, coord, var_name, title,
328333
if not isinstance(axes, np.ndarray):
329334
axes_iter = np.array([[axes]])
330335

331-
if 'cmap' not in kwargs:
332-
kwargs['cmap'] = CMAP_RASTER
336+
# prepare colormap
337+
cmap = plt.get_cmap(kwargs.pop("cmap", CMAP_RASTER))
338+
cmap.set_bad("gainsboro") # For NaNs and infs
339+
cmap.set_under("white", alpha=0) # For values below vmin
333340

334341
# Generate each subplot
335342
for array_im, axis, tit, name in zip(list_arr, axes_iter.flatten(), list_tit, list_name):
@@ -340,8 +347,11 @@ def geo_im_from_array(array_sub, coord, var_name, title,
340347
grid_x, grid_y = np.mgrid[
341348
extent[0]: extent[1]: complex(0, RESOLUTION),
342349
extent[2]: extent[3]: complex(0, RESOLUTION)]
343-
grid_im = griddata((coord[:, 1], coord[:, 0]), array_im,
344-
(grid_x, grid_y))
350+
grid_im = griddata(
351+
(coord[:, 1], coord[:, 0]),
352+
array_im,
353+
(grid_x, grid_y),
354+
fill_value=min_value)
345355
else:
346356
grid_x = coord[:, 1].reshape((width, height)).transpose()
347357
grid_y = coord[:, 0].reshape((width, height)).transpose()
@@ -359,8 +369,14 @@ def geo_im_from_array(array_sub, coord, var_name, title,
359369
# Create colormesh, colorbar and labels in axis
360370
cbax = make_axes_locatable(axis).append_axes('right', size="6.5%",
361371
pad=0.1, axes_class=plt.Axes)
362-
img = axis.pcolormesh(grid_x - mid_lon, grid_y, np.squeeze(grid_im),
363-
transform=proj, **kwargs)
372+
img = axis.pcolormesh(
373+
grid_x - mid_lon,
374+
grid_y,
375+
np.squeeze(grid_im),
376+
transform=proj,
377+
cmap=cmap,
378+
**kwargs
379+
)
364380
cbar = plt.colorbar(img, cax=cbax, orientation='vertical')
365381
cbar.set_label(name)
366382
axis.set_title("\n".join(wrap(tit)))
@@ -876,7 +892,7 @@ def multibar_plot(ax, data, colors=None, total_width=0.8, single_width=1,
876892
if legend:
877893
ax.legend(bars, data.keys())
878894

879-
def subplots_from_gdf(
895+
def plot_from_gdf(
880896
gdf: gpd.GeoDataFrame,
881897
colorbar_name: str = None,
882898
title_subplots: callable = None,
@@ -917,7 +933,7 @@ def subplots_from_gdf(
917933
# check if inputs are correct types
918934
if not isinstance(gdf, gpd.GeoDataFrame):
919935
raise ValueError("gdf is not a GeoDataFrame")
920-
gdf = gdf[['geometry', *[col for col in gdf.columns if col != 'geometry']]]
936+
gdf_values = gdf.drop(columns='geometry').values.T
921937

922938
# read meta data for fig and axis labels
923939
if not isinstance(colorbar_name, str):
@@ -927,23 +943,32 @@ def subplots_from_gdf(
927943
print("Unknown subplot-title-generation function. Subplot titles will be column names.")
928944
title_subplots = lambda cols: [f"{col}" for col in cols]
929945

930-
# change default plot kwargs if plotting return periods
931-
if colorbar_name.strip().startswith('Return Period'):
932-
if 'cmap' not in kwargs.keys():
933-
kwargs.update({'cmap': 'viridis_r'})
934-
if 'norm' not in kwargs.keys():
935-
kwargs.update(
936-
{'norm': mpl.colors.LogNorm(
937-
vmin=gdf.values[:,1:].min(), vmax=gdf.values[:,1:].max()
938-
),
939-
'vmin': None, 'vmax': None}
940-
)
946+
# use log colorbar for return periods and impact
947+
if (
948+
colorbar_name.strip().startswith(('Return Period', 'Impact')) and
949+
'norm' not in kwargs.keys() and
950+
# check if there are no zeros values in gdf
951+
not np.any(gdf_values == 0) and
952+
# check if value range too small for logarithmic colorscale
953+
(np.log10(np.nanmax(gdf_values)) - np.log10(np.nanmin(gdf_values))) > 2
954+
):
955+
kwargs.update(
956+
{'norm': mpl.colors.LogNorm(
957+
vmin=np.nanmin(gdf_values), vmax=np.nanmax(gdf_values)
958+
),
959+
'vmin': None, 'vmax': None}
960+
)
961+
962+
# use inverted color bar for return periods
963+
if (colorbar_name.strip().startswith('Return Period') and
964+
'cmap' not in kwargs.keys()):
965+
kwargs.update({'cmap': 'viridis_r'})
941966

942967
axis = geo_im_from_array(
943-
gdf.values[:,1:].T,
968+
gdf_values,
944969
gdf.geometry.get_coordinates().values[:,::-1],
945970
colorbar_name,
946-
title_subplots(gdf.columns[1:]),
971+
title_subplots(np.delete(gdf.columns, np.where(gdf.columns == 'geometry'))),
947972
smooth=smooth,
948973
axes=axis,
949974
figsize=figsize,

climada/util/test/test_plot.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def test_geo_bin_from_array(self):
127127
plt.close()
128128

129129
def test_geo_im_from_array(self):
130-
values = np.array([1, 2.0, 5, 1])
130+
values = np.array([1, 2.0, 5, np.nan])
131131
coord = np.array([[-17, 178], [-10, 180], [-27, 175], [-16, 186]])
132132
var_name = 'test'
133133
title = 'test'
@@ -137,8 +137,8 @@ def test_geo_im_from_array(self):
137137
proj=projection, smooth=True, axes=None, figsize=(9, 13), cmap=cmap)
138138
self.assertEqual(var_name, ax.get_title())
139139
colorbar = next(x.colorbar for x in ax.collections if x.colorbar)
140-
self.assertAlmostEqual(np.max(values), colorbar.vmax)
141-
self.assertAlmostEqual(np.min(values), colorbar.vmin)
140+
self.assertAlmostEqual(np.nanmax(values), colorbar.vmax)
141+
self.assertAlmostEqual(np.nanmin(values), colorbar.vmin)
142142
self.assertEqual(cmap, ax.collections[0].cmap.name)
143143
plt.close()
144144

@@ -147,20 +147,38 @@ def test_geo_im_from_array(self):
147147
proj=projection, smooth=True, axes=None, figsize=(9, 13), cmap=cmap)
148148
self.assertEqual(var_name, ax.get_title())
149149
colorbar = next(x.colorbar for x in ax.collections if x.colorbar)
150-
self.assertAlmostEqual(np.max(values), colorbar.vmax)
151-
self.assertAlmostEqual(np.min(values), colorbar.vmin)
150+
self.assertAlmostEqual(np.nanmax(values), colorbar.vmax)
151+
self.assertAlmostEqual(np.nanmin(values), colorbar.vmin)
152152
self.assertEqual(cmap, ax.collections[0].cmap.name)
153153
plt.close()
154154

155-
def test_subplots_from_gdf(self):
155+
def test_plot_from_gdf_no_log(self):
156+
"""test plot_from_gdf() with linear color bar (because there is a 0 in data)"""
157+
return_periods = gpd.GeoDataFrame(
158+
data = ((2., 5.), (0., 6.), (None, 2.), (1., 1000.)),
159+
columns = ('10.0', '20.0')
160+
)
161+
return_periods['geometry'] = (Point(45., 26.), Point(46., 26.), Point(45., 27.), Point(46., 27.))
162+
colorbar_name = 'Return Periods (Years)'
163+
title_subplots = lambda cols: [f'Threshold Intensity: {col} m/s' for col in cols]
164+
(axis1, axis2) = u_plot.plot_from_gdf(
165+
return_periods,
166+
colorbar_name=colorbar_name,
167+
title_subplots=title_subplots)
168+
self.assertEqual('Threshold Intensity: 10.0 m/s', axis1.get_title())
169+
self.assertEqual('Threshold Intensity: 20.0 m/s', axis2.get_title())
170+
plt.close()
171+
172+
def test_plot_from_gdf_log(self):
173+
"""test plot_from_gdf() with log color bar)"""
156174
return_periods = gpd.GeoDataFrame(
157-
data = ((2., 5.), (3., 6.), (None, 2.), (1., 7.)),
175+
data = ((2., 5.), (3., 6.), (None, 2.), (1., 1000.)),
158176
columns = ('10.0', '20.0')
159177
)
160178
return_periods['geometry'] = (Point(45., 26.), Point(46., 26.), Point(45., 27.), Point(46., 27.))
161179
colorbar_name = 'Return Periods (Years)'
162180
title_subplots = lambda cols: [f'Threshold Intensity: {col} m/s' for col in cols]
163-
(axis1, axis2) = u_plot.subplots_from_gdf(
181+
(axis1, axis2) = u_plot.plot_from_gdf(
164182
return_periods,
165183
colorbar_name=colorbar_name,
166184
title_subplots=title_subplots)

doc/tutorial/climada_hazard_Hazard.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@
577577
"<a id='Part5'></a> \n",
578578
"## Part 5: Visualize Hazards\n",
579579
"\n",
580-
"There are three different plot functions: `plot_intensity()`, `plot_fraction()`, and `plot_rp_intensity()`. Depending on the inputs, different properties can be visualized. Check the documentation of the functions. Using the function `local_return_period()` and the util function `subplots_from_gdf()`, one can plot local return periods for specific hazard intensities."
580+
"There are three different plot functions: `plot_intensity()`, `plot_fraction()`, and `plot_rp_intensity()`. Depending on the inputs, different properties can be visualized. Check the documentation of the functions. Using the function `local_return_period()` and the util function `plot_from_gdf()`, one can plot local return periods for specific hazard intensities."
581581
]
582582
},
583583
{
@@ -759,8 +759,8 @@
759759
"\n",
760760
"# 5. tropical cyclone return period maps for the threshold intensities [30, 40]\n",
761761
"return_periods, label, column_label = haz_tc_fl.local_return_period([30, 40])\n",
762-
"from climada.util.plot import subplots_from_gdf\n",
763-
"subplots_from_gdf(return_periods, colorbar_name=label, title_subplots=column_label)\n",
762+
"from climada.util.plot import plot_from_gdf\n",
763+
"plot_from_gdf(return_periods, colorbar_name=label, title_subplots=column_label)\n",
764764
"\n",
765765
"# 6. intensities of all the events in centroid with id 50\n",
766766
"haz_tc_fl.plot_intensity(centr=50)\n",

0 commit comments

Comments
 (0)