7676 indices. This avoids the deletion of coordinate information if different
7777 groups of datasets have different dimensions but increases the memory
7878 footprint of this diagnostic.
79+ write_netcdf: bool, optional (default: False)
80+ Output netCDF file for plotted data. What is written into the file
81+ is decided based on the pandas data frame and the
82+ seaborn kwargs: "x", "y", "hue" and "col".
83+ Because there is no direct way to write panda data frames into netCDF
84+ and to make data CF complient, the data frame is converted to an iris
85+ CubeList first. This is not possible for all data frames and often
86+ reset_index: true is required. Therefore the default is set to False.
7987savefig_kwargs: dict, optional
8088 Optional keyword arguments for :func:`matplotlib.pyplot.savefig`. By
8189 default, uses ``bbox_inches: tight, dpi: 300, orientation: landscape``.
108116import iris
109117import iris .pandas
110118import matplotlib .pyplot as plt
119+ import numpy as np
111120import pandas as pd
112121import seaborn as sns
122+ from iris .cube import CubeList
113123from matplotlib .colors import LogNorm , Normalize
114124
115125from esmvaltool .diag_scripts .shared import (
116126 ProvenanceLogger ,
127+ get_diagnostic_filename ,
117128 get_plot_filename ,
118129 group_metadata ,
130+ io ,
119131 run_diagnostic ,
120132)
121133
@@ -199,6 +211,10 @@ def _create_plot(
199211 [0.83 , pos_joint_ax .y0 , 0.07 , pos_joint_ax .height ]
200212 )
201213
214+ # Save plot data
215+ if cfg ["write_netcdf" ]:
216+ _save_nc_data (data_frame , cfg )
217+
202218 # Save plot
203219 plot_path = get_plot_filename (cfg ["plot_filename" ], cfg )
204220 plt .savefig (plot_path , ** cfg ["savefig_kwargs" ])
@@ -213,8 +229,16 @@ def _create_plot(
213229 "authors" : ["schlund_manuel" ],
214230 "caption" : caption ,
215231 }
216- with ProvenanceLogger (cfg ) as provenance_logger :
217- provenance_logger .log (plot_path , provenance_record )
232+ if cfg ["write_netcdf" ]:
233+ with ProvenanceLogger (cfg ) as provenance_logger :
234+ provenance_logger .log (plot_path , provenance_record )
235+ provenance_logger .log (
236+ get_diagnostic_filename (cfg ["plot_filename" ], cfg ),
237+ provenance_record ,
238+ )
239+ else :
240+ with ProvenanceLogger (cfg ) as provenance_logger :
241+ provenance_logger .log (plot_path , provenance_record )
218242
219243
220244def _get_grouped_data (cfg : dict ) -> dict :
@@ -399,6 +423,7 @@ def _get_default_cfg(cfg: dict) -> dict:
399423 cfg .setdefault ("plot_object_methods" , {})
400424 cfg .setdefault ("plot_filename" , f"seaborn_{ cfg .get ('seaborn_func' , '' )} " )
401425 cfg .setdefault ("reset_index" , False )
426+ cfg .setdefault ("write_netcdf" , False )
402427 cfg .setdefault (
403428 "savefig_kwargs" ,
404429 {
@@ -434,6 +459,13 @@ def _get_plot_func(cfg: dict) -> callable:
434459 return getattr (sns , cfg ["seaborn_func" ])
435460
436461
462+ def _is_strictly_monotonic (arr ):
463+ """Test if np.array is strictly monotonic."""
464+ result = np .all (np .diff (arr ) > 0 ) | np .all (np .diff (arr ) < 0 )
465+
466+ return result
467+
468+
437469def _modify_dataframe (data_frame : pd .DataFrame , cfg : dict ) -> pd .DataFrame :
438470 """Modify data frame according to the option ``data_frame_ops``."""
439471 allowed_funcs = ("query" , "eval" )
@@ -463,6 +495,76 @@ def _modify_dataframe(data_frame: pd.DataFrame, cfg: dict) -> pd.DataFrame:
463495 return data_frame
464496
465497
498+ def _prepare_cube (cube , cubes_to_aux , cubes_to_coord , cfg ):
499+ """Prepare cube to save data as netCDF."""
500+ cube .attributes .globals ["seaborn_func" ] = cfg ["seaborn_func" ]
501+ cube .attributes .globals ["seaborn_kwargs" ] = _get_str_from_kwargs (
502+ cfg ["seaborn_kwargs" ]
503+ )
504+ for auxcube in cubes_to_aux :
505+ aux_coord = iris .coords .AuxCoord (
506+ auxcube .data , long_name = auxcube .long_name
507+ )
508+ # Add the auxiliary coordinate to the cube
509+ cube .add_aux_coord (aux_coord , data_dims = 0 )
510+
511+ for dimcube in cubes_to_coord :
512+ dim_coord = iris .coords .DimCoord (
513+ dimcube .data , long_name = dimcube .long_name
514+ )
515+ # Add the auxiliary coordinate to the cube
516+ cube .add_dim_coord (dim_coord , data_dims = 0 )
517+
518+ return cube
519+
520+
521+ def _save_nc_data (dframe : pd .DataFrame , cfg ) -> None :
522+ """Save netCDF files for plot."""
523+ cubes_to_save = CubeList ()
524+ cubes_to_aux = CubeList ()
525+ cubes_to_coord = CubeList ()
526+
527+ strings_to_save = []
528+ for key in cfg ["seaborn_kwargs" ]:
529+ if key in ["x" , "y" , "hue" , "col" ]:
530+ strings_to_save .append (cfg ["seaborn_kwargs" ][key ])
531+
532+ for something in dframe :
533+ if something in strings_to_save :
534+ testcube = iris .pandas .as_cubes (dframe [something ])[0 ]
535+ testcube .var_name = something
536+ testcube .remove_coord ("unknown" )
537+
538+ if something in UNITS :
539+ testcube .units = UNITS [something ]
540+
541+ if something in ["shape_id" , "dataset" , "alias" ]:
542+ testcube .data = testcube .data .astype (str )
543+ cubes_to_aux .append (testcube )
544+ elif something in [
545+ "latitude" ,
546+ "longitude" ,
547+ "height" ,
548+ "plev" ,
549+ "time" ,
550+ ] and _is_strictly_monotonic (testcube .data ):
551+ cubes_to_coord .append (testcube )
552+ else :
553+ cubes_to_save .append (testcube )
554+
555+ for cube in cubes_to_save :
556+ cube = _prepare_cube (
557+ cube ,
558+ cubes_to_aux ,
559+ cubes_to_coord ,
560+ cfg ,
561+ )
562+
563+ io .iris_save (
564+ cubes_to_save , get_diagnostic_filename (cfg ["plot_filename" ], cfg )
565+ )
566+
567+
466568def _set_legend_title (plot_obj , legend_title : str ) -> None :
467569 """Set legend title."""
468570 if hasattr (plot_obj , "get_legend" ): # Axes
0 commit comments