From 3b0680f3d89bec5a9aa0bdebf7023bc9a3fa582d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Tue, 28 May 2024 18:11:49 -0400 Subject: [PATCH 01/15] normalized_taylor diagram --- figanos/matplotlib/__init__.py | 1 + figanos/matplotlib/plot.py | 181 ++++++++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 4 deletions(-) diff --git a/figanos/matplotlib/__init__.py b/figanos/matplotlib/__init__.py index 974fcdad..7dfcb3b9 100644 --- a/figanos/matplotlib/__init__.py +++ b/figanos/matplotlib/__init__.py @@ -5,6 +5,7 @@ gridmap, hatchmap, heatmap, + normalized_taylordiagram, partition, scattermap, stripes, diff --git a/figanos/matplotlib/plot.py b/figanos/matplotlib/plot.py index 858f888e..1699f286 100644 --- a/figanos/matplotlib/plot.py +++ b/figanos/matplotlib/plot.py @@ -1827,6 +1827,7 @@ def taylordiagram( legend_kw: dict[str, Any] | None = None, std_label: str | None = None, corr_label: str | None = None, + floating_ax: FloatingSubplot | None = None, ): """Build a Taylor diagram. @@ -2023,7 +2024,8 @@ def taylordiagram( ax.clabel(ct, ct.levels, fontsize=8) - ct_line = Line2D( + # points.append(ct_line) + ct_line = ax.plot( [0], [0], ls=contours_kw["linestyles"], @@ -2031,7 +2033,7 @@ def taylordiagram( c="k" if "colors" not in contours_kw else contours_kw["colors"], label="rmse", ) - points.append(ct_line) + points.append(ct_line[0]) # get color options style_colors = matplotlib.rcParams["axes.prop_cycle"].by_key()["color"] @@ -2061,9 +2063,180 @@ def taylordiagram( # legend legend_kw.setdefault("loc", "upper right") - fig.legend(points, [pt.get_label() for pt in points], **legend_kw) + legend = fig.legend(points, [pt.get_label() for pt in points], **legend_kw) - return floating_ax + return fig, floating_ax, legend + + +def normalized_taylordiagram( + data: xr.DataArray | dict[str, xr.DataArray], + plot_kw: dict[str, Any] | None = None, + fig_kw: dict[str, Any] | None = None, + std_range: tuple = (0, 1.5), + contours: int | None = 4, + contours_kw: dict[str, Any] | None = None, + legend_kw: dict[str, Any] | None = None, + std_label: str | None = None, + corr_label: str | None = None, + markers_dim: str | dict | None = None, + colors_dim: str | dict | None = None, +): + """Build a Taylor diagram. + + Based on the following code: https://gist.github.com/ycopin/3342888. + + Parameters + ---------- + data : xr.DataArray or dict + DataArray or dictionary of DataArrays created by xclim.sdba.measures.taylordiagram, each corresponding + to a point on the diagram. The dictionary keys will become their labels. + plot_kw : dict, optional + Arguments to pass to the `plot()` function. Changes how the markers look. + If 'data' is a dictionary, must be a nested dictionary with the same keys as 'data'. + fig_kw : dict, optional + Arguments to pass to `plt.figure()`. + std_range : tuple + Range of the x and y axes, in units of the highest standard deviation in the data. + contours : int, optional + Number of rsme contours to plot. + contours_kw : dict, optional + Arguments to pass to `plt.contour()` for the rmse contours. + legend_kw : dict, optional + Arguments to pass to `plt.legend()`. + std_label : str, optional + Label for the standard deviation (x and y) axes. + corr_label : str, optional + Label for the correlation axis. + + Returns + ------- + matplotlib.axes.Axes + """ + plot_kw = empty_dict(plot_kw) + fig_kw = empty_dict(fig_kw) + contours_kw = empty_dict(contours_kw) + legend_kw = empty_dict(legend_kw) + + if not std_label: + try: + std_label = get_localized_term("Normalized standard deviation") + except AttributeError: + std_label = get_localized_term("Normalized standard deviation").capitalize() + + # convert SSP, RCP, CMIP formats in keys + if isinstance(data, dict): + data = process_keys(data, convert_scen_name) + if isinstance(plot_kw, dict): + plot_kw = process_keys(plot_kw, convert_scen_name) + if not plot_kw: + plot_kw = {} + # if only one data input, insert in dict. + if not isinstance(data, dict): + data = {"_no_label": data} # mpl excludes labels starting with "_" from legend + plot_kw = {"_no_label": empty_dict(plot_kw)} + + # markers/colors are attributed to given dimensions, if specified + if markers_dim is not None or colors_dim is not None: + keys = list(data.keys()) + if len(keys) > 1: + raise ValueError( + "Can only plot one dimension if many DataArrays are given as input" + ) + + da = data[keys[0]] + plot_kw = plot_kw[keys[0]] + dims = [] + if markers_dim: + if isinstance(markers_dim, str): + # do not use "s", it's used for reference + default_markers = [ + "o", + "D", + "v", + "^", + "<", + ">", + "p", + "*", + "h", + "H", + "+", + "x", + "|", + "_", + ] + markers = [ + default_markers[i % len(default_markers)] + for i in range(da[markers_dim].size) + ] + else: + markers = list(markers_dim.values())[0] + markers_dim = list(markers_dim.keys())[0] + markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} + dims.append(markers_dim) + if colors_dim: + if isinstance(colors_dim, str): + colors = [f"C{i}" for i in range(da[colors_dim].size)] + else: + colors = list(colors_dim.values())[0] + colors_dim = list(colors_dim.keys())[0] + colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} + dims.append(colors_dim) + da = da.stack(pl_dims=dims) + for i, key in enumerate(da.pl_dims.values): + if isinstance(key, list) or isinstance(key, tuple): + key = "_".join([str(k) for k in key]) + data[key] = da.isel(pl_dims=i) + data.pop("_no_label") + plot_kw = {k: empty_dict(plot_kw) for k in data.keys()} + + # normalize data (such that ref_std == 1, unitless) + for k in data.keys(): + data[k][{"taylor_param": 1}] = ( + data[k][{"taylor_param": 1}] / data[k][{"taylor_param": 0}] + ) + data[k][{"taylor_param": 0}] = ( + data[k][{"taylor_param": 0}] / data[k][{"taylor_param": 0}] + ) + + for (key, da), i in zip(data.items(), range(len(data))): + if markers_dim: + plot_kw[key]["marker"] = markersd[da[markers_dim].values.item()] + if colors_dim: + plot_kw[key]["color"] = colorsd[da[colors_dim].values.item()] + + fig, floating_ax, legend = taylordiagram( + data, + plot_kw, + fig_kw, + std_range, + contours, + contours_kw, + legend_kw, + std_label, + corr_label, + ) + + legend_kw.setdefault("loc", "upper right") + + if colors_dim or markers_dim: + old_handles = [] + handles_labels = floating_ax.get_legend_handles_labels() + for il, label in enumerate(handles_labels[1]): + if "rmse" in label or get_localized_term("reference") in label: + old_handles.append(handles_labels[0][il]) + chandles = [] + if colors_dim: + for k, c in colorsd.items(): + chandles.append(Line2D([0], [0], color=c, label=k, ls="-")) + mhandles = [] + if markers_dim: + for k, m in markersd.items(): + mhandles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) + legend.remove() + legend = fig.legend(handles=old_handles + mhandles + chandles) + + return fig, floating_ax, legend def hatchmap( From 86eed68f69578bf6c615f3b76dadbef83787bd7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Tue, 28 May 2024 23:09:45 -0400 Subject: [PATCH 02/15] update documentation / author stuff --- .zenodo.json | 5 +++++ AUTHORS.rst | 1 + CHANGELOG.rst | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.zenodo.json b/.zenodo.json index e86d2547..3890dd9f 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -28,6 +28,11 @@ "name": "Braun, Marco", "affiliation": "Ouranos", "orcid": "0000-0001-5061-3217" + }, + { + "name": "Dupuis, Éric", + "affiliation": "Ouranos, Montréal, Québec, Canada", + "orcid": "0000-0001-7976-4596" } ], "keywords": [ diff --git a/AUTHORS.rst b/AUTHORS.rst index 1900c4e3..8026058a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -20,3 +20,4 @@ Contributors * Gabriel Rondeau-Genesse `@RondeauG `_ * Marco Braun `@vindelico `_ +* Éric Dupuis `@coxipi `_ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5cf149ac..0c3b1fd0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog 0.4.0 (unreleased) ------------------ -Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Marco Braun (:user:`vindelico`), Pascal Bourgault (:user:`aulemahal`), Sarah-Claude Bourdeau-Goulet (:user:`Sarahclaude`) +Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Marco Braun (:user:`vindelico`), Pascal Bourgault (:user:`aulemahal`), Sarah-Claude Bourdeau-Goulet (:user:`Sarahclaude`), Éric Dupuis (:user:`coxipi`) New features and enhancements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -13,6 +13,7 @@ New features and enhancements * Added style sheet ``transparent.mplstyle`` (:issue:`183`, :pull:`185`) * Fix ``NaN`` issues, extreme values in sizes legend and added edgecolors in ``fg.matplotlib.scattermap`` (:pull:`184`). * New function ``fg.data`` for fetching package data and defined `matplotlib` style definitions. (:pull:`211`). +* New function ``fg.normalized_taylordiagram`` for plotting Taylor diagram where standard deviations are normalized such that reference's standard deviation is 1. (:pull:`214`). Breaking changes ^^^^^^^^^^^^^^^^ @@ -20,6 +21,7 @@ Breaking changes * `figanos` now uses a `'src' layout `_ for the package. (:pull:`210`). * `cartopy` has been pinned above v0.23.0 due to a licensing issue. (:pull:`210`). * `twine` and `wheel` have been removed from the `dev` requirements. (:pull:`210`). +* ``fg.taylordiagram`` returns a tuple of `(fig, floating_ax, legend)` instead of only `floating_ax`. (:pull:`214`). Internal changes ^^^^^^^^^^^^^^^^ From fb7a4e267f57baa6946cbba77e0bd68e950ec802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Tue, 28 May 2024 23:20:23 -0400 Subject: [PATCH 03/15] remove unused arg, better formatting --- src/figanos/matplotlib/plot.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 1699f286..89934e4f 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1827,7 +1827,6 @@ def taylordiagram( legend_kw: dict[str, Any] | None = None, std_label: str | None = None, corr_label: str | None = None, - floating_ax: FloatingSubplot | None = None, ): """Build a Taylor diagram. @@ -2148,23 +2147,8 @@ def normalized_taylordiagram( dims = [] if markers_dim: if isinstance(markers_dim, str): - # do not use "s", it's used for reference - default_markers = [ - "o", - "D", - "v", - "^", - "<", - ">", - "p", - "*", - "h", - "H", - "+", - "x", - "|", - "_", - ] + # do not use "s" for markers, it's used for reference + default_markers = "oDv^<>p*hH+x|_" markers = [ default_markers[i % len(default_markers)] for i in range(da[markers_dim].size) From fd184ea8a10c40bad47f2e10a06e5312cd22464b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Wed, 29 May 2024 10:30:27 -0400 Subject: [PATCH 04/15] {markers,colors}_dim not necessary for multidim DataArray --- src/figanos/matplotlib/plot.py | 76 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 89934e4f..22ad0f9e 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1857,7 +1857,7 @@ def taylordiagram( Returns ------- - matplotlib.axes.Axes + (plt.figure, mpl_toolkits.axisartist.floating_axes.FloatingSubplot, plt.legend) """ plot_kw = empty_dict(plot_kw) fig_kw = empty_dict(fig_kw) @@ -2080,9 +2080,7 @@ def normalized_taylordiagram( markers_dim: str | dict | None = None, colors_dim: str | dict | None = None, ): - """Build a Taylor diagram. - - Based on the following code: https://gist.github.com/ycopin/3342888. + """Build a Taylor diagram with normalized standard deviation. Parameters ---------- @@ -2106,10 +2104,16 @@ def normalized_taylordiagram( Label for the standard deviation (x and y) axes. corr_label : str, optional Label for the correlation axis. + markers_dim : str or dict, optional + Dimension of `data` that should be represented with markers. A dict with the dimension as key and a list of markers + as value can be passed. + colors_dim : str or dict, optional + Dimension of `data` that should be represented with colors. A dict with the dimension as key and a list of markers + as value can be passed. Returns ------- - matplotlib.axes.Axes + (plt.figure, mpl_toolkits.axisartist.floating_axes.FloatingSubplot, plt.legend) """ plot_kw = empty_dict(plot_kw) fig_kw = empty_dict(fig_kw) @@ -2134,44 +2138,42 @@ def normalized_taylordiagram( data = {"_no_label": data} # mpl excludes labels starting with "_" from legend plot_kw = {"_no_label": empty_dict(plot_kw)} + data_keys = list(data.keys()) + if len(data_keys) > 1 and len(data[data_keys[0]].dims) > 1: + raise ValueError( + "Either give a dict of one-dimensional DataArrays or a single DataArray (with a maximum of 3 dimensions)" + ) # markers/colors are attributed to given dimensions, if specified - if markers_dim is not None or colors_dim is not None: - keys = list(data.keys()) - if len(keys) > 1: - raise ValueError( - "Can only plot one dimension if many DataArrays are given as input" - ) - - da = data[keys[0]] - plot_kw = plot_kw[keys[0]] - dims = [] - if markers_dim: - if isinstance(markers_dim, str): - # do not use "s" for markers, it's used for reference - default_markers = "oDv^<>p*hH+x|_" - markers = [ - default_markers[i % len(default_markers)] - for i in range(da[markers_dim].size) - ] - else: - markers = list(markers_dim.values())[0] - markers_dim = list(markers_dim.keys())[0] - markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} - dims.append(markers_dim) - if colors_dim: - if isinstance(colors_dim, str): - colors = [f"C{i}" for i in range(da[colors_dim].size)] - else: - colors = list(colors_dim.values())[0] - colors_dim = list(colors_dim.keys())[0] - colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} - dims.append(colors_dim) + if len(data[data_keys[0]].dims) > 1: + da = data[data_keys[0]] + plot_kw = plot_kw[data_keys[0]] + if markers_dim is not None or colors_dim is not None: + if markers_dim: + if isinstance(markers_dim, str): + # do not use "s" for markers, it's used for reference + default_markers = "oDv^<>p*hH+x|_" + markers = [ + default_markers[i % len(default_markers)] + for i in range(da[markers_dim].size) + ] + else: + markers = list(markers_dim.values())[0] + markers_dim = list(markers_dim.keys())[0] + markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} + if colors_dim: + if isinstance(colors_dim, str): + colors = [f"C{i}" for i in range(da[colors_dim].size)] + else: + colors = list(colors_dim.values())[0] + colors_dim = list(colors_dim.keys())[0] + colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} + dims = list(set(da.dims) - {"taylor_param"}) da = da.stack(pl_dims=dims) for i, key in enumerate(da.pl_dims.values): if isinstance(key, list) or isinstance(key, tuple): key = "_".join([str(k) for k in key]) data[key] = da.isel(pl_dims=i) - data.pop("_no_label") + data.pop(data_keys[0]) plot_kw = {k: empty_dict(plot_kw) for k in data.keys()} # normalize data (such that ref_std == 1, unitless) From b9d30ddc5fa18cac8fe921c067f699c23b8ffdc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Wed, 29 May 2024 10:58:54 -0400 Subject: [PATCH 05/15] refactor --- src/figanos/matplotlib/plot.py | 71 ++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 22ad0f9e..e8fc176a 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -2138,43 +2138,51 @@ def normalized_taylordiagram( data = {"_no_label": data} # mpl excludes labels starting with "_" from legend plot_kw = {"_no_label": empty_dict(plot_kw)} + # only one multi-dimensional DataArray or a dict of one-dimensional DataArrays are accepted data_keys = list(data.keys()) if len(data_keys) > 1 and len(data[data_keys[0]].dims) > 1: raise ValueError( - "Either give a dict of one-dimensional DataArrays or a single DataArray (with a maximum of 3 dimensions)" + "Either give a dict of one-dimensional DataArrays or a single DataArray (with a maximum of 3 dimensions including `taylor_param`)." ) # markers/colors are attributed to given dimensions, if specified if len(data[data_keys[0]].dims) > 1: da = data[data_keys[0]] - plot_kw = plot_kw[data_keys[0]] - if markers_dim is not None or colors_dim is not None: - if markers_dim: - if isinstance(markers_dim, str): - # do not use "s" for markers, it's used for reference - default_markers = "oDv^<>p*hH+x|_" - markers = [ - default_markers[i % len(default_markers)] - for i in range(da[markers_dim].size) - ] - else: - markers = list(markers_dim.values())[0] - markers_dim = list(markers_dim.keys())[0] - markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} - if colors_dim: - if isinstance(colors_dim, str): - colors = [f"C{i}" for i in range(da[colors_dim].size)] - else: - colors = list(colors_dim.values())[0] - colors_dim = list(colors_dim.keys())[0] - colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} + + if markers_dim is not None: + if isinstance(markers_dim, str): + # do not use "s" for markers, it's used for reference + default_markers = "oDv^<>p*hH+x|_" + markers = [ + default_markers[i % len(default_markers)] + for i in range(da[markers_dim].size) + ] + else: + markers = list(markers_dim.values())[0] + markers_dim = list(markers_dim.keys())[0] + markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} + if colors_dim is not None: + if isinstance(colors_dim, str): + colors = [f"C{i}" for i in range(da[colors_dim].size)] + else: + colors = list(colors_dim.values())[0] + colors_dim = list(colors_dim.keys())[0] + colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} + dims = list(set(da.dims) - {"taylor_param"}) da = da.stack(pl_dims=dims) for i, key in enumerate(da.pl_dims.values): + da0 = da.isel(pl_dims=i) if isinstance(key, list) or isinstance(key, tuple): key = "_".join([str(k) for k in key]) - data[key] = da.isel(pl_dims=i) + data[key] = da0 + plot_kw[key] = empty_dict(plot_kw[data_keys[0]]) + if markers_dim: + plot_kw[key]["marker"] = markersd[da0[markers_dim].values.item()] + if colors_dim: + plot_kw[key]["color"] = colorsd[da0[colors_dim].values.item()] + data.pop(data_keys[0]) - plot_kw = {k: empty_dict(plot_kw) for k in data.keys()} + plot_kw.pop(data_keys[0]) # normalize data (such that ref_std == 1, unitless) for k in data.keys(): @@ -2185,12 +2193,6 @@ def normalized_taylordiagram( data[k][{"taylor_param": 0}] / data[k][{"taylor_param": 0}] ) - for (key, da), i in zip(data.items(), range(len(data))): - if markers_dim: - plot_kw[key]["marker"] = markersd[da[markers_dim].values.item()] - if colors_dim: - plot_kw[key]["color"] = colorsd[da[colors_dim].values.item()] - fig, floating_ax, legend = taylordiagram( data, plot_kw, @@ -2203,9 +2205,9 @@ def normalized_taylordiagram( corr_label, ) - legend_kw.setdefault("loc", "upper right") - - if colors_dim or markers_dim: + # plot new legend if markers/colors represent a certain dimension + if colors_dim is not None or markers_dim is not None: + # leave reference / rmse in legend if applicable old_handles = [] handles_labels = floating_ax.get_legend_handles_labels() for il, label in enumerate(handles_labels[1]): @@ -2219,8 +2221,9 @@ def normalized_taylordiagram( if markers_dim: for k, m in markersd.items(): mhandles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) + legend.remove() - legend = fig.legend(handles=old_handles + mhandles + chandles) + legend = fig.legend(handles=old_handles + mhandles + chandles, **legend_kw) return fig, floating_ax, legend From e6452813c2fa4b5d1140c07455a955cac3ac99e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Wed, 29 May 2024 11:27:28 -0400 Subject: [PATCH 06/15] remove contours, ref legend & add std=1 line --- src/figanos/matplotlib/plot.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index e8fc176a..e5608148 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -2072,8 +2072,6 @@ def normalized_taylordiagram( plot_kw: dict[str, Any] | None = None, fig_kw: dict[str, Any] | None = None, std_range: tuple = (0, 1.5), - contours: int | None = 4, - contours_kw: dict[str, Any] | None = None, legend_kw: dict[str, Any] | None = None, std_label: str | None = None, corr_label: str | None = None, @@ -2094,10 +2092,6 @@ def normalized_taylordiagram( Arguments to pass to `plt.figure()`. std_range : tuple Range of the x and y axes, in units of the highest standard deviation in the data. - contours : int, optional - Number of rsme contours to plot. - contours_kw : dict, optional - Arguments to pass to `plt.contour()` for the rmse contours. legend_kw : dict, optional Arguments to pass to `plt.legend()`. std_label : str, optional @@ -2117,7 +2111,6 @@ def normalized_taylordiagram( """ plot_kw = empty_dict(plot_kw) fig_kw = empty_dict(fig_kw) - contours_kw = empty_dict(contours_kw) legend_kw = empty_dict(legend_kw) if not std_label: @@ -2193,6 +2186,7 @@ def normalized_taylordiagram( data[k][{"taylor_param": 0}] / data[k][{"taylor_param": 0}] ) + contours, contours_kw = 0, {} fig, floating_ax, legend = taylordiagram( data, plot_kw, @@ -2205,14 +2199,16 @@ def normalized_taylordiagram( corr_label, ) + # add a line along std = 1 + transform = PolarAxes.PolarTransform() + ax = floating_ax.get_aux_axes(transform) # return the axes that can be plotted on + radius_value = 1.0 + angles_for_line = np.linspace(0, np.pi / 2, 100) + radii_for_line = np.full_like(angles_for_line, radius_value) + ax.plot(angles_for_line, radii_for_line, color="k", linewidth=0.5, linestyle="-") + # plot new legend if markers/colors represent a certain dimension if colors_dim is not None or markers_dim is not None: - # leave reference / rmse in legend if applicable - old_handles = [] - handles_labels = floating_ax.get_legend_handles_labels() - for il, label in enumerate(handles_labels[1]): - if "rmse" in label or get_localized_term("reference") in label: - old_handles.append(handles_labels[0][il]) chandles = [] if colors_dim: for k, c in colorsd.items(): @@ -2221,9 +2217,15 @@ def normalized_taylordiagram( if markers_dim: for k, m in markersd.items(): mhandles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) - - legend.remove() - legend = fig.legend(handles=old_handles + mhandles + chandles, **legend_kw) + new_handles = mhandles + chandles + else: + new_handles = [] + handles_labels = floating_ax.get_legend_handles_labels() + for il, label in enumerate(handles_labels[1]): + if get_localized_term("reference") not in label: + new_handles.append(handles_labels[0][il]) + legend.remove() + legend = fig.legend(handles=new_handles, **legend_kw) return fig, floating_ax, legend From 86839005def626d70d867a83e7ce26e92576b608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Wed, 29 May 2024 11:36:52 -0400 Subject: [PATCH 07/15] more doc --- src/figanos/matplotlib/plot.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index e5608148..4877dabd 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -2108,6 +2108,10 @@ def normalized_taylordiagram( Returns ------- (plt.figure, mpl_toolkits.axisartist.floating_axes.FloatingSubplot, plt.legend) + + Notes + ----- + Inputing `markers_dim` and/or `colors_dim` only works for DataArrays with at most three dimensions, including `taylor_param`. """ plot_kw = empty_dict(plot_kw) fig_kw = empty_dict(fig_kw) @@ -2139,6 +2143,12 @@ def normalized_taylordiagram( ) # markers/colors are attributed to given dimensions, if specified if len(data[data_keys[0]].dims) > 1: + if (markers_dim is not None or colors_dim is not None) and len( + data[data_keys[0]].dims + ) > 3: + raise ValueError( + "DataArray must have at most 3 dimensions including `taylor_param` when specifying `markers_dim` or `colors_dim`." + ) da = data[data_keys[0]] if markers_dim is not None: From 4e837c33ceff77e15e5dd3a856b2516c17ae92ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Wed, 29 May 2024 12:56:33 -0400 Subject: [PATCH 08/15] bring back contours/refence label --- src/figanos/matplotlib/plot.py | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 4877dabd..4e095d05 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1827,6 +1827,7 @@ def taylordiagram( legend_kw: dict[str, Any] | None = None, std_label: str | None = None, corr_label: str | None = None, + corr_range: tuple = (0, np.pi / 2), ): """Build a Taylor diagram. @@ -1962,7 +1963,7 @@ def taylordiagram( # Set up the axes range in the parameter "extremes" ghelper = GridHelperCurveLinear( transform, - extremes=(0, np.pi / 2, radius_min, radius_max), + extremes=(corr_range[0], corr_range[1], radius_min, radius_max), grid_locator1=gl1, tick_formatter1=tf1, ) @@ -2013,7 +2014,8 @@ def taylordiagram( # rmse contours from reference standard deviation if contours: radii, angles = np.meshgrid( - np.linspace(radius_min, radius_max), np.linspace(0, np.pi / 2) + np.linspace(radius_min, radius_max), + np.linspace(corr_range[0], corr_range[1]), ) # Compute centered RMS difference rms = np.sqrt(ref_std**2 + radii**2 - 2 * ref_std * radii * np.cos(angles)) @@ -2072,6 +2074,8 @@ def normalized_taylordiagram( plot_kw: dict[str, Any] | None = None, fig_kw: dict[str, Any] | None = None, std_range: tuple = (0, 1.5), + contours: int | None = 4, + contours_kw: dict | None = None, legend_kw: dict[str, Any] | None = None, std_label: str | None = None, corr_label: str | None = None, @@ -2092,6 +2096,10 @@ def normalized_taylordiagram( Arguments to pass to `plt.figure()`. std_range : tuple Range of the x and y axes, in units of the highest standard deviation in the data. + contours : int, optional + Number of rsme contours to plot. + contours_kw : dict, optional + Arguments to pass to `plt.contour()` for the rmse contours. legend_kw : dict, optional Arguments to pass to `plt.legend()`. std_label : str, optional @@ -2119,9 +2127,11 @@ def normalized_taylordiagram( if not std_label: try: - std_label = get_localized_term("Normalized standard deviation") + std_label = get_localized_term("standard deviation (normalized)") except AttributeError: - std_label = get_localized_term("Normalized standard deviation").capitalize() + std_label = get_localized_term( + "standard deviation (normalized)" + ).capitalize() # convert SSP, RCP, CMIP formats in keys if isinstance(data, dict): @@ -2196,7 +2206,6 @@ def normalized_taylordiagram( data[k][{"taylor_param": 0}] / data[k][{"taylor_param": 0}] ) - contours, contours_kw = 0, {} fig, floating_ax, legend = taylordiagram( data, plot_kw, @@ -2219,6 +2228,12 @@ def normalized_taylordiagram( # plot new legend if markers/colors represent a certain dimension if colors_dim is not None or markers_dim is not None: + old_handles = [] + handles_labels = floating_ax.get_legend_handles_labels() + for il, label in enumerate(handles_labels[1]): + if "rmse" in label or get_localized_term("reference") in label: + old_handles.append(handles_labels[0][il]) + chandles = [] if colors_dim: for k, c in colorsd.items(): @@ -2227,15 +2242,8 @@ def normalized_taylordiagram( if markers_dim: for k, m in markersd.items(): mhandles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) - new_handles = mhandles + chandles - else: - new_handles = [] - handles_labels = floating_ax.get_legend_handles_labels() - for il, label in enumerate(handles_labels[1]): - if get_localized_term("reference") not in label: - new_handles.append(handles_labels[0][il]) - legend.remove() - legend = fig.legend(handles=new_handles, **legend_kw) + legend.remove() + legend = fig.legend(handles=old_handles + mhandles + chandles, **legend_kw) return fig, floating_ax, legend From 084b16649d4b2fc2d67e385bc84ba174ea527a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Thu, 30 May 2024 15:01:08 -0400 Subject: [PATCH 09/15] normalized_taylordiagram in taylordiagram --- src/figanos/matplotlib/plot.py | 422 +++++++++++++++++++-------------- 1 file changed, 242 insertions(+), 180 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 4e095d05..86f9a08c 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1828,6 +1828,8 @@ def taylordiagram( std_label: str | None = None, corr_label: str | None = None, corr_range: tuple = (0, np.pi / 2), + colors_key: str | None = None, + markers_key: str | None = None, ): """Build a Taylor diagram. @@ -1855,6 +1857,12 @@ def taylordiagram( Label for the standard deviation (x and y) axes. corr_label : str, optional Label for the correlation axis. + colors_key : str, optional + Attribute or dimension of DataArrays used to separate DataArrays into groups with different colors. If present, + it overrides the "color" key in `plot_kw`. + markers_key : str, optional + Attribute or dimension of DataArrays used to separate DataArrays into groups with different markers. If present, + it overrides the "marker" key in `plot_kw`. Returns ------- @@ -1887,6 +1895,47 @@ def taylordiagram( if key == "reference": raise ValueError("'reference' is not allowed as a key in data.") + # If there are other dimensions than 'taylor_param', create a bigger dict with them + # It's possible that some dimensions are used a discriminating attributes for colors and markers + keys = list(data.keys()) + for key in keys: + da = data[key] + dims = list(set(da.dims) - {"taylor_param"}) + if dims != []: + da = da.stack(pl_dims=dims) + for i, key2 in enumerate(da.pl_dims.values): + da0 = da.isel(pl_dims=i) + if markers_key in dims: + da0.attrs[markers_key] = da0[markers_key].values.item() + if colors_key in dims: + da0.attrs[colors_key] = da0[colors_key].values.item() + if isinstance(key2, list) or isinstance(key2, tuple): + key2 = "-".join([str(k) for k in key2]) + new_key = f"{key}-{key2}" if key != "_no_label" else key2 + data[new_key] = da0 + plot_kw[new_key] = empty_dict(plot_kw[f"{key}"]) + data.pop(key) + plot_kw.pop(key) + + # set colors and markers based on discrimnating attributes + if colors_key or markers_key: + if colors_key: + colorkeys = {da.attrs[colors_key] for da in data.values()} + colorsd = {key: f"C{i}" for i, key in enumerate(colorkeys)} + if markers_key: + default_markers = "oDv^<>p*hH+x|_" + markerkeys = {da.attrs[markers_key] for da in data.values()} + markersd = { + key: default_markers[i % len(markerkeys)] + for i, key in enumerate(markerkeys) + } + + for key, da in data.items(): + if colors_key: + plot_kw[key]["color"] = colorsd[da.attrs[colors_key]] + if markers_key: + plot_kw[key]["marker"] = markersd[da.attrs[markers_key]] + # remove negative correlations initial_len = len(data) removed = [ @@ -1930,10 +1979,9 @@ def taylordiagram( # make labels if not std_label: try: - std_label = ( - get_localized_term("standard deviation") - + f" ({list(data.values())[0].units})" - ) + units = list(data.values())[0].units + std_label = get_localized_term("standard deviation") + std_label = std_label if units == "" else f"{std_label} ({units})" except AttributeError: std_label = get_localized_term("standard deviation").capitalize() @@ -2056,7 +2104,9 @@ def taylordiagram( # set defaults plot_kw[key] = {"label": key} | plot_kw[key] - # plot + # legend will be handled later in this case + if markers_key or colors_key: + plot_kw[key]["label"] = "" pt = ax.scatter( plot_corr, da.sel(taylor_param="sim_std").values, **plot_kw[key] ) @@ -2066,188 +2116,200 @@ def taylordiagram( legend_kw.setdefault("loc", "upper right") legend = fig.legend(points, [pt.get_label() for pt in points], **legend_kw) - return fig, floating_ax, legend - - -def normalized_taylordiagram( - data: xr.DataArray | dict[str, xr.DataArray], - plot_kw: dict[str, Any] | None = None, - fig_kw: dict[str, Any] | None = None, - std_range: tuple = (0, 1.5), - contours: int | None = 4, - contours_kw: dict | None = None, - legend_kw: dict[str, Any] | None = None, - std_label: str | None = None, - corr_label: str | None = None, - markers_dim: str | dict | None = None, - colors_dim: str | dict | None = None, -): - """Build a Taylor diagram with normalized standard deviation. - - Parameters - ---------- - data : xr.DataArray or dict - DataArray or dictionary of DataArrays created by xclim.sdba.measures.taylordiagram, each corresponding - to a point on the diagram. The dictionary keys will become their labels. - plot_kw : dict, optional - Arguments to pass to the `plot()` function. Changes how the markers look. - If 'data' is a dictionary, must be a nested dictionary with the same keys as 'data'. - fig_kw : dict, optional - Arguments to pass to `plt.figure()`. - std_range : tuple - Range of the x and y axes, in units of the highest standard deviation in the data. - contours : int, optional - Number of rsme contours to plot. - contours_kw : dict, optional - Arguments to pass to `plt.contour()` for the rmse contours. - legend_kw : dict, optional - Arguments to pass to `plt.legend()`. - std_label : str, optional - Label for the standard deviation (x and y) axes. - corr_label : str, optional - Label for the correlation axis. - markers_dim : str or dict, optional - Dimension of `data` that should be represented with markers. A dict with the dimension as key and a list of markers - as value can be passed. - colors_dim : str or dict, optional - Dimension of `data` that should be represented with colors. A dict with the dimension as key and a list of markers - as value can be passed. - - Returns - ------- - (plt.figure, mpl_toolkits.axisartist.floating_axes.FloatingSubplot, plt.legend) - - Notes - ----- - Inputing `markers_dim` and/or `colors_dim` only works for DataArrays with at most three dimensions, including `taylor_param`. - """ - plot_kw = empty_dict(plot_kw) - fig_kw = empty_dict(fig_kw) - legend_kw = empty_dict(legend_kw) - - if not std_label: - try: - std_label = get_localized_term("standard deviation (normalized)") - except AttributeError: - std_label = get_localized_term( - "standard deviation (normalized)" - ).capitalize() - - # convert SSP, RCP, CMIP formats in keys - if isinstance(data, dict): - data = process_keys(data, convert_scen_name) - if isinstance(plot_kw, dict): - plot_kw = process_keys(plot_kw, convert_scen_name) - if not plot_kw: - plot_kw = {} - # if only one data input, insert in dict. - if not isinstance(data, dict): - data = {"_no_label": data} # mpl excludes labels starting with "_" from legend - plot_kw = {"_no_label": empty_dict(plot_kw)} - - # only one multi-dimensional DataArray or a dict of one-dimensional DataArrays are accepted - data_keys = list(data.keys()) - if len(data_keys) > 1 and len(data[data_keys[0]].dims) > 1: - raise ValueError( - "Either give a dict of one-dimensional DataArrays or a single DataArray (with a maximum of 3 dimensions including `taylor_param`)." - ) - # markers/colors are attributed to given dimensions, if specified - if len(data[data_keys[0]].dims) > 1: - if (markers_dim is not None or colors_dim is not None) and len( - data[data_keys[0]].dims - ) > 3: - raise ValueError( - "DataArray must have at most 3 dimensions including `taylor_param` when specifying `markers_dim` or `colors_dim`." - ) - da = data[data_keys[0]] - - if markers_dim is not None: - if isinstance(markers_dim, str): - # do not use "s" for markers, it's used for reference - default_markers = "oDv^<>p*hH+x|_" - markers = [ - default_markers[i % len(default_markers)] - for i in range(da[markers_dim].size) - ] - else: - markers = list(markers_dim.values())[0] - markers_dim = list(markers_dim.keys())[0] - markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} - if colors_dim is not None: - if isinstance(colors_dim, str): - colors = [f"C{i}" for i in range(da[colors_dim].size)] - else: - colors = list(colors_dim.values())[0] - colors_dim = list(colors_dim.keys())[0] - colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} - - dims = list(set(da.dims) - {"taylor_param"}) - da = da.stack(pl_dims=dims) - for i, key in enumerate(da.pl_dims.values): - da0 = da.isel(pl_dims=i) - if isinstance(key, list) or isinstance(key, tuple): - key = "_".join([str(k) for k in key]) - data[key] = da0 - plot_kw[key] = empty_dict(plot_kw[data_keys[0]]) - if markers_dim: - plot_kw[key]["marker"] = markersd[da0[markers_dim].values.item()] - if colors_dim: - plot_kw[key]["color"] = colorsd[da0[colors_dim].values.item()] - - data.pop(data_keys[0]) - plot_kw.pop(data_keys[0]) - - # normalize data (such that ref_std == 1, unitless) - for k in data.keys(): - data[k][{"taylor_param": 1}] = ( - data[k][{"taylor_param": 1}] / data[k][{"taylor_param": 0}] - ) - data[k][{"taylor_param": 0}] = ( - data[k][{"taylor_param": 0}] / data[k][{"taylor_param": 0}] - ) - - fig, floating_ax, legend = taylordiagram( - data, - plot_kw, - fig_kw, - std_range, - contours, - contours_kw, - legend_kw, - std_label, - corr_label, - ) - - # add a line along std = 1 - transform = PolarAxes.PolarTransform() - ax = floating_ax.get_aux_axes(transform) # return the axes that can be plotted on - radius_value = 1.0 - angles_for_line = np.linspace(0, np.pi / 2, 100) - radii_for_line = np.full_like(angles_for_line, radius_value) - ax.plot(angles_for_line, radii_for_line, color="k", linewidth=0.5, linestyle="-") - # plot new legend if markers/colors represent a certain dimension - if colors_dim is not None or markers_dim is not None: - old_handles = [] - handles_labels = floating_ax.get_legend_handles_labels() - for il, label in enumerate(handles_labels[1]): - if "rmse" in label or get_localized_term("reference") in label: - old_handles.append(handles_labels[0][il]) - - chandles = [] - if colors_dim: - for k, c in colorsd.items(): - chandles.append(Line2D([0], [0], color=c, label=k, ls="-")) - mhandles = [] - if markers_dim: + if colors_key or markers_key: + handles = list(floating_ax.get_legend_handles_labels()[0]) + if markers_key: for k, m in markersd.items(): - mhandles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) + handles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) + if colors_key: + for k, c in colorsd.items(): + handles.append(Line2D([0], [0], color=c, label=k, ls="-")) legend.remove() - legend = fig.legend(handles=old_handles + mhandles + chandles, **legend_kw) + legend = fig.legend(handles=handles, **legend_kw) return fig, floating_ax, legend +# def normalized_taylordiagram( +# data: xr.DataArray | dict[str, xr.DataArray], +# plot_kw: dict[str, Any] | None = None, +# fig_kw: dict[str, Any] | None = None, +# std_range: tuple = (0, 1.5), +# contours: int | None = 4, +# contours_kw: dict | None = None, +# legend_kw: dict[str, Any] | None = None, +# std_label: str | None = None, +# corr_label: str | None = None, +# markers_dim: str | dict | None = None, +# colors_dim: str | dict | None = None, +# ): +# """Build a Taylor diagram with normalized standard deviation. + +# Parameters +# ---------- +# data : xr.DataArray or dict +# DataArray or dictionary of DataArrays created by xclim.sdba.measures.taylordiagram, each corresponding +# to a point on the diagram. The dictionary keys will become their labels. +# plot_kw : dict, optional +# Arguments to pass to the `plot()` function. Changes how the markers look. +# If 'data' is a dictionary, must be a nested dictionary with the same keys as 'data'. +# fig_kw : dict, optional +# Arguments to pass to `plt.figure()`. +# std_range : tuple +# Range of the x and y axes, in units of the highest standard deviation in the data. +# contours : int, optional +# Number of rsme contours to plot. +# contours_kw : dict, optional +# Arguments to pass to `plt.contour()` for the rmse contours. +# legend_kw : dict, optional +# Arguments to pass to `plt.legend()`. +# std_label : str, optional +# Label for the standard deviation (x and y) axes. +# corr_label : str, optional +# Label for the correlation axis. +# markers_dim : str or dict, optional +# Dimension of `data` that should be represented with markers. A dict with the dimension as key and a list of markers +# as value can be passed. +# colors_dim : str or dict, optional +# Dimension of `data` that should be represented with colors. A dict with the dimension as key and a list of markers +# as value can be passed. + +# Returns +# ------- +# (plt.figure, mpl_toolkits.axisartist.floating_axes.FloatingSubplot, plt.legend) + +# Notes +# ----- +# Inputing `markers_dim` and/or `colors_dim` only works for DataArrays with at most three dimensions, including `taylor_param`. +# """ +# plot_kw = empty_dict(plot_kw) +# fig_kw = empty_dict(fig_kw) +# legend_kw = empty_dict(legend_kw) + +# if not std_label: +# try: +# std_label = get_localized_term("standard deviation (normalized)") +# except AttributeError: +# std_label = get_localized_term( +# "standard deviation (normalized)" +# ).capitalize() + +# # convert SSP, RCP, CMIP formats in keys +# if isinstance(data, dict): +# data = process_keys(data, convert_scen_name) +# if isinstance(plot_kw, dict): +# plot_kw = process_keys(plot_kw, convert_scen_name) +# if not plot_kw: +# plot_kw = {} +# # if only one data input, insert in dict. +# if not isinstance(data, dict): +# data = {"_no_label": data} # mpl excludes labels starting with "_" from legend +# plot_kw = {"_no_label": empty_dict(plot_kw)} + +# # only one multi-dimensional DataArray or a dict of one-dimensional DataArrays are accepted +# data_keys = list(data.keys()) +# if len(data_keys) > 1 and len(data[data_keys[0]].dims) > 1: +# raise ValueError( +# "Either give a dict of one-dimensional DataArrays or a single DataArray (with a maximum of 3 dimensions including `taylor_param`)." +# ) +# # markers/colors are attributed to given dimensions, if specified +# if len(data[data_keys[0]].dims) > 1: +# if (markers_dim is not None or colors_dim is not None) and len( +# data[data_keys[0]].dims +# ) > 3: +# raise ValueError( +# "DataArray must have at most 3 dimensions including `taylor_param` when specifying `markers_dim` or `colors_dim`." +# ) +# da = data[data_keys[0]] + +# if markers_dim is not None: +# if isinstance(markers_dim, str): +# # do not use "s" for markers, it's used for reference +# default_markers = "oDv^<>p*hH+x|_" +# markers = [ +# default_markers[i % len(default_markers)] +# for i in range(da[markers_dim].size) +# ] +# else: +# markers = list(markers_dim.values())[0] +# markers_dim = list(markers_dim.keys())[0] +# markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} +# if colors_dim is not None: +# if isinstance(colors_dim, str): +# colors = [f"C{i}" for i in range(da[colors_dim].size)] +# else: +# colors = list(colors_dim.values())[0] +# colors_dim = list(colors_dim.keys())[0] +# colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} + +# dims = list(set(da.dims) - {"taylor_param"}) +# da = da.stack(pl_dims=dims) +# for i, key in enumerate(da.pl_dims.values): +# da0 = da.isel(pl_dims=i) +# if isinstance(key, list) or isinstance(key, tuple): +# key = "_".join([str(k) for k in key]) +# data[key] = da0 +# plot_kw[key] = empty_dict(plot_kw[data_keys[0]]) +# if markers_dim: +# plot_kw[key]["marker"] = markersd[da0[markers_dim].values.item()] +# if colors_dim: +# plot_kw[key]["color"] = colorsd[da0[colors_dim].values.item()] + +# data.pop(data_keys[0]) +# plot_kw.pop(data_keys[0]) + +# # normalize data (such that ref_std == 1, unitless) +# for k in data.keys(): +# data[k][{"taylor_param": 1}] = ( +# data[k][{"taylor_param": 1}] / data[k][{"taylor_param": 0}] +# ) +# data[k][{"taylor_param": 0}] = ( +# data[k][{"taylor_param": 0}] / data[k][{"taylor_param": 0}] +# ) + +# fig, floating_ax, legend = taylordiagram( +# data, +# plot_kw, +# fig_kw, +# std_range, +# contours, +# contours_kw, +# legend_kw, +# std_label, +# corr_label, +# ) + +# # add a line along std = 1 +# transform = PolarAxes.PolarTransform() +# ax = floating_ax.get_aux_axes(transform) # return the axes that can be plotted on +# radius_value = 1.0 +# angles_for_line = np.linspace(0, np.pi / 2, 100) +# radii_for_line = np.full_like(angles_for_line, radius_value) +# ax.plot(angles_for_line, radii_for_line, color="k", linewidth=0.5, linestyle="-") + +# # plot new legend if markers/colors represent a certain dimension +# if colors_dim is not None or markers_dim is not None: +# old_handles = [] +# handles_labels = floating_ax.get_legend_handles_labels() +# for il, label in enumerate(handles_labels[1]): +# if "rmse" in label or get_localized_term("reference") in label: +# old_handles.append(handles_labels[0][il]) + +# chandles = [] +# if colors_dim: +# for k, c in colorsd.items(): +# chandles.append(Line2D([0], [0], color=c, label=k, ls="-")) +# mhandles = [] +# if markers_dim: +# for k, m in markersd.items(): +# mhandles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) +# legend.remove() +# legend = fig.legend(handles=old_handles + mhandles + chandles, **legend_kw) + +# return fig, floating_ax, legend + + def hatchmap( data: dict[str, Any] | xr.DataArray | xr.Dataset, ax: matplotlib.axes.Axes | None = None, From 4f5166d5db3c5c3b52a01946a8a7d5e7b551858f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Thu, 30 May 2024 15:14:07 -0400 Subject: [PATCH 10/15] remove corr_range and old code --- src/figanos/matplotlib/plot.py | 184 +-------------------------------- 1 file changed, 2 insertions(+), 182 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 86f9a08c..842cae13 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1827,7 +1827,6 @@ def taylordiagram( legend_kw: dict[str, Any] | None = None, std_label: str | None = None, corr_label: str | None = None, - corr_range: tuple = (0, np.pi / 2), colors_key: str | None = None, markers_key: str | None = None, ): @@ -2011,7 +2010,7 @@ def taylordiagram( # Set up the axes range in the parameter "extremes" ghelper = GridHelperCurveLinear( transform, - extremes=(corr_range[0], corr_range[1], radius_min, radius_max), + extremes=(0, np.pi / 2, radius_min, radius_max), grid_locator1=gl1, tick_formatter1=tf1, ) @@ -2063,7 +2062,7 @@ def taylordiagram( if contours: radii, angles = np.meshgrid( np.linspace(radius_min, radius_max), - np.linspace(corr_range[0], corr_range[1]), + np.linspace(0, np.pi / 2), ) # Compute centered RMS difference rms = np.sqrt(ref_std**2 + radii**2 - 2 * ref_std * radii * np.cos(angles)) @@ -2131,185 +2130,6 @@ def taylordiagram( return fig, floating_ax, legend -# def normalized_taylordiagram( -# data: xr.DataArray | dict[str, xr.DataArray], -# plot_kw: dict[str, Any] | None = None, -# fig_kw: dict[str, Any] | None = None, -# std_range: tuple = (0, 1.5), -# contours: int | None = 4, -# contours_kw: dict | None = None, -# legend_kw: dict[str, Any] | None = None, -# std_label: str | None = None, -# corr_label: str | None = None, -# markers_dim: str | dict | None = None, -# colors_dim: str | dict | None = None, -# ): -# """Build a Taylor diagram with normalized standard deviation. - -# Parameters -# ---------- -# data : xr.DataArray or dict -# DataArray or dictionary of DataArrays created by xclim.sdba.measures.taylordiagram, each corresponding -# to a point on the diagram. The dictionary keys will become their labels. -# plot_kw : dict, optional -# Arguments to pass to the `plot()` function. Changes how the markers look. -# If 'data' is a dictionary, must be a nested dictionary with the same keys as 'data'. -# fig_kw : dict, optional -# Arguments to pass to `plt.figure()`. -# std_range : tuple -# Range of the x and y axes, in units of the highest standard deviation in the data. -# contours : int, optional -# Number of rsme contours to plot. -# contours_kw : dict, optional -# Arguments to pass to `plt.contour()` for the rmse contours. -# legend_kw : dict, optional -# Arguments to pass to `plt.legend()`. -# std_label : str, optional -# Label for the standard deviation (x and y) axes. -# corr_label : str, optional -# Label for the correlation axis. -# markers_dim : str or dict, optional -# Dimension of `data` that should be represented with markers. A dict with the dimension as key and a list of markers -# as value can be passed. -# colors_dim : str or dict, optional -# Dimension of `data` that should be represented with colors. A dict with the dimension as key and a list of markers -# as value can be passed. - -# Returns -# ------- -# (plt.figure, mpl_toolkits.axisartist.floating_axes.FloatingSubplot, plt.legend) - -# Notes -# ----- -# Inputing `markers_dim` and/or `colors_dim` only works for DataArrays with at most three dimensions, including `taylor_param`. -# """ -# plot_kw = empty_dict(plot_kw) -# fig_kw = empty_dict(fig_kw) -# legend_kw = empty_dict(legend_kw) - -# if not std_label: -# try: -# std_label = get_localized_term("standard deviation (normalized)") -# except AttributeError: -# std_label = get_localized_term( -# "standard deviation (normalized)" -# ).capitalize() - -# # convert SSP, RCP, CMIP formats in keys -# if isinstance(data, dict): -# data = process_keys(data, convert_scen_name) -# if isinstance(plot_kw, dict): -# plot_kw = process_keys(plot_kw, convert_scen_name) -# if not plot_kw: -# plot_kw = {} -# # if only one data input, insert in dict. -# if not isinstance(data, dict): -# data = {"_no_label": data} # mpl excludes labels starting with "_" from legend -# plot_kw = {"_no_label": empty_dict(plot_kw)} - -# # only one multi-dimensional DataArray or a dict of one-dimensional DataArrays are accepted -# data_keys = list(data.keys()) -# if len(data_keys) > 1 and len(data[data_keys[0]].dims) > 1: -# raise ValueError( -# "Either give a dict of one-dimensional DataArrays or a single DataArray (with a maximum of 3 dimensions including `taylor_param`)." -# ) -# # markers/colors are attributed to given dimensions, if specified -# if len(data[data_keys[0]].dims) > 1: -# if (markers_dim is not None or colors_dim is not None) and len( -# data[data_keys[0]].dims -# ) > 3: -# raise ValueError( -# "DataArray must have at most 3 dimensions including `taylor_param` when specifying `markers_dim` or `colors_dim`." -# ) -# da = data[data_keys[0]] - -# if markers_dim is not None: -# if isinstance(markers_dim, str): -# # do not use "s" for markers, it's used for reference -# default_markers = "oDv^<>p*hH+x|_" -# markers = [ -# default_markers[i % len(default_markers)] -# for i in range(da[markers_dim].size) -# ] -# else: -# markers = list(markers_dim.values())[0] -# markers_dim = list(markers_dim.keys())[0] -# markersd = {k: m for k, m in zip(da[markers_dim].values, markers)} -# if colors_dim is not None: -# if isinstance(colors_dim, str): -# colors = [f"C{i}" for i in range(da[colors_dim].size)] -# else: -# colors = list(colors_dim.values())[0] -# colors_dim = list(colors_dim.keys())[0] -# colorsd = {k: c for k, c in zip(da[colors_dim].values, colors)} - -# dims = list(set(da.dims) - {"taylor_param"}) -# da = da.stack(pl_dims=dims) -# for i, key in enumerate(da.pl_dims.values): -# da0 = da.isel(pl_dims=i) -# if isinstance(key, list) or isinstance(key, tuple): -# key = "_".join([str(k) for k in key]) -# data[key] = da0 -# plot_kw[key] = empty_dict(plot_kw[data_keys[0]]) -# if markers_dim: -# plot_kw[key]["marker"] = markersd[da0[markers_dim].values.item()] -# if colors_dim: -# plot_kw[key]["color"] = colorsd[da0[colors_dim].values.item()] - -# data.pop(data_keys[0]) -# plot_kw.pop(data_keys[0]) - -# # normalize data (such that ref_std == 1, unitless) -# for k in data.keys(): -# data[k][{"taylor_param": 1}] = ( -# data[k][{"taylor_param": 1}] / data[k][{"taylor_param": 0}] -# ) -# data[k][{"taylor_param": 0}] = ( -# data[k][{"taylor_param": 0}] / data[k][{"taylor_param": 0}] -# ) - -# fig, floating_ax, legend = taylordiagram( -# data, -# plot_kw, -# fig_kw, -# std_range, -# contours, -# contours_kw, -# legend_kw, -# std_label, -# corr_label, -# ) - -# # add a line along std = 1 -# transform = PolarAxes.PolarTransform() -# ax = floating_ax.get_aux_axes(transform) # return the axes that can be plotted on -# radius_value = 1.0 -# angles_for_line = np.linspace(0, np.pi / 2, 100) -# radii_for_line = np.full_like(angles_for_line, radius_value) -# ax.plot(angles_for_line, radii_for_line, color="k", linewidth=0.5, linestyle="-") - -# # plot new legend if markers/colors represent a certain dimension -# if colors_dim is not None or markers_dim is not None: -# old_handles = [] -# handles_labels = floating_ax.get_legend_handles_labels() -# for il, label in enumerate(handles_labels[1]): -# if "rmse" in label or get_localized_term("reference") in label: -# old_handles.append(handles_labels[0][il]) - -# chandles = [] -# if colors_dim: -# for k, c in colorsd.items(): -# chandles.append(Line2D([0], [0], color=c, label=k, ls="-")) -# mhandles = [] -# if markers_dim: -# for k, m in markersd.items(): -# mhandles.append(Line2D([0], [0], color="k", label=k, marker=m, ls="")) -# legend.remove() -# legend = fig.legend(handles=old_handles + mhandles + chandles, **legend_kw) - -# return fig, floating_ax, legend - - def hatchmap( data: dict[str, Any] | xr.DataArray | xr.Dataset, ax: matplotlib.axes.Axes | None = None, From 6d78b2085ededf0044e9295303892620feba6a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= <71575674+coxipi@users.noreply.github.com> Date: Thu, 30 May 2024 15:25:05 -0400 Subject: [PATCH 11/15] Apply suggestions from code review remove normalized_taylordiagram mentions --- CHANGELOG.rst | 3 ++- src/figanos/matplotlib/__init__.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c3b1fd0..37f34451 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,7 +13,8 @@ New features and enhancements * Added style sheet ``transparent.mplstyle`` (:issue:`183`, :pull:`185`) * Fix ``NaN`` issues, extreme values in sizes legend and added edgecolors in ``fg.matplotlib.scattermap`` (:pull:`184`). * New function ``fg.data`` for fetching package data and defined `matplotlib` style definitions. (:pull:`211`). -* New function ``fg.normalized_taylordiagram`` for plotting Taylor diagram where standard deviations are normalized such that reference's standard deviation is 1. (:pull:`214`). +* ``fg.taylordiagram`` can now accept datasets with many dimensions (not only `taylor_params`), provided that they all share the same `ref_std` (e.g. normalized taylor diagrams) (:pull:`214`). +* A new way to organize points in a `fg.taylordiagram` with `colors_key`, `markers_key` : DataArrays with a common dimension value or a common attrtibute are grouped with the same color/marker (:pull:`214`). Breaking changes ^^^^^^^^^^^^^^^^ diff --git a/src/figanos/matplotlib/__init__.py b/src/figanos/matplotlib/__init__.py index 7dfcb3b9..974fcdad 100644 --- a/src/figanos/matplotlib/__init__.py +++ b/src/figanos/matplotlib/__init__.py @@ -5,7 +5,6 @@ gridmap, hatchmap, heatmap, - normalized_taylordiagram, partition, scattermap, stripes, From b456394e40e16f87a6afdd7cdcd557819b890e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= <71575674+coxipi@users.noreply.github.com> Date: Thu, 30 May 2024 15:26:32 -0400 Subject: [PATCH 12/15] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 37f34451..3272c9ac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ New features and enhancements * Fix ``NaN`` issues, extreme values in sizes legend and added edgecolors in ``fg.matplotlib.scattermap`` (:pull:`184`). * New function ``fg.data`` for fetching package data and defined `matplotlib` style definitions. (:pull:`211`). * ``fg.taylordiagram`` can now accept datasets with many dimensions (not only `taylor_params`), provided that they all share the same `ref_std` (e.g. normalized taylor diagrams) (:pull:`214`). -* A new way to organize points in a `fg.taylordiagram` with `colors_key`, `markers_key` : DataArrays with a common dimension value or a common attrtibute are grouped with the same color/marker (:pull:`214`). +* A new optional way to organize points in a `fg.taylordiagram` with `colors_key`, `markers_key` : DataArrays with a common dimension value or a common attrtibute are grouped with the same color/marker (:pull:`214`). Breaking changes ^^^^^^^^^^^^^^^^ From 5e6aebca6ef21253a1535c2dc9fcb9fc2161a9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Thu, 30 May 2024 15:56:58 -0400 Subject: [PATCH 13/15] change some types for better behaviour when data=dict --- src/figanos/matplotlib/plot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 842cae13..1e82be14 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1883,8 +1883,7 @@ def taylordiagram( data = {"_no_label": data} # mpl excludes labels starting with "_" from legend plot_kw = {"_no_label": empty_dict(plot_kw)} elif not plot_kw: - plot_kw = {} - + plot_kw = {k: {} for k in data.keys()} # check type for key, v in data.items(): if not isinstance(v, xr.DataArray): @@ -1915,7 +1914,6 @@ def taylordiagram( plot_kw[new_key] = empty_dict(plot_kw[f"{key}"]) data.pop(key) plot_kw.pop(key) - # set colors and markers based on discrimnating attributes if colors_key or markers_key: if colors_key: From 53ff64ddeb62d8e402fb762718f25daba5676e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Fri, 31 May 2024 11:55:00 -0400 Subject: [PATCH 14/15] reorganize code; use get_scen_color in new organizing mode --- src/figanos/matplotlib/plot.py | 77 ++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 1e82be14..8a394b5d 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1894,44 +1894,28 @@ def taylordiagram( raise ValueError("'reference' is not allowed as a key in data.") # If there are other dimensions than 'taylor_param', create a bigger dict with them - # It's possible that some dimensions are used a discriminating attributes for colors and markers - keys = list(data.keys()) - for key in keys: - da = data[key] + data_keys = list(data.keys()) + for data_key in data_keys: + da = data[data_key] dims = list(set(da.dims) - {"taylor_param"}) if dims != []: da = da.stack(pl_dims=dims) - for i, key2 in enumerate(da.pl_dims.values): + for i, dim_key in enumerate(da.pl_dims.values): + if isinstance(dim_key, list) or isinstance(dim_key, tuple): + dim_key = "-".join([str(k) for k in dim_key]) da0 = da.isel(pl_dims=i) + # if colors_key/markers_key is a dimension, add it as an attribute for later use if markers_key in dims: da0.attrs[markers_key] = da0[markers_key].values.item() if colors_key in dims: da0.attrs[colors_key] = da0[colors_key].values.item() - if isinstance(key2, list) or isinstance(key2, tuple): - key2 = "-".join([str(k) for k in key2]) - new_key = f"{key}-{key2}" if key != "_no_label" else key2 - data[new_key] = da0 - plot_kw[new_key] = empty_dict(plot_kw[f"{key}"]) - data.pop(key) - plot_kw.pop(key) - # set colors and markers based on discrimnating attributes - if colors_key or markers_key: - if colors_key: - colorkeys = {da.attrs[colors_key] for da in data.values()} - colorsd = {key: f"C{i}" for i, key in enumerate(colorkeys)} - if markers_key: - default_markers = "oDv^<>p*hH+x|_" - markerkeys = {da.attrs[markers_key] for da in data.values()} - markersd = { - key: default_markers[i % len(markerkeys)] - for i, key in enumerate(markerkeys) - } - - for key, da in data.items(): - if colors_key: - plot_kw[key]["color"] = colorsd[da.attrs[colors_key]] - if markers_key: - plot_kw[key]["marker"] = markersd[da.attrs[markers_key]] + new_data_key = ( + f"{data_key}-{dim_key}" if data_key != "_no_label" else dim_key + ) + data[new_data_key] = da0 + plot_kw[new_data_key] = empty_dict(plot_kw[f"{data_key}"]) + data.pop(data_key) + plot_kw.pop(data_key) # remove negative correlations initial_len = len(data) @@ -2086,15 +2070,38 @@ def taylordiagram( if len(data) > len(style_colors): style_colors = style_colors * math.ceil(len(data) / len(style_colors)) cat_colors = Path(__file__).parents[1] / "data/ipcc_colors/categorical_colors.json" + # get marker options (only used if `markers_key` is set) + style_markers = "oDv^<>p*hH+x|_" + if len(data) > len(style_markers): + style_markers = style_markers * math.ceil(len(data) / len(style_markers)) + + # set colors and markers styles based on discrimnating attributes (if specified) + if colors_key or markers_key: + if colors_key: + # get_scen_color : look for SSP, RCP, CMIP model color + colorsd = { + k: get_scen_color(k, cat_colors) or style_colors[i] + for i, k in enumerate({da.attrs[colors_key] for da in data.values()}) + } + if markers_key: + markersd = { + k: style_markers[i] + for i, k in enumerate({da.attrs[markers_key] for da in data.values()}) + } + + for key, da in data.items(): + if colors_key: + plot_kw[key]["color"] = colorsd[da.attrs[colors_key]] + if markers_key: + plot_kw[key]["marker"] = markersd[da.attrs[markers_key]] # plot scatter for (key, da), i in zip(data.items(), range(len(data))): # look for SSP, RCP, CMIP model color - if get_scen_color(key, cat_colors): - plot_kw[key].setdefault("color", get_scen_color(key, cat_colors)) - else: - plot_kw[key].setdefault("color", style_colors[i]) - + if colors_key is None: + plot_kw[key].setdefault( + "color", get_scen_color(key, cat_colors) or style_colors[i] + ) # convert corr to polar coordinates plot_corr = (1 - da.sel(taylor_param="corr").values) * 90 * np.pi / 180 From f7271748d8d9787959a25d620d58e8db74f1bda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Tue, 4 Jun 2024 10:39:14 -0400 Subject: [PATCH 15/15] corr ~ cos(theta) instead of corr ~ (1-theta) --- src/figanos/matplotlib/plot.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 8a394b5d..f8a201a4 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1979,12 +1979,11 @@ def taylordiagram( transform = PolarAxes.PolarTransform() # Setup the axis, here we map angles in degrees to angles in radius - rlocs = np.array([0, 0.2, 0.4, 0.6, 0.8, 1]) - rlocs_deg = rlocs * 90 - tlocs = rlocs_deg * np.pi / 180 # convert degrees to radians + # Correlation labels + rlocs = np.array([0, 0.2, 0.4, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1]) + tlocs = np.arccos(rlocs) # Conversion to polar angles gl1 = gf.FixedLocator(tlocs) # Positions - tf1 = gf.DictFormatter(dict(zip(tlocs, np.flip(rlocs).astype(str)))) - + tf1 = gf.DictFormatter(dict(zip(tlocs, map(str, rlocs)))) # Standard deviation axis extent radius_min = std_range[0] * max(max_std) radius_max = std_range[1] * max(max_std) @@ -2102,17 +2101,18 @@ def taylordiagram( plot_kw[key].setdefault( "color", get_scen_color(key, cat_colors) or style_colors[i] ) - # convert corr to polar coordinates - plot_corr = (1 - da.sel(taylor_param="corr").values) * 90 * np.pi / 180 - # set defaults plot_kw[key] = {"label": key} | plot_kw[key] # legend will be handled later in this case if markers_key or colors_key: plot_kw[key]["label"] = "" + + # plot pt = ax.scatter( - plot_corr, da.sel(taylor_param="sim_std").values, **plot_kw[key] + np.arccos(da.sel(taylor_param="corr").values), + da.sel(taylor_param="sim_std").values, + **plot_kw[key], ) points.append(pt)