###############################################################################
## ##
## LIBRARIES ##
## ##
###############################################################################
# Standard library imports
import random
import itertools
from types import SimpleNamespace
from pathlib import Path
# Data handling and plotting libraries
import numpy as np
import matplotlib.pyplot as plt
import skill_metrics as sm
from matplotlib.lines import Line2D
# Module utils
from Hydrological_model_validator.Processing.Taylor_computations import compute_yearly_taylor_stats, build_all_points
from Hydrological_model_validator.Processing.utils import extract_options
# Mosule formatting and default options
from Hydrological_model_validator.Plotting.formatting import get_variable_label_unit
from Hydrological_model_validator.Plotting.default_taylor_options import (
default_taylor_base_options,
default_taylor_ref_marker_options,
default_taylor_data_marker_options,
default_taylor_plt_options,
default_marker_shapes,
default_monthly_taylor_base_options,
default_monthly_ref_marker_options,
default_monthly_data_marker_options,
default_month_colors,
default_monthly_plt_options,
)
###############################################################################
## ##
## FUNCTIONS ##
## ##
###############################################################################
[docs]
def comprehensive_taylor_diagram(data_dict, **kwargs):
"""
Plot a comprehensive yearly Taylor diagram comparing model and satellite statistics.
Parameters
----------
data_dict : dict
Dictionary with model and satellite data organized by year.
Other Parameters
----------------
**kwargs
Additional keyword arguments include:
output_path : str or Path
Required. Directory where the figure is saved.
variable_name : str
Short name used to infer full variable name and unit.
variable : str
Full variable name (e.g., "Chlorophyll").
unit : str
Unit of measurement (e.g., "mg Chl/m³").
marker_shapes : list of str
Marker symbols used for each year (e.g., ['o', 's', 'D']).
Plot Options:
figsize : tuple of float
Figure size in inches.
dpi : int
Plot resolution.
title : str
Plot title.
title_fontsize : int
Font size of the title.
title_fontweight : str or int
Font weight of the title.
title_pad : float
Padding between title and plot.
Base Taylor Options:
tickrms : list of float
RMSD contours for base diagram.
titleRMS : str
Turn RMS title on/off.
Reference Marker Options (prefix `Ref_`):
Ref_markersymbol : str
Marker symbol for reference overlay.
Ref_markercolor : str or tuple
Marker face or edge color.
Ref_markersize : float
Marker size.
Data Marker Options:
markersymbol : str
Marker symbol for yearly points.
markercolor : str or tuple
Marker color.
markersize : float
Marker size.
Raises
------
ValueError
If neither 'variable_name' nor both 'variable' and 'unit' are provided.
"""
# ----- CONVERT KWARGS TO NAMESPACE -----
options = SimpleNamespace(**kwargs)
options.output_path = getattr(options, "output_path", None)
options.variable = getattr(options, "variable", None)
options.unit = getattr(options, "unit", None)
options.variable_name = getattr(options, "variable_name", None)
# ----- RETRIEVE NECESSARY OUTPUT PATH AND VARIABLE/UNIT INFO -----
if options.output_path is None:
raise ValueError("output_path must be specified either in kwargs or default options.")
if options.variable_name is not None:
variable, unit = get_variable_label_unit(options.variable_name)
options.variable = options.variable or variable
options.unit = options.unit or unit
else:
if options.variable is None or options.unit is None:
raise ValueError(
"If 'variable_name' is not provided, both 'variable' and 'unit' must be specified in kwargs or defaults."
)
stats_by_year, std_ref = compute_yearly_taylor_stats(data_dict)
# ----- ASSIGN SHAPES FOR EACH DATA ENTRY -----
user_shapes = getattr(options, "marker_shapes", default_marker_shapes)
labels = ["Ref"] + [entry[0] for entry in stats_by_year]
if len(user_shapes) < len(labels) - 1:
all_markers = [m for m in Line2D.markers.keys() if isinstance(m, str) and m not in user_shapes and m != ' ']
user_shapes += random.sample(all_markers, len(labels) - 1 - len(user_shapes))
# ----- NORMALIZE THE DATA -----
sdev = np.array([std_ref] + [e[1] for e in stats_by_year]) / std_ref
crmsd = np.array([0.0] + [e[2] for e in stats_by_year]) / std_ref
ccoef = np.array([1.0] + [e[3] for e in stats_by_year])
# ----- FETCH DEFAULT OPTIONS -----
base_opts = extract_options(kwargs, default_taylor_base_options)
ref_opts = extract_options(kwargs, default_taylor_ref_marker_options, prefix="Ref_")
data_opts = extract_options(kwargs, default_taylor_data_marker_options)
plt_opts = extract_options(kwargs, default_taylor_plt_options)
# ----- SET REFERENCE MARKER VALUES -----
sdev_ref = kwargs.pop("sdevRef", 1.0)
crmsd_ref = kwargs.pop("crmsdRef", 0.0)
ccoef_ref = kwargs.pop("ccoefRef", 1.0)
# ----- CREATE FIGURE -----
plt.figure(figsize=plt_opts.get('figsize'), dpi=plt_opts.get('dpi'))
# ----- SET TITLE -----
plt.title(
plt_opts.get('title', f"Yearly Taylor Diagram (Normalized Stats) | {options.variable}"),
pad=plt_opts.get('title_pad'),
fontsize=plt_opts.get('title_fontsize'),
fontweight=plt_opts.get('title_fontweight')
)
# ----- BASE DIAGRAM -----
sm.taylor_diagram(
sdev, crmsd, ccoef,
markersymbol='none',
markercolors={"face": "none", "edge": "none"},
markersize=0,
alpha=0,
**base_opts
)
# ----- CREATE LABELS -----
ax = plt.gca()
y_min, y_max = ax.get_ylim()
label_y = y_min - 0.11 * (y_max - y_min)
tickrms = base_opts.get("tickrms", [0.5])
first_tickrms = tickrms[0] if tickrms else 0.5
plt.text(sdev[0], label_y, "Ref", ha="center", va="center", fontsize=16, fontweight='bold', color='r')
if base_opts.get('titleRMS', 'off') == 'off':
plt.text(sdev[0] + first_tickrms, label_y, 'RMSD', fontsize=12,
ha='center', va='center', fontweight='bold', color=(0.0, 0.6, 0.0))
# ----- OVERLAY REFERENCE -----
sm.taylor_diagram(
np.array([sdev_ref] * 2),
np.array([crmsd_ref] * 2),
np.array([ccoef_ref] * 2),
tickrms=[0.0],
overlay='on',
**ref_opts
)
# ----- OVERLAY DATA POINTS -----
# When there's only one year of data:
if len(sdev[1:]) == 1:
ax.plot(
sdev[1], crmsd[1],
marker=user_shapes[0],
markerfacecolor='r',
markeredgecolor='r',
linestyle='None',
**data_opts
)
else:
# Normal Taylor diagram overlay for multiple points
for shape, (x, y, c) in zip(itertools.cycle(user_shapes), zip(sdev[1:], crmsd[1:], ccoef[1:])):
sm.taylor_diagram(
np.array([x, x]),
np.array([y, y]),
np.array([c, c]),
showlabelsrms='off',
overlay='on',
markersymbol=shape,
**data_opts
)
# ----- CHECK DIRECTORY EXISTENCE -----
Path(options.output_path).mkdir(parents=True, exist_ok=True)
# ----- PRINT AND SAVE PLOT -----
plt.savefig(Path(options.output_path) / 'Taylor_diagram_summary.png')
plt.close()
###############################################################################
###############################################################################
[docs]
def monthly_taylor_diagram(data_dict, **kwargs):
"""
Plot a unified Taylor diagram with points for each month and year.
Parameters
----------
data_dict : dict
Dictionary containing model and satellite data for each month and year.
Other Parameters
----------------
**kwargs
Additional keyword arguments include:
output_path : str or Path
Required. Directory where the figure is saved.
variable_name : str
Short name used to infer full variable name and unit.
variable : str
Full variable name (e.g., "Chlorophyll").
unit : str
Unit of measurement (e.g., "mg Chl/m³").
Plot Options:
title : str
Plot title.
figsize : tuple of float
Figure size in inches.
dpi : int
Plot resolution.
title_pad : float
Padding between title and plot.
title_fontsize : int
Font size of the title.
title_fontweight : str or int
Font weight of the title.
Raises
------
ValueError
If neither 'variable_name' nor both 'variable' and 'unit' are provided.
"""
# ----- CONVERT KWARGS TO NAMESPACE -----
options = SimpleNamespace(**kwargs)
options.output_path = getattr(options, "output_path", None)
options.variable = getattr(options, "variable", None)
options.unit = getattr(options, "unit", None)
options.variable_name = getattr(options, "variable_name", None)
# ----- RETRIEVE NECESSARY OUTPUT PATH AND VARIABLE/UNIT INFO -----
if options.output_path is None:
raise ValueError("output_path must be specified either in kwargs or default options.")
if options.variable_name is not None:
variable, unit = get_variable_label_unit(options.variable_name)
options.variable = options.variable or variable
options.unit = options.unit or unit
else:
if options.variable is None or options.unit is None:
raise ValueError(
"If 'variable_name' is not provided, both 'variable' and 'unit' must be specified in kwargs or defaults."
)
# ----- PREPARE OUTPUT DIRECTORY -----
output_path = Path(options.output_path)
output_path.mkdir(parents=True, exist_ok=True)
# ----- BUILD DATAFRAME AND YEARS -----
df, years = build_all_points(data_dict)
# ----- EXTRACT PLOTTING OPTIONS -----
plt_opts = extract_options(kwargs, default_monthly_plt_options)
# ----- CREATE FIGURE -----
plt.figure(figsize=plt_opts["figsize"], dpi=plt_opts["dpi"])
# ----- SET TITLE -----
plt.title(
kwargs.get("title", f"Monthly Taylor Diagram (Normalized Stats) | {options.variable}"),
pad=plt_opts["title_pad"],
fontsize=plt_opts["title_fontsize"],
fontweight=plt_opts["title_fontweight"],
)
# ----- BASE DIAGRAM -----
sm.taylor_diagram(
df["sdev"].values,
df["crmsd"].values,
df["ccoef"].values,
**default_monthly_taylor_base_options
)
# ----- REFERENCE POINT AND LABEL -----
ref = df[df["year"] == "Ref"].iloc[0]
ax = plt.gca()
y_min, y_max = ax.get_ylim()
label_y = y_min - 0.09 * (y_max - y_min)
tickrms = default_monthly_taylor_base_options.get("tickrms")
first_tickrms = tickrms[0] if isinstance(tickrms, (list, tuple)) and tickrms else 0.5
plt.text(ref["sdev"], label_y, "Ref", ha="center", va="center", fontsize=16, fontweight='bold', color='r')
if default_monthly_taylor_base_options.get('titleRMS', 'off') == 'off':
plt.text(ref["sdev"] + first_tickrms, label_y, 'RMSD', fontsize=12,
ha='center', va='center', fontweight='bold', color=(0.0, 0.6, 0.0))
# ----- OVERLAY REFERENCE MARKER -----
sm.taylor_diagram(
np.array([ref["sdev"], ref["sdev"]]),
np.array([ref["crmsd"], ref["crmsd"]]),
np.array([ref["ccoef"], ref["ccoef"]]),
**default_monthly_ref_marker_options
)
# ----- PLOT MONTHLY POINTS -----
non_ref = df[df["year"] != "Ref"]
for _, row in non_ref.iterrows():
month_color = default_month_colors[row["month"]]
year_index = years.index(row["year"]) if row["year"] in years else -1
marker_shape = default_marker_shapes[year_index % len(default_marker_shapes)]
sm.taylor_diagram(
np.array([row["sdev"], row["sdev"]]),
np.array([row["crmsd"], row["crmsd"]]),
np.array([row["ccoef"], row["ccoef"]]),
markersymbol=marker_shape,
markercolors={
"face": month_color,
"edge": default_monthly_data_marker_options["markercolors_edge"],
},
markersize=default_monthly_data_marker_options["markersize"],
alpha=default_monthly_data_marker_options["alpha"],
overlay=default_monthly_data_marker_options["overlay"],
)
# ----- SAVE AND PRINT FIGURE -----
save_path = output_path / "Unified_Taylor_Diagram.png"
plt.savefig(save_path)
plt.close()
###############################################################################