Source code for scmdata.plotting

"""
Plotting helpers for :class:`ScmRun <scmdata.run.ScmRun>`

See the example notebook 'plotting-with-seaborn.ipynb' for usage examples
"""
import warnings
from itertools import cycle

import numpy as np

try:
    import matplotlib.lines as mlines
    import matplotlib.patches as mpatches
    import matplotlib.pyplot as plt

    has_matplotlib = True
except ImportError:  # pragma: no cover
    plt = None
    has_matplotlib = False

try:
    import seaborn as sns

    has_seaborn = True
except ImportError:  # pragma: no cover
    sns = None
    has_seaborn = False


RCMIP_SCENARIO_COLOURS = {
    "historical": "black",
    "ssp119": "#1e9583",
    "ssp126": "#1d3354",
    "ssp245": "#e9dc3d",
    "ssp370": "#f11111",
    "ssp370-lowNTCF-aerchemmip": "tab:pink",
    "ssp370-lowNTCF-gidden": "tab:red",
    "ssp434": "#63bce4",
    "ssp460": "#e78731",
    "ssp534-over": "#996dc8",
    "ssp585": "#830b22",
}


[docs]def lineplot(self, time_axis=None, **kwargs): # pragma: no cover """ Make a line plot via `seaborn's lineplot <https://seaborn.pydata.org/generated/seaborn.lineplot.html>`_ If only a single unit is present, it will be used as the y-axis label. The axis object is returned so this can be changed by the user if desired. Parameters ---------- time_axis : {None, "year", "year-month", "days since 1970-01-01", "seconds since 1970-01-01"} # noqa: E501 Time axis to use for the plot. If ``None``, :class:`datetime.datetime` objects will be used. If ``"year"``, the year of each time point will be used. If ``"year-month"``, the year plus (month - 0.5) / 12 will be used. If ``"days since 1970-01-01"``, the number of days since 1st Jan 1970 will be used (calculated using the :mod:`datetime` module). If ``"seconds since 1970-01-01"``, the number of seconds since 1st Jan 1970 will be used (calculated using the :mod:`datetime` module). **kwargs Keyword arguments to be passed to ``seaborn.lineplot``. If none are passed, sensible defaults will be used. Returns ------- :class:`matplotlib.axes._subplots.AxesSubplot` Output of call to ``seaborn.lineplot`` """ if not has_seaborn: raise ImportError("seaborn is not installed. Run 'pip install seaborn'") plt_df = self.long_data(time_axis=time_axis) kwargs.setdefault("x", "time") kwargs.setdefault("y", "value") if "scenario" in self.meta_attributes: kwargs.setdefault("hue", "scenario") kwargs.setdefault("ci", "sd") kwargs.setdefault("estimator", np.median) ax = sns.lineplot(data=plt_df, **kwargs) try: unit = self.get_unique_meta("unit", no_duplicates=True) ax.set_ylabel(unit) except ValueError: pass # don't set ylabel return ax
[docs]def plumeplot( # pragma: no cover self, ax=None, quantiles_plumes=[((0.05, 0.95), 0.5), ((0.5,), 1.0)], hue_var="scenario", hue_label="Scenario", palette=None, style_var="variable", style_label="Variable", dashes=None, linewidth=2, time_axis=None, pre_calculated=False, quantile_over=("ensemble_member",), ): """ Make a plume plot, showing plumes for custom quantiles Parameters ---------- ax : :class:`matplotlib.axes._subplots.AxesSubplot` Axes on which to make the plot quantiles_plumes : list[tuple[tuple, float]] Configuration to use when plotting quantiles. Each element is a tuple, the first element of which is itself a tuple and the second element of which is the alpha to use for the quantile. If the first element has length two, these two elements are the quantiles to plot and a plume will be made between these two quantiles. If the first element has length one, then a line will be plotted to represent this quantile. hue_var : str The column of ``self.meta`` which should be used to distinguish different hues. hue_label : str Label to use in the legend for ``hue_var``. palette : dict Dictionary defining the colour to use for different values of ``hue_var``. style_var : str The column of ``self.meta`` which should be used to distinguish different styles. style_label : str Label to use in the legend for ``style_var``. dashes : dict Dictionary defining the style to use for different values of ``style_var``. linewidth : float Width of lines to use (for quantiles which are not to be shown as plumes) time_axis : str Time axis to use for the plot (see :meth:`~ScmRun.timeseries`) pre_calculated : bool Are the quantiles pre-calculated? If no, the quantiles will be calculated within this function. Pre-calculating the quantiles using :meth:`ScmRun.quantiles_over` can lead to faster plotting if multiple plots are to be made with the same quantiles. quantile_over : str, tuple[str] Columns of ``self.meta`` over which the quantiles should be calculated. Only used if ``pre_calculated`` is ``False``. Returns ------- :class:`matplotlib.axes._subplots.AxesSubplot`, list Axes on which the plot was made and the legend items we have made (in case the user wants to move the legend to a different position for example) Examples -------- >>> scmrun = ScmRun( ... data=np.random.random((10, 3)).T, ... columns={ ... "model": ["a_iam"], ... "climate_model": ["a_model"] * 5 + ["a_model_2"] * 5, ... "scenario": ["a_scenario"] * 5 + ["a_scenario_2"] * 5, ... "ensemble_member": list(range(5)) + list(range(5)), ... "region": ["World"], ... "variable": ["Surface Air Temperature Change"], ... "unit": ["K"], ... }, ... index=[2005, 2010, 2015], ... ) Plot the plumes, calculated over the different ensemble members. >>> scmrun.plumeplot(quantile_over="ensemble_member") Pre-calculate the quantiles, then plot >>> summary_stats = ScmRun( ... scmrun.quantiles_over("ensemble_member", quantiles=quantiles) ... ) >>> summary_stats.plumeplot(pre_calculated=True) Note ---- ``scmdata`` is not a plotting library so this function is provided as is, with little testing. In some ways, it is more intended as inspiration for other users than as a robust plotting tool. """ if not has_matplotlib: raise ImportError("matplotlib is not installed. Run 'pip install matplotlib'") if not pre_calculated: quantiles = [v for qv in quantiles_plumes for v in qv[0]] _pdf = type(self)(self.quantiles_over(quantile_over, quantiles=quantiles)) else: _pdf = self if ax is None: ax = plt.figure().add_subplot(111) _palette = {} if palette is None else palette if dashes is None: _dashes = {} lines = ["-", "--", "-.", ":"] linestyle_cycler = cycle(lines) else: _dashes = dashes _plotted_lines = False quantile_labels = {} for q, alpha in quantiles_plumes: for hdf in _pdf.groupby(hue_var): hue_value = hdf.get_unique_meta(hue_var, no_duplicates=True) pkwargs = {"alpha": alpha} for hsdf in hdf.groupby(style_var): style_value = hsdf.get_unique_meta(style_var, no_duplicates=True) xaxis = hsdf.timeseries(time_axis=time_axis).columns.tolist() if palette is not None: try: pkwargs["color"] = _palette[hue_value] except KeyError as exc: error_msg = "{} not in palette: {}".format(hue_value, palette) raise KeyError(error_msg) from exc elif hue_value in _palette: pkwargs["color"] = _palette[hue_value] if len(q) == 2: label = "{:.0f}th - {:.0f}th".format(q[0] * 100, q[1] * 100) p = ax.fill_between( xaxis, _get_1d_or_raise( hsdf.filter(quantile=q[0]), hue_var, style_var ), _get_1d_or_raise( hsdf.filter(quantile=q[1]), hue_var, style_var ), label=label, **pkwargs ) if palette is None: _palette[hue_value] = p.get_facecolor()[0] elif len(q) == 1: _plotted_lines = True if dashes is not None: try: pkwargs["linestyle"] = _dashes[style_value] except KeyError as exc: error_msg = "{} not in dashes: {}".format( style_value, dashes ) raise KeyError(error_msg) from exc else: if style_value not in _dashes: _dashes[style_value] = next(linestyle_cycler) pkwargs["linestyle"] = _dashes[style_value] if isinstance(q[0], str): label = q[0] else: label = "{:.0f}th".format(q[0] * 100) p = ax.plot( xaxis, _get_1d_or_raise( hsdf.filter(quantile=q[0]), hue_var, style_var ), label=label, linewidth=linewidth, **pkwargs )[0] if dashes is None: _dashes[style_value] = p.get_linestyle() else: raise ValueError( "quantiles to plot must be of length one or two, " "received: {}".format(q) ) if label not in quantile_labels: quantile_labels[label] = p # Fake the line handles for the legend hue_val_lines = [ mlines.Line2D([0], [0], color=_palette[hue_value], label=hue_value) for hue_value in self.get_unique_meta(hue_var) ] legend_items = [ mpatches.Patch(alpha=0, label="Quantiles"), *quantile_labels.values(), mpatches.Patch(alpha=0, label=hue_label), *hue_val_lines, ] if _plotted_lines: style_val_lines = [ mlines.Line2D( [0], [0], linestyle=_dashes[style_value], label=style_value, color="gray", linewidth=linewidth, ) for style_value in self.get_unique_meta(style_var) ] legend_items += [ mpatches.Patch(alpha=0, label=style_label), *style_val_lines, ] else: if dashes is not None: warnings.warn( "`dashes` was passed but no lines were plotted, the style settings " "will not be used" ) ax.legend(handles=legend_items, loc="best") units = self.get_unique_meta("unit") if len(units) == 1: ax.set_ylabel(units[0]) return ax, legend_items
def _get_1d_or_raise(in_scmrun, hue_var, style_var): out_arr = in_scmrun.values.squeeze() if len(out_arr.shape) > 1: quantile = in_scmrun.get_unique_meta("quantile", True) hue_var_value = in_scmrun.get_unique_meta(hue_var, True) style_var_value = in_scmrun.get_unique_meta(style_var, True) error_msg = ( "More than one timeseries for " "quantile: {}, " "{}: {}, " "{}: {}.\n" "Please process your data to create unique quantile timeseries " "before calling :meth:`plumeplot`.\n" "Found: {}".format( quantile, hue_var, hue_var_value, style_var, style_var_value, in_scmrun, ) ) raise ValueError(error_msg) return out_arr def _deprecated_line_plot(self, **kwargs): # pragma: no cover """ Make a line plot via `seaborn's lineplot <https://seaborn.pydata.org/generated/seaborn.lineplot.html>`_ Deprecated: use :func:`lineplot` instead Parameters ---------- **kwargs Keyword arguments to be passed to ``seaborn.lineplot``. If none are passed, sensible defaults will be used. Returns ------- :class:`matplotlib.axes._subplots.AxesSubplot` Output of call to ``seaborn.lineplot`` """ warnings.warn("Use lineplot instead", DeprecationWarning) self.lineplot(**kwargs)
[docs]def inject_plotting_methods(cls): """ Inject the plotting methods Parameters ---------- cls Target class """ methods = [ ("lineplot", lineplot), ("line_plot", _deprecated_line_plot), # for compatibility ("plumeplot", plumeplot), ] for name, f in methods: setattr(cls, name, f)