diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1f4795e..baf6e46f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Removed: - `util.lines_polys_handler` solve polygon disaggregation issue in metre-based projection [#666](https://github.com/CLIMADA-project/climada_python/pull/666) - Problem with `pyproj.CRS` as `Impact` attribute, [#706](https://github.com/CLIMADA-project/climada_python/issues/706). Now CRS is always stored as `str` in WKT format. - Correctly handle assertion errors in `Centroids.values_from_vector_files` and fix the associated test [#768](https://github.com/CLIMADA-project/climada_python/pull/768/) +- Text in `Forecast` class plots can now be adjusted [#769](https://github.com/CLIMADA-project/climada_python/issues/769) ### Deprecated diff --git a/climada/engine/forecast.py b/climada/engine/forecast.py index 6549b5fe4..02d470b62 100644 --- a/climada/engine/forecast.py +++ b/climada/engine/forecast.py @@ -313,6 +313,7 @@ def calc(self, force_reassign=False): def plot_imp_map( self, run_datetime=None, + explain_str=None, save_fig=True, close_fig=False, polygon_file=None, @@ -321,13 +322,17 @@ def plot_imp_map( figsize=(9, 13), adapt_fontsize=True, ): - """plot a map of the impacts + """ plot a map of the impacts Parameters ---------- run_datetime : datetime.datetime, optional Select the used hazard by the run_datetime, default is first element of attribute run_datetime. + explain_str : str, optional + Short str which explains type of impact, explain_str is included + in the title of the figure. + default is 'mean building damage caused by wind' save_fig : bool, optional Figure is saved if True, folder is within your configurable save_dir and filename is derived from the method summary_str() @@ -372,7 +377,7 @@ def plot_imp_map( "run_start": ( run_datetime.strftime("%d.%m.%Y %HUTC +") + lead_time_str + "d" ), - "explain_text": ("mean building damage caused by wind"), + "explain_text": "mean building damage caused by wind" if explain_str is None else explain_str, "model_text": "CLIMADA IMPACT", } fig, axes = self._plot_imp_map( @@ -526,15 +531,24 @@ def _plot_imp_map( return fig, axis_sub def plot_hist( - self, run_datetime=None, save_fig=True, close_fig=False, figsize=(9, 8) + self, + run_datetime=None, + explain_str=None, + save_fig=True, + close_fig=False, + figsize=(9, 8), ): - """plot histogram of the forecasted impacts all ensemble members + """ plot histogram of the forecasted impacts all ensemble members Parameters ---------- run_datetime : datetime.datetime, optional Select the used hazard by the run_datetime, default is first element of attribute run_datetime. + explain_str : str, optional + Short str which explains type of impact, explain_str is included + in the title of the figure. + default is 'total building damage' save_fig : bool, optional Figure is saved if True, folder is within your configurable save_dir and filename is derived from the method summary_str() @@ -603,7 +617,7 @@ def plot_hist( axes.xaxis.set_ticks(x_ticks) axes.xaxis.set_ticklabels(x_ticklabels) plt.xticks(rotation=15, horizontalalignment="right") - plt.xlim([(10**-0.25) * bins[0], (10**0.25) * bins[-1]]) + plt.xlim([(10 ** -0.25) * bins[0], (10 ** 0.25) * bins[-1]]) lead_time_str = "{:.0f}".format( self.lead_time(run_datetime).days @@ -614,7 +628,7 @@ def plot_hist( "run_start": ( run_datetime.strftime("%d.%m.%Y %HUTC +") + lead_time_str + "d" ), - "explain_text": ("total building damage"), + "explain_text": ("total building damage") if explain_str is None else explain_str, "model_text": "CLIMADA IMPACT", } title_position = { @@ -656,8 +670,9 @@ def plot_hist( plt.text( 0.75, 0.85, - "mean damage:\nCHF " - + self._number_to_str(self._impact[haz_ind].at_event.mean()), + "mean impact:\n " + + self._number_to_str(self._impact[haz_ind].at_event.mean()) + + ' ' + self._impact[haz_ind].unit, horizontalalignment="center", verticalalignment="center", transform=axes.transAxes, @@ -740,7 +755,7 @@ def plot_exceedence_prob( The default is (9, 13) adapt_fontsize : bool, optional If set to true, the size of the fonts will be adapted to the size of the figure. - Otherwise the default matplotlib font size is used. Default is True. + Otherwise, the default matplotlib font size is used. Default is True. Returns ------- @@ -750,10 +765,10 @@ def plot_exceedence_prob( if run_datetime is None: run_datetime = self.run_datetime[0] haz_ind = np.argwhere(np.isin(self.run_datetime, run_datetime))[0][0] - wind_map_file_name = ( + exceedence_map_file_name = ( self.summary_str(run_datetime) + "_exceed_" + str(threshold) + "_map.jpeg" ) - wind_map_file_name_full = FORECAST_PLOT_DIR / wind_map_file_name + exceedence_map_file_name_full = FORECAST_PLOT_DIR / exceedence_map_file_name lead_time_str = "{:.0f}".format( self.lead_time(run_datetime).days + self.lead_time(run_datetime).seconds / 60 / 60 / 24 @@ -783,7 +798,7 @@ def plot_exceedence_prob( adapt_fontsize=adapt_fontsize, ) if save_fig: - plt.savefig(wind_map_file_name_full) + plt.savefig(exceedence_map_file_name_full) if close_fig: plt.clf() plt.close(fig) @@ -974,7 +989,7 @@ def plot_warn_map( Figure is not drawn if True. The default is False. adapt_fontsize : bool, optional If set to true, the size of the fonts will be adapted to the size of the figure. - Otherwise the default matplotlib font size is used. Default is True. + Otherwise, the default matplotlib font size is used. Default is True. Returns ------- @@ -1086,7 +1101,7 @@ def _plot_warn( decision_dict_functions[aggregation] = np.mean else: raise ValueError( - "Parameter area_aggregation of " + "Parameter " + aggregation + " of " + "Forecast.plot_warn_map() must eiter be " + "a float between [0..1], which " + "specifys a quantile. or 'sum' or 'mean'." diff --git a/climada/engine/test/test_forecast.py b/climada/engine/test/test_forecast.py index 3ca7f06e5..460656ac3 100644 --- a/climada/engine/test/test_forecast.py +++ b/climada/engine/test/test_forecast.py @@ -104,7 +104,8 @@ class TestPlot(unittest.TestCase): def test_Forecast_plot(self): """Test cplotting functions from the Forecast class""" - #hazard + ## given a forecast based on hazard exposure and vulnerability + #hazard haz1 = StormEurope.from_cosmoe_file( HAZ_DIR.joinpath('storm_europe_cosmoe_forecast_vmax_testfile.nc'), run_datetime=dt.datetime(2018,1,1), @@ -149,8 +150,10 @@ def test_Forecast_plot(self): for f in source: if f['properties']['adm0_a3'] == 'CHE': sink.write(f) - #test plotting functions + ## test plotting functions + # should save plot without failing forecast.plot_imp_map(run_datetime=dt.datetime(2017,12,31), + explain_str='test text', polygon_file=str(cantons_file), save_fig=True, close_fig=True) map_file_name = (forecast.summary_str(dt.datetime(2017,12,31)) + @@ -158,12 +161,30 @@ def test_Forecast_plot(self): '.jpeg') map_file_name_full = Path(FORECAST_PLOT_DIR) / map_file_name map_file_name_full.absolute().unlink(missing_ok=False) - forecast.plot_hist(run_datetime=dt.datetime(2017,12,31), - save_fig=False, close_fig=True) - forecast.plot_exceedence_prob(run_datetime=dt.datetime(2017,12,31), - threshold=5000, save_fig=False, close_fig=True) - - + #should contain title strings + ax = forecast.plot_hist(run_datetime=dt.datetime(2017,12,31), + explain_str='test text', + save_fig=False, close_fig=False) + title_artists = ax.get_figure().get_children() + title_texts = [x.get_text() for x in title_artists if isinstance(x, plt.Text)] + self.assertIn('test text', title_texts) + self.assertIn('Wed 03 Jan 2018 00-24UTC', title_texts) + self.assertIn('31.12.2017 00UTC +3d', title_texts) + #should contain average impact in axes + artists = ax.get_children() + texts = [x.get_text() for x in artists if type(x) == plt.Text] + self.assertIn('mean impact:\n 26 USD', texts) + ax.get_figure().clf() + #should contain title strings + ax = forecast.plot_exceedence_prob(run_datetime=dt.datetime(2017,12,31), + threshold=5000, explain_str='test text exceedence', + save_fig=False, close_fig=False)[0][0] + title_artists = ax.get_figure().get_children() + title_texts = [x.get_text() for x in title_artists if isinstance(x, plt.Text)] + self.assertIn('test text exceedence', title_texts) + self.assertIn('Wed 03 Jan 2018 00-24UTC', title_texts) + self.assertIn('31.12.2017 00UTC +3d', title_texts) + ax.get_figure().clf() forecast.plot_warn_map(str(cantons_file), decision_level = 'polygon', thresholds=[100000,500000, @@ -187,9 +208,10 @@ def test_Forecast_plot(self): close_fig=True) forecast.plot_hexbin_ei_exposure() plt.close() - with self.assertRaises(ValueError): + # should fail because of invalid decision_level + with self.assertRaises(ValueError) as cm: forecast.plot_warn_map(str(cantons_file), - decision_level = 'test_fail', + decision_level='test_fail', probability_aggregation=0.2, area_aggregation=0.2, title="Building damage warning", @@ -197,9 +219,13 @@ def test_Forecast_plot(self): save_fig=False, close_fig=True) plt.close() - with self.assertRaises(ValueError): + self.assertIn( + "Parameter decision_level", str(cm.exception) + ) + # should fail because of invalid probability_aggregation + with self.assertRaises(ValueError) as cm: forecast.plot_warn_map(str(cantons_file), - decision_level = 'exposure_point', + decision_level='exposure_point', probability_aggregation='test_fail', area_aggregation=0.2, title="Building damage warning", @@ -207,9 +233,13 @@ def test_Forecast_plot(self): save_fig=False, close_fig=True) plt.close() - with self.assertRaises(ValueError): + self.assertIn( + "Parameter probability_aggregation", str(cm.exception) + ) + # should fail because of invalid area_aggregation + with self.assertRaises(ValueError) as cm: forecast.plot_warn_map(str(cantons_file), - decision_level = 'exposure_point', + decision_level='exposure_point', probability_aggregation=0.2, area_aggregation='test_fail', title="Building damage warning", @@ -217,6 +247,9 @@ def test_Forecast_plot(self): save_fig=False, close_fig=True) plt.close() + self.assertIn( + "Parameter area_aggregation", str(cm.exception) + ) # Execute Tests