From 42b0fb87d1057e6c096b20715d43095fc3f50c55 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20L=C3=BCnenschlo=C3=9F?= <peter.luenenschloss@ufz.de>
Date: Mon, 21 Aug 2023 20:37:38 +0200
Subject: [PATCH] Docu update multiplot

---
 CHANGELOG.md                                |  5 +-
 docs/cookbooks/MultivariateFlagging.rst     |  9 +--
 docs/cookbooks/ResidualOutlierDetection.rst | 20 +------
 docs/documentation/GlobalKeywords.rst       |  1 -
 saqc/funcs/tools.py                         | 13 ++++-
 saqc/lib/plotting.py                        | 63 +++++++++++++++------
 6 files changed, 65 insertions(+), 46 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0c4d6d66..fff327b5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 [List of commits](https://git.ufz.de/rdm-software/saqc/-/compare/v2.4.0...develop)
 ### Added
 - add multivariate plotting options to `plot`
-- added `plot_kwargs` keyword to `plot` function
+- added `plot_kwargs` keyword to `plot` function 
 - added checks and unified error message for common inputs.
 - added command line `--version` option
 - `-ll` CLI option as a shorthand for `--log-level`
@@ -20,6 +20,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
 - pin pandas to versions >= 2.0
 - parameter `fill_na` of `SaQC.flagUniLOF` and `SaQC.assignUniLOF` is now of type 
   `bool` instead of one of `[None, "linear"]`
+- in `plot` function: changed default color for single variables to `black` with `80% transparency`
+- in `plot` function: added seperate legend for flags
+
 ### Removed
 - removed deprecated `DictOfSeries.to_df`
 - removed plotting option with complete history (`history="complete"`)
diff --git a/docs/cookbooks/MultivariateFlagging.rst b/docs/cookbooks/MultivariateFlagging.rst
index a66e30ed2..e4e6b78ba 100644
--- a/docs/cookbooks/MultivariateFlagging.rst
+++ b/docs/cookbooks/MultivariateFlagging.rst
@@ -246,17 +246,14 @@ Check out the results for the year *2016*
 
 .. doctest:: exampleMV
 
-   >>> plt.plot(qc.data['sac254_raw']['2016'], alpha=.5, color='black', label='original') # doctest:+SKIP
-   >>> plt.plot(qc.data['sac254_corrected']['2016'], color='black', label='corrected') # doctest:+SKIP
+   >>> qc.plot(['sac254_raw','sac254_corrected'], xscope='2016', plot_kwargs={'color':['black', 'black'], 'alpha':[.5, 1], 'label':['original', 'corrrected']}) # doctest:+SKIP
 
 .. plot::
    :context:
    :include-source: False
 
-   plt.figure(figsize=(16,9))
-   plt.plot(qc.data['sac254_raw']['2016'], alpha=.5, color='black', label='original')
-   plt.plot(qc.data['sac254_corrected']['2016'], color='black', label='corrected')
-   plt.legend()
+   >>> qc.plot(['sac254_raw','sac254_corrected'], xscope='2016', plot_kwargs={'color':['black', 'black'], 'alpha':[.5, 1], 'label':['original', 'corrrected']})
+
 
 Multivariate Flagging Procedure
 -------------------------------
diff --git a/docs/cookbooks/ResidualOutlierDetection.rst b/docs/cookbooks/ResidualOutlierDetection.rst
index d022ad7a5..0d60150d5 100644
--- a/docs/cookbooks/ResidualOutlierDetection.rst
+++ b/docs/cookbooks/ResidualOutlierDetection.rst
@@ -255,25 +255,11 @@ This function object, we can pass on to the :py:meth:`~saqc.SaQC.processGeneric`
 Visualisation
 -------------
 
-We can obtain those updated informations by generating a `pandas dataframe <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html>`_
-representation of it, with the :py:attr:`data <saqc.core.core.SaQC.data>` method:
+To see all the results obtained so far, plotted in one figure window, we make use of the :py:meth:`~saqc.SaQC.plot` method.
 
 .. doctest:: exampleOD
 
-   >>> data = qc.data
-
-.. plot::
-   :context:
-   :include-source: False
-
-   data = qc.data
-
-To see all the results obtained so far, plotted in one figure window, we make use of the dataframes `plot <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.plot.html>`_ method.
-
-.. doctest:: exampleOD
-
-   >>> data.to_pandas().plot()
-   <Axes...>
+   >>> qc.plot(".", regex=True) # doctest: +SKIP
 
 .. plot::
    :context:
@@ -281,7 +267,7 @@ To see all the results obtained so far, plotted in one figure window, we make us
    :width: 80 %
    :class: center
 
-   data.to_pandas().plot()
+   qc.plot(".", regex=True)
 
 
 Residuals and Scores
diff --git a/docs/documentation/GlobalKeywords.rst b/docs/documentation/GlobalKeywords.rst
index 781dfbc6f..101f64c31 100644
--- a/docs/documentation/GlobalKeywords.rst
+++ b/docs/documentation/GlobalKeywords.rst
@@ -37,7 +37,6 @@ Example Data
    :context: close-figs
    :include-source: False
 
-   import matplotlib.pyplot as plt
    import pandas as pd
    import numpy as np
    import saqc
diff --git a/saqc/funcs/tools.py b/saqc/funcs/tools.py
index 599ac6bc9..7da335d54 100644
--- a/saqc/funcs/tools.py
+++ b/saqc/funcs/tools.py
@@ -306,9 +306,16 @@ class ToolsMixin:
             * ``"cycleskip"``: (int) start the cycle of shapes that are assigned any flag-type with a certain lag - defaults to ``0`` (no skip)
 
         plot_kwargs :
-            Keywords to modify data line appearance. The markers are set via the
-            `matplotlib.pyplot.plot <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.scatter.html>`_
-            method and can have the options listed there.
+            Keywords to modify the plot appearance. The plotting is delegated to
+            `matplotlib.pyplot.plot <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.scatter.html>`_, all options listed there are available. Additionally the following saqc specific configurations are possible:
+
+            * ``"alpha"``: Either a scalar float in *[0,1]*, that determines all plots' transparencies, or
+              a list of floats, matching the number of variables to plot.
+
+            * ``"linewidth"``: Either single float in *[0,1]*, that determines the thickness of all plotted,
+              or a list of floats, matching the number of variables to plot.
+
+
 
         Notes
         -----
diff --git a/saqc/lib/plotting.py b/saqc/lib/plotting.py
index 5c5d60448..ab13e9ea8 100644
--- a/saqc/lib/plotting.py
+++ b/saqc/lib/plotting.py
@@ -35,7 +35,7 @@ MARKER_COL_CYCLE = [
 ]
 
 # default color cycle for plot colors (many-in-one-plots)
-PLOT_COL_CYCLE = MARKER_COL_CYCLE  # itertools.cycle(MARKER_COL_CYCLE)
+PLOT_COL_CYCLE = [(0, 0, 0)] + MARKER_COL_CYCLE  # itertools.cycle(MARKER_COL_CYCLE)
 
 # default data plot configuration (color kwarg only effective for many-to-one-plots)
 PLOT_KWARGS = {"alpha": 0.8, "linewidth": 1, "color": PLOT_COL_CYCLE}
@@ -248,14 +248,15 @@ def makeFig(
         mode,
     )
 
+    # readability formattin fo the x-tick labels:
+    fig.autofmt_xdate()
     return fig
 
 
-def _instantiateKwargContext(
+def _instantiateAxesContext(
     plot_kwargs, scatter_kwargs, ax_kwargs, var_num, var_name, mode
 ):
     _scatter_mem = {}
-    _plot_kwargs = plot_kwargs.copy()
     _scatter_kwargs = scatter_kwargs.copy()
     _ax_kwargs = ax_kwargs.copy()
     _scatter_mem = {}
@@ -286,7 +287,6 @@ def _instantiateKwargContext(
     _ax_kwargs["title"] = var_name if title is None else title
 
     return (
-        _plot_kwargs,
         _scatter_kwargs,
         _ax_kwargs,
         _scatter_mem,
@@ -379,6 +379,26 @@ def _configMarkers(
     return flags_i, _scatter_kwargs, _scatter_mem, marker_shape_cycle, marker_col_cycle
 
 
+def _instantiatePlotContext(plot_kwargs, mode, var_name, var_num, plot_col_cycle):
+    _plot_kwargs = plot_kwargs.copy()
+    # get current plot color from plot color cycle
+    _plot_kwargs["color"] = next(plot_col_cycle)
+    # assign variable specific plot appearance
+    for plot_spec in ["alpha", "linewidth", "label"]:
+        spec = plot_kwargs.get(plot_spec, None)
+        if isinstance(spec, list):
+            _plot_kwargs[plot_spec] = spec[var_num]
+        elif isinstance(spec, dict):
+            _plot_kwargs[plot_spec] = spec.get(var_name, None)
+
+    if mode == "oneplot":
+        _plot_kwargs["label"] = _plot_kwargs.get("label", None) or var_name
+    # when plotting in subplots, plot black line and label it as 'data' (if not opted otherwise)
+    else:
+        _plot_kwargs["label"] = _plot_kwargs.get("label", None) or "data"
+    return _plot_kwargs
+
+
 def _plotVarWithFlags(
     axes,
     dat_dict,
@@ -410,25 +430,19 @@ def _plotVarWithFlags(
         # every time, axis target is fresh, reinstantiate the kwarg-contexts :
         if var_num == 0 or mode == "subplots":
             (
-                _plot_kwargs,
                 _scatter_kwargs,
                 _ax_kwargs,
                 _scatter_mem,
                 marker_col_cycle,
                 marker_shape_cycle,
-            ) = _instantiateKwargContext(
+            ) = _instantiateAxesContext(
                 plot_kwargs, scatter_kwargs, ax_kwargs, var_num, var_name, mode
             )
             ax.set(**_ax_kwargs)
 
-        # get current color from plot color cycle
-        _plot_kwargs["color"] = next(plot_col_cycle)
-        if mode == "oneplot":
-            _plot_kwargs["label"] = var_name
-        # when plotting in subplots, plot black line and label it as 'data' (if not opted otherwise)
-        else:
-            _plot_kwargs["label"] = _plot_kwargs.get("label", None) or "data"
-
+        _plot_kwargs = _instantiatePlotContext(
+            plot_kwargs, mode, var_name, var_num, plot_col_cycle
+        )
         # plot the data
         ax.plot(var_dat, **_plot_kwargs)
 
@@ -482,12 +496,14 @@ def _plotVarWithFlags(
                 _scatter_kwargs,
             )
 
-        _rmDupesFromLegend(ax, dat_dict)
-
+    _formatLegend(ax, dat_dict)
+    if mode == "subplots":
+        for ax in axes[1:]:
+            _formatLegend(ax, dat_dict)
     return
 
 
-def _rmDupesFromLegend(ax, dat_dict):
+def _formatLegend(ax, dat_dict):
     # the legend generated might contain dublucate entries, we remove those, since dubed entries are assigned all
     # the same marker color and shape:
     legend_h, legend_l = ax.get_legend_handles_labels()
@@ -503,7 +519,18 @@ def _rmDupesFromLegend(ax, dat_dict):
             legend_f.append((legend_h[l[0]], l[1]))
     leg_l = [l[1] for l in legend_v] + [l[1] for l in legend_f]
     leg_h = [l[0] for l in legend_v] + [l[0] for l in legend_f]
-    ax.legend(leg_h, leg_l)
+    # if more than one variable is plotted, list plot line and flag marker shapes in seperate
+    # legends
+    h_types = np.array([isinstance(h, mpl.lines.Line2D) for h in leg_h])
+    if sum(h_types) > 1:
+        lines_h = np.array(leg_h)[h_types]
+        lines_l = np.array(leg_l)[h_types]
+        flags_h = np.array(leg_h)[~h_types]
+        flags_l = np.array(leg_l)[~h_types]
+        ax.add_artist(plt.legend(flags_h, flags_l))
+        ax.legend(lines_h, lines_l)
+    else:
+        ax.legend(leg_h, leg_l)
     return
 
 
-- 
GitLab