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/20] 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/20] 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/20] 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/20] {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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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) From a8c7215d591a2999e00151f272dc12ac22c2d963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Wed, 5 Jun 2024 11:00:36 -0400 Subject: [PATCH 16/20] taylordiagram in docs; preserve marker/color order --- docs/notebooks/figanos_multiplots.ipynb | 75 ++++++++++++++++++++++++- src/figanos/matplotlib/plot.py | 27 +++++++-- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/docs/notebooks/figanos_multiplots.ipynb b/docs/notebooks/figanos_multiplots.ipynb index 09a94acf..fc16da3b 100644 --- a/docs/notebooks/figanos_multiplots.ipynb +++ b/docs/notebooks/figanos_multiplots.ipynb @@ -199,6 +199,79 @@ "im.fig.suptitle(\"Multiple hatchmaps\", y=1.08)\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Taylor diagram\n", + "\n", + "A Taylor diagram displays the correlation along a given dimension between a target and a source DataArray. The standard deviation of each DataArrays are also displayed. In the example below, maximum temperatures `tasmax` are compared along time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from xclim.testing import open_dataset\n", + "from xclim import sdba\n", + "from xclim.core.units import convert_units_to\n", + "\n", + "ds = open_dataset(\"sdba/CanESM2_1950-2100.nc\").sel(time=slice(\"1950\", \"2013\"))\n", + "ds2 = open_dataset(\"sdba/ahccd_1950-2013.nc\")\n", + "for v in ds.data_vars: \n", + " ds2[v] = convert_units_to(ds2[v], ds[v], context=\"hydro\")\n", + "tx_ref = ds.tasmax.isel(location=0)\n", + "tx_sim = ds2.tasmax.isel(location=0)\n", + "out = {}\n", + "out[\"sim\"] = sdba.measures.taylordiagram(ref=tx_ref, sim=tx_sim, dim=\"time\")\n", + "out[\"sim2\"] = sdba.measures.taylordiagram(ref=tx_ref, sim=tx_sim2, dim=\"time\")\n", + "# In the default behaviour of `fg.taylordiagram`, the keys of the dictionary of \n", + "# DataArray are used for the legend\n", + "# A single DataArray can also be passed.\n", + "fg.taylordiagram(out)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Normalized taylor diagram\n", + "\n", + "If we normalize the standard deviation of our measures, many Taylor diagrams with difference references can be combined in a single plot. In the following example, we keep all three locations in the Datasets from the previous example, and we also include the precipitation instead of only selecting the maximum temperature. For each location (3) and variable (2), a Taylordiagram measure is computed. Each set of correlation and standard deviation is then plotted. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from xclim.testing import open_dataset\n", + "from xclim import sdba\n", + "from xclim.core.units import convert_units_to\n", + "import figanos.matplotlib as fg\n", + "\n", + "# Here, we have three locations, two variables. We stack variables to convert from\n", + "# a Dataset to a DataArray.\n", + "da_ref = sdba.stack_variables(ds)\n", + "da_sim = sdba.stack_variables(ds2)\n", + "\n", + "# Each location/variable will have its own set of taylor parameters\n", + "out = sdba.measures.taylordiagram(ref=da_ref, sim=da_sim, dim=\"time\")\n", + "\n", + "# If we normalize the taylor diagrams, they can be compared on the same plot\n", + "out[{\"taylor_param\":[0,1]}] = out[{\"taylor_param\":[0,1]}]/out[{\"taylor_param\":0}]\n", + "\n", + "# The `markers_key` and `colors_key` are used to separate between two different features. \n", + "# Here, the type of marker is used to distinguish between locations, and the color \n", + "# distinguishes between variables. If those parameters are not specified, then each \n", + "# pair (location, multivar) has simply its own color.\n", + "# NB: Negatives correlations are not plotted\n", + "fg.taylordiagram(out, markers_key=\"location\", colors_key=\"multivar\")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -367,7 +440,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.0" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index f8a201a4..56121fc5 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1872,6 +1872,10 @@ def taylordiagram( contours_kw = empty_dict(contours_kw) legend_kw = empty_dict(legend_kw) + # preserve order of dimensions if used for marker/color + ordered_markers_type = None + ordered_colors_type = None + # convert SSP, RCP, CMIP formats in keys if isinstance(data, dict): data = process_keys(data, convert_scen_name) @@ -1899,6 +1903,11 @@ def taylordiagram( da = data[data_key] dims = list(set(da.dims) - {"taylor_param"}) if dims != []: + if markers_key in dims: + ordered_markers_type = da[markers_key].values + if colors_key in dims: + ordered_colors_type = da[colors_key].values + da = da.stack(pl_dims=dims) for i, dim_key in enumerate(da.pl_dims.values): if isinstance(dim_key, list) or isinstance(dim_key, tuple): @@ -1997,7 +2006,6 @@ def taylordiagram( ) fig = plt.figure(**fig_kw) - floating_ax = FloatingSubplot(fig, 111, grid_helper=ghelper) fig.add_subplot(floating_ax) @@ -2078,15 +2086,22 @@ def taylordiagram( if colors_key or markers_key: if colors_key: # get_scen_color : look for SSP, RCP, CMIP model color + colors_type = ( + ordered_colors_type + if ordered_colors_type is not None + else {da.attrs[colors_key] for da in data.values()} + ) 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()}) + for i, k in enumerate(colors_type) } if markers_key: - markersd = { - k: style_markers[i] - for i, k in enumerate({da.attrs[markers_key] for da in data.values()}) - } + markers_type = ( + ordered_markers_type + if ordered_markers_type is not None + else {da.attrs[markers_key] for da in data.values()} + ) + markersd = {k: style_markers[i] for i, k in enumerate(markers_type)} for key, da in data.items(): if colors_key: From 6976fec45e4796e692220f5e7cab17fbea643478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Thu, 6 Jun 2024 13:06:29 -0400 Subject: [PATCH 17/20] fix missing dataset --- docs/notebooks/figanos_multiplots.ipynb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/notebooks/figanos_multiplots.ipynb b/docs/notebooks/figanos_multiplots.ipynb index fc16da3b..b0ded4ee 100644 --- a/docs/notebooks/figanos_multiplots.ipynb +++ b/docs/notebooks/figanos_multiplots.ipynb @@ -219,18 +219,22 @@ "from xclim.core.units import convert_units_to\n", "\n", "ds = open_dataset(\"sdba/CanESM2_1950-2100.nc\").sel(time=slice(\"1950\", \"2013\"))\n", - "ds2 = open_dataset(\"sdba/ahccd_1950-2013.nc\")\n", + "ds1 = open_dataset(\"sdba/ahccd_1950-2013.nc\")\n", + "ds2 = open_dataset(\"sdba/nrcan_1950-2013.nc\")\n", "for v in ds.data_vars: \n", + " ds1[v] = convert_units_to(ds1[v], ds[v], context=\"hydro\")\n", " ds2[v] = convert_units_to(ds2[v], ds[v], context=\"hydro\")\n", "tx_ref = ds.tasmax.isel(location=0)\n", - "tx_sim = ds2.tasmax.isel(location=0)\n", + "tx_sim = ds1.tasmax.isel(location=0)\n", + "tx_sim2 = ds2.tasmax.isel(location=0)\n", "out = {}\n", "out[\"sim\"] = sdba.measures.taylordiagram(ref=tx_ref, sim=tx_sim, dim=\"time\")\n", "out[\"sim2\"] = sdba.measures.taylordiagram(ref=tx_ref, sim=tx_sim2, dim=\"time\")\n", "# In the default behaviour of `fg.taylordiagram`, the keys of the dictionary of \n", "# DataArray are used for the legend\n", "# A single DataArray can also be passed.\n", - "fg.taylordiagram(out)\n" + "fg.taylordiagram(out)\n", + "\n" ] }, { From 808e192884aedc0def547ece2e5841076cee69b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Tue, 11 Jun 2024 17:14:18 -0400 Subject: [PATCH 18/20] move example, modify single-taylor one --- docs/notebooks/figanos_docs.ipynb | 70 +++++++++++++++++----- docs/notebooks/figanos_multiplots.ipynb | 77 ------------------------- 2 files changed, 54 insertions(+), 93 deletions(-) diff --git a/docs/notebooks/figanos_docs.ipynb b/docs/notebooks/figanos_docs.ipynb index ef8a5439..0c5acbce 100644 --- a/docs/notebooks/figanos_docs.ipynb +++ b/docs/notebooks/figanos_docs.ipynb @@ -1009,23 +1009,61 @@ "source": [ "da_ref = ds_time['tx_max_p50']\n", "\n", + "# Toy data with same mean as `da_ref` & modify deviations with trigonometric functions\n", + "homogenous_ref_mean = xr.full_like(da_ref, da_ref.mean(dim=\"time\"))\n", + "simd = {}\n", + "for i, f_trig in enumerate([np.cos, lambda x: np.cos(x)**2, np.tan]):\n", + " da = homogenous_ref_mean + f_trig(da_ref.values)\n", + " da.attrs[\"units\"] = da_ref.attrs[\"units\"]\n", + " simd[f\"model{i}\"] = sdba.measures.taylordiagram(sim=da, ref=da_ref)\n", "\n", - "rands = np.arange(15)\n", - "sims = {}\n", - "for rand in rands:\n", - " name = \"model\" + str(rand)\n", - " da = xr.DataArray(data=np.random.rand(13)*20+290+rand,\n", - " dims={'time': da_ref.time.values})\n", - " da.attrs[\"units\"] = da_ref.units\n", + "fg.taylordiagram(simd, std_range=(0, 1.3), contours=5, contours_kw={'colors': 'green'}, plot_kw={'reference': {'marker':'*'}})\n" + ] + }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "### Normalized taylor diagram\n", "\n", - " sims[name] = sdba.measures.taylordiagram(da, da_ref)\n", + "If we normalize the standard deviation of our measures, many Taylor diagrams with difference references can be combined in a single plot. In the following example, we have datasets with two variables (`tasmax, pr`) and three location coordinates. For each location (3) and variable (2), a Taylordiagram measure is computed. Each set of correlation and standard deviation is then plotted. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "ds_ref = xc.testing.open_dataset(\"sdba/ahccd_1950-2013.nc\")\n", + "ds_sim = xc.testing.open_dataset(\"sdba/nrcan_1950-2013.nc\")\n", + "for v in ds.data_vars: \n", + " ds_sim[v] = xc.core.units.convert_units_to(ds_sim[v], ds_ref[v], context=\"hydro\")\n", + "\n", + "# Here, we have three locations, two variables. We stack variables to convert from\n", + "# a Dataset to a DataArray.\n", + "da_ref = sdba.stack_variables(ds_ref)\n", + "da_sim = sdba.stack_variables(ds_sim)\n", + "\n", + "# Each location/variable will have its own set of taylor parameters\n", + "out = sdba.measures.taylordiagram(ref=da_ref, sim=da_sim, dim=\"time\")\n", + "\n", + "# If we normalize the taylor diagrams, they can be compared on the same plot\n", + "out[{\"taylor_param\":[0,1]}] = out[{\"taylor_param\":[0,1]}]/out[{\"taylor_param\":0}]\n", "\n", - "fg.taylordiagram(sims, std_range=(0, 1.3), contours=5, contours_kw={'colors': 'green'}, plot_kw={'reference': {'marker':'*'}})\n" + "# The `markers_key` and `colors_key` are used to separate between two different features. \n", + "# Here, the type of marker is used to distinguish between locations, and the color \n", + "# distinguishes between variables. If those parameters are not specified, then each \n", + "# pair (location, multivar) has simply its own color.\n", + "# NB: Negatives correlations are not plotted\n", + "fg.taylordiagram(out, markers_key=\"location\", colors_key=\"multivar\")\n" ] }, { "cell_type": "markdown", - "id": "63", + "id": "65", "metadata": {}, "source": [ "## Partition plots\n", @@ -1045,7 +1083,7 @@ { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "66", "metadata": {}, "outputs": [], "source": [ @@ -1095,7 +1133,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "67", "metadata": {}, "source": [ "Compute uncertainties with xclim and use `fractional_uncertainty` to have the right format to plot." @@ -1104,7 +1142,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "68", "metadata": {}, "outputs": [], "source": [ @@ -1140,7 +1178,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "69", "metadata": {}, "outputs": [], "source": [ @@ -1160,7 +1198,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "70", "metadata": {}, "outputs": [], "source": [] @@ -1178,7 +1216,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.0" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/docs/notebooks/figanos_multiplots.ipynb b/docs/notebooks/figanos_multiplots.ipynb index b0ded4ee..ad7c9102 100644 --- a/docs/notebooks/figanos_multiplots.ipynb +++ b/docs/notebooks/figanos_multiplots.ipynb @@ -199,83 +199,6 @@ "im.fig.suptitle(\"Multiple hatchmaps\", y=1.08)\n" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Taylor diagram\n", - "\n", - "A Taylor diagram displays the correlation along a given dimension between a target and a source DataArray. The standard deviation of each DataArrays are also displayed. In the example below, maximum temperatures `tasmax` are compared along time." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from xclim.testing import open_dataset\n", - "from xclim import sdba\n", - "from xclim.core.units import convert_units_to\n", - "\n", - "ds = open_dataset(\"sdba/CanESM2_1950-2100.nc\").sel(time=slice(\"1950\", \"2013\"))\n", - "ds1 = open_dataset(\"sdba/ahccd_1950-2013.nc\")\n", - "ds2 = open_dataset(\"sdba/nrcan_1950-2013.nc\")\n", - "for v in ds.data_vars: \n", - " ds1[v] = convert_units_to(ds1[v], ds[v], context=\"hydro\")\n", - " ds2[v] = convert_units_to(ds2[v], ds[v], context=\"hydro\")\n", - "tx_ref = ds.tasmax.isel(location=0)\n", - "tx_sim = ds1.tasmax.isel(location=0)\n", - "tx_sim2 = ds2.tasmax.isel(location=0)\n", - "out = {}\n", - "out[\"sim\"] = sdba.measures.taylordiagram(ref=tx_ref, sim=tx_sim, dim=\"time\")\n", - "out[\"sim2\"] = sdba.measures.taylordiagram(ref=tx_ref, sim=tx_sim2, dim=\"time\")\n", - "# In the default behaviour of `fg.taylordiagram`, the keys of the dictionary of \n", - "# DataArray are used for the legend\n", - "# A single DataArray can also be passed.\n", - "fg.taylordiagram(out)\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Normalized taylor diagram\n", - "\n", - "If we normalize the standard deviation of our measures, many Taylor diagrams with difference references can be combined in a single plot. In the following example, we keep all three locations in the Datasets from the previous example, and we also include the precipitation instead of only selecting the maximum temperature. For each location (3) and variable (2), a Taylordiagram measure is computed. Each set of correlation and standard deviation is then plotted. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from xclim.testing import open_dataset\n", - "from xclim import sdba\n", - "from xclim.core.units import convert_units_to\n", - "import figanos.matplotlib as fg\n", - "\n", - "# Here, we have three locations, two variables. We stack variables to convert from\n", - "# a Dataset to a DataArray.\n", - "da_ref = sdba.stack_variables(ds)\n", - "da_sim = sdba.stack_variables(ds2)\n", - "\n", - "# Each location/variable will have its own set of taylor parameters\n", - "out = sdba.measures.taylordiagram(ref=da_ref, sim=da_sim, dim=\"time\")\n", - "\n", - "# If we normalize the taylor diagrams, they can be compared on the same plot\n", - "out[{\"taylor_param\":[0,1]}] = out[{\"taylor_param\":[0,1]}]/out[{\"taylor_param\":0}]\n", - "\n", - "# The `markers_key` and `colors_key` are used to separate between two different features. \n", - "# Here, the type of marker is used to distinguish between locations, and the color \n", - "# distinguishes between variables. If those parameters are not specified, then each \n", - "# pair (location, multivar) has simply its own color.\n", - "# NB: Negatives correlations are not plotted\n", - "fg.taylordiagram(out, markers_key=\"location\", colors_key=\"multivar\")" - ] - }, { "cell_type": "markdown", "metadata": {}, From ee15980a324f1cde750db151207728b1af9a2a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Wed, 12 Jun 2024 15:08:09 -0400 Subject: [PATCH 19/20] typos in examples --- docs/notebooks/figanos_docs.ipynb | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/notebooks/figanos_docs.ipynb b/docs/notebooks/figanos_docs.ipynb index 0c5acbce..a1d51e39 100644 --- a/docs/notebooks/figanos_docs.ipynb +++ b/docs/notebooks/figanos_docs.ipynb @@ -1037,9 +1037,11 @@ "metadata": {}, "outputs": [], "source": [ - "ds_ref = xc.testing.open_dataset(\"sdba/ahccd_1950-2013.nc\")\n", - "ds_sim = xc.testing.open_dataset(\"sdba/nrcan_1950-2013.nc\")\n", - "for v in ds.data_vars: \n", + "from xclim.testing import open_dataset\n", + "\n", + "ds_ref = open_dataset(\"sdba/ahccd_1950-2013.nc\")\n", + "ds_sim = open_dataset(\"sdba/nrcan_1950-2013.nc\")\n", + "for v in ds_ref.data_vars: \n", " ds_sim[v] = xc.core.units.convert_units_to(ds_sim[v], ds_ref[v], context=\"hydro\")\n", "\n", "# Here, we have three locations, two variables. We stack variables to convert from\n", @@ -1194,14 +1196,6 @@ " line_kw={'lw':2}\n", ")\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "70", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 537cc032cdcc1933fe3a8fe49fe254fabfe9fc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Dupuis?= Date: Fri, 14 Jun 2024 09:32:45 -0400 Subject: [PATCH 20/20] add optional ref_std_line --- src/figanos/matplotlib/plot.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 56121fc5..d7b7804e 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -1824,6 +1824,7 @@ def taylordiagram( std_range: tuple = (0, 1.5), contours: int | None = 4, contours_kw: dict[str, Any] | None = None, + ref_std_line: bool = False, legend_kw: dict[str, Any] | None = None, std_label: str | None = None, corr_label: str | None = None, @@ -1850,6 +1851,8 @@ def taylordiagram( Number of rsme contours to plot. contours_kw : dict, optional Arguments to pass to `plt.contour()` for the rmse contours. + ref_std_line : bool, optional + If True, draws a circular line on radius `std = ref_std`. Default: False legend_kw : dict, optional Arguments to pass to `plt.legend()`. std_label : str, optional @@ -2047,6 +2050,18 @@ def taylordiagram( points = [ref_pt] # set up for later + # plot a circular line along `ref_std` + if ref_std_line: + angles_for_line = np.linspace(0, np.pi / 2, 100) + radii_for_line = np.full_like(angles_for_line, ref_std) + ax.plot( + angles_for_line, + radii_for_line, + color=ref_kw["color"], + linewidth=0.5, + linestyle="--", + ) + # rmse contours from reference standard deviation if contours: radii, angles = np.meshgrid(