Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade Taylor Diagram function #874

Merged
merged 17 commits into from
Oct 12, 2022
Merged
248 changes: 203 additions & 45 deletions pcmdi_metrics/graphics/taylor_diagram/taylor_diagram.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,110 @@
import math


def TaylorDiagram(
stddev, corrcoef, refstd, fig, colors,
normalize=True,
labels=None, markers=None, markersizes=None, zorders=None,
ref_label=None, smax=None):
stddev, corrcoef, refstd,
fig=None,
rect=111,
title=None,
titleprops_dict=dict(),
colors=None,
cmap=None,
normalize=False,
labels=None,
markers=None,
markersizes=None,
closed_marker=True,
markercloses=None,
zorders=None,
ref_label=None,
smax=None,
compare_models=None,
arrowprops_dict=None,
annotate_text=None,
radial_axis_title=None,
angular_axis_title="Correlation",
grid=True,
debug=False):

"""Plot a Taylor diagram

Jiwoo Lee (PCMDI LLNL) - last update: 2022. 10

"""Plot a Taylor diagram.
This code was adpated from the ILAMB code by Nathan Collier found here:
https://github.com/rubisco-sfa/ILAMB/blob/master/src/ILAMB/Post.py#L80,
which was revised by Jiwoo Lee to enable more flexible customization of the plot.
This code was adpated from the ILAMB code that was written by Nathan Collier (ORNL)
(https://github.com/rubisco-sfa/ILAMB/blob/master/src/ILAMB/Post.py#L80)
and revised by Jiwoo Lee (LLNL) to add capabilities and enable more customizations
for implementation into PCMDI Metrics Package (PMP).
The original code was written by Yannick Copin (https://gist.github.com/ycopin/3342888)

Reference for Taylor Diagram:
Taylor, K. E. (2001), Summarizing multiple aspects of model performance in a single diagram,
J. Geophys. Res., 106(D7), 7183–7192, http:https://dx.doi.org/10.1029/2000JD900719

The original code was written by Yannick Copin that can be found here:
https://gist.github.com/ycopin/3342888

Parameters
----------
stddev : numpy.ndarray
an array of standard deviations
corrcoeff : numpy.ndarray
corrcoef : numpy.ndarray
an array of correlation coefficients
refstd : float
the reference standard deviation
fig : matplotlib figure
fig : matplotlib figure, optional
the matplotlib figure
colors : array
rect : a 3-digit integer, optional
ax subplot rect, , default is 111, which indicate the figure has 1 row, 1 column, and this plot is the first plot.
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplot.html
https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.add_subplot
title : string, optional
title for the plot
titleprops_dict : dict, optional
title property dict (e.g., fontsize)
cmap : string, optional
a name of matplotlib colormap
https://matplotlib.org/stable/gallery/color/colormap_reference.html
colors : array, optional
an array or list of colors for each element of the input arrays
if colors is given, it will override cmap
normalize : bool, optional
disable to skip normalization of the standard deviation
default is False
labels : list, optional
list of text for labels
markers : list, optional
list of marker type
markersizes : list, optional
list of integer for marker size
closed_marker : bool, optional
closed marker or opened marker
default - True
markercloses : list of bool, optional
When closed_marker is False but you still want to have a few closed markers among opened markers, provide list of True (close) or False (open)
default - None
zorders : list, optional
list of integer for zorder
ref_label : str, optional
label for reference data
smax : int or float, optional
maximum of axis range for (normalized) standard deviation
compare_models : list of tuples, optional
list of pair of two models to compare by showing arrows
arrowprops_dict: dict, optional
dict for matplotlib annotation arrowprops for compare_models arrow
See https://matplotlib.org/stable/tutorials/text/annotations.html for details
annotate_text : string, optional
text to place at the begining of the comparing arrow
radial_axis_title : string, optional
axis title for radial axis
default - Standard deviation (when normalize=False) or Normalized standard deviation (when normalize=True)
angular_axis_title : string, optional
axis title for angular axis
default - Correlation
grid : bool, optional
grid line in plot
default - True
debug : bool, optional
default - False
if true print some interim results for debugging purpose

Return
------
Expand All @@ -46,6 +113,7 @@ def TaylorDiagram(
ax : matplotlib axis
the matplotlib axis
"""
import matplotlib.pyplot as plt
import mpl_toolkits.axisartist.floating_axes as FA
import mpl_toolkits.axisartist.grid_finder as GF
import numpy as np
Expand All @@ -64,6 +132,8 @@ def TaylorDiagram(
if normalize:
stddev = stddev / refstd
refstd = 1.

# Radial axis range
smin = 0
if smax is None:
smax = max(2.0, 1.1 * stddev.max())
Expand All @@ -73,64 +143,152 @@ def TaylorDiagram(
extremes=(0, np.pi / 2, smin, smax),
grid_locator1=gl1,
tick_formatter1=tf1)
ax = FA.FloatingSubplot(fig, 111, grid_helper=ghelper)
fig.add_subplot(ax)

if fig is None:
fig = plt.figure(figsize=(8, 8))

ax = fig.add_subplot(
rect, axes_class=FA.FloatingAxes, grid_helper=ghelper)

if title is not None:
ax.set_title(title, **titleprops_dict)

if colors is None:
if cmap is None:
cmap = 'viridis'
cm = plt.get_cmap(cmap)
colors = cm(np.linspace(0.1, 0.9, len(stddev)))

if radial_axis_title is None:
if normalize:
radial_axis_title = "Normalized standard deviation"
else:
radial_axis_title = "Standard deviation"

# adjust axes
ax.axis["top"].set_axis_direction("bottom")
ax.axis["top"].toggle(ticklabels=True, label=True)
ax.axis["top"].major_ticklabels.set_axis_direction("top")
ax.axis["top"].label.set_axis_direction("top")
ax.axis["top"].label.set_text("Correlation")
ax.axis["top"].label.set_text(angular_axis_title)
ax.axis["left"].set_axis_direction("bottom")
if normalize:
ax.axis["left"].label.set_text("Normalized standard deviation")
else:
ax.axis["left"].label.set_text("Standard deviation")
ax.axis["left"].label.set_text(radial_axis_title)
ax.axis["right"].set_axis_direction("top")
ax.axis["right"].toggle(ticklabels=True)
ax.axis["right"].major_ticklabels.set_axis_direction("left")
ax.axis["bottom"].set_visible(False)
ax.grid(True)
ax.grid(grid)

ax = ax.get_aux_axes(tr)

# Add reference point and stddev contour
ax.plot([0], refstd, 'k*', ms=12, mew=0, label=ref_label)
t = np.linspace(0, np.pi / 2)
r = np.zeros_like(t) + refstd
ax.plot(t, r, 'k--')

# centralized rms contours
rs, ts = np.meshgrid(np.linspace(smin, smax),
np.linspace(0, np.pi / 2))
rms = np.sqrt(refstd**2 + rs**2 - 2 * refstd * rs * np.cos(ts))
contours = ax.contour(ts, rs, rms, 5, colors='k', alpha=0.4)
ax.clabel(contours, fmt='%1.1f')

# Plot data
corrcoef = corrcoef.clip(-1, 1)
for i in range(len(corrcoef)):
# --- customize start ---
# customize label
if labels is not None:
label = labels[i]
else:
if labels is None:
label = None
# customize marker
if markers is not None:
marker = markers[i]
else:
label = labels[i]
# customize marker
if markers is None:
marker = 'o'
# customize marker size
if markersizes is not None:
ms = markersizes[i]
else:
marker = markers[i]
# customize marker size
if markersizes is None:
ms = 8
else:
ms = markersizes[i]
# customize marker order
if zorders is not None:
if zorders is None:
zorder = None
else:
zorder = zorders[i]
# customize marker closed/opened
if closed_marker:
markerclose = True
else:
zorder = None
# customize end
ax.plot(np.arccos(corrcoef[i]), stddev[i], marker, color=colors[i], mew=0, ms=ms, label=label, zorder=zorder)
if markercloses is None:
markerclose = False
else:
markerclose = markercloses[i]

# Add reference point and stddev contour
l, = ax.plot([0], refstd, 'k*', ms=12, mew=0, label=ref_label)
t = np.linspace(0, np.pi / 2)
r = np.zeros_like(t) + refstd
ax.plot(t, r, 'k--')
if markerclose:
if closed_marker:
marker_dict = dict(
color=colors[i],
mew=0,
)
else:
marker_dict = dict(
mfc=colors[i],
mec='k',
mew=1
)
else:
marker_dict = dict(
mec=colors[i],
mfc='none',
mew=1,
)
# --- customize end ---

# centralized rms contours
rs, ts = np.meshgrid(np.linspace(smin, smax),
np.linspace(0, np.pi / 2))
rms = np.sqrt(refstd**2 + rs**2 - 2 * refstd * rs * np.cos(ts))
contours = ax.contour(ts, rs, rms, 5, colors='k', alpha=0.4)
ax.clabel(contours, fmt='%1.1f')
# -------------------------
# place marker on the graph
# -------------------------
ax.plot(
np.arccos(corrcoef[i]),
stddev[i],
marker,
ms=ms,
label=label,
zorder=zorder,
**marker_dict)

# debugging
if debug:
crmsd = math.sqrt(stddev[i]**2 + refstd**2 - 2 * stddev[i] * refstd * corrcoef[i]) # centered rms difference
print(
'i, label, corrcoef[i], np.arccos(corrcoef[i]), stddev[i], crmsd:',
i, label, corrcoef[i], np.arccos(corrcoef[i]), stddev[i], crmsd)

# Add arrow(s)
if arrowprops_dict is None:
arrowprops_dict = dict(facecolor='black',
lw=0.5,
width=0.5,
shrink=0.05) # shrink arrow length little bit to make it look good...
if compare_models is not None:
for compare_models_pair in compare_models:
index_model1 = labels.index(compare_models_pair[0])
index_model2 = labels.index(compare_models_pair[1])
theta1 = np.arccos(corrcoef[index_model1])
theta2 = np.arccos(corrcoef[index_model2])
r1 = stddev[index_model1]
r2 = stddev[index_model2]

ax.annotate(
annotate_text,
xy=(theta2, r2), # theta, radius of arrival
xytext=(theta1, r1), # theta, radius of departure
xycoords='data',
textcoords='data',
arrowprops=arrowprops_dict,
horizontalalignment='center',
verticalalignment='center')

return fig, ax
Loading