Repository: pyfar/pyfar
Issue: Plots for multi-dimensional signals fail

## General

* pyfar version: all

## Description

Pyfar line plots to not work for multidimensional signals.
I'd propose flattening and adjusting the legend to represent the cshape of the plotted signal object, i.e. `'Ch 1,2'`. instead of the flattened index.

Changed files:
pyfar/plot/_interaction.py, tests/test_interaction.py, pyfar/plot/line.py

diff --git a/pyfar/plot/_interaction.py b/pyfar/plot/_interaction.py
index 40ce684b..9afde985 100644
--- a/pyfar/plot/_interaction.py
+++ b/pyfar/plot/_interaction.py
@@ -58,18 +58,22 @@ class Cycle(object):
     """ Cycle class implementation inspired by itertools.cycle. Supports
     circular iterations into two directions.
     """
-    def __init__(self, n_channels, index=0):
+    def __init__(self, cshape, index=0):
         """
         Parameters
         ----------
-        n_channels : int
-            number of channels in the signal
+        cshape : tupel, array like, int
+            cshape of the signal
         index : int, optional
             index of the current channel. The default is 0
         """
-        self._n_channels = n_channels
+        self._n_channels = np.prod(cshape)
         self._index = index
 
+        self._channels = []
+        for index in np.ndindex(cshape):
+            self._channels.append(index)
+
     def increase_index(self):
         self._index = (self.index + 1) % self.n_channels
 
@@ -81,12 +85,19 @@ class Cycle(object):
 
     @property
     def index(self):
-        return self._index
+        return int(self._index)
 
     @property
     def n_channels(self):
         return self._n_channels
 
+    @property
+    def current_channel(self):
+        channel = self._channels[self.index]
+        if len(channel) == 1:
+            channel = channel[0]
+        return channel
+
 
 class EventEmu(object):
     def __init__(self, key):
@@ -397,7 +408,8 @@ class Interaction(object):
         """
 
         # save input arguments
-        self.signal = signal
+        self.cshape = signal.cshape
+        self.signal = signal.flatten()
         self.ax = axes
         self.figure = axes.figure
         self.style = style
@@ -408,7 +420,7 @@ class Interaction(object):
         self.event = None
 
         # initialize cycler
-        self.cycler = Cycle(self.signal.cshape[0])
+        self.cycler = Cycle(self.cshape)
 
         # initialize visibility
         self.all_visible = True
@@ -538,7 +550,8 @@ class Interaction(object):
             elif event.key in plot['group_delay']:
                 self.params.update_axis_type('group_delay')
                 self.ax = _line._group_delay(
-                    self.signal, prm.unit, prm.xscale, self.ax, **self.kwargs)
+                    self.signal, prm.unit, prm.xscale, self.ax,
+                    **self.kwargs)
 
             elif event.key in plot['spectrogram']:
                 self.params.update_axis_type('spectrogram')
@@ -720,7 +733,7 @@ class Interaction(object):
                         ec=mpl.rcParams["axes.facecolor"], alpha=.5)
 
             self.txt = self.ax.text(
-                x_pos, y_pos, f'Ch. {self.cycler.index}',
+                x_pos, y_pos, f'Ch. {self.cycler.current_channel}',
                 horizontalalignment='right', verticalalignment='baseline',
                 bbox=bbox, transform=self.ax.transAxes)
 
diff --git a/pyfar/plot/line.py b/pyfar/plot/line.py
index 09b4d65e..2eefb047 100644
--- a/pyfar/plot/line.py
+++ b/pyfar/plot/line.py
@@ -16,7 +16,9 @@ def time(signal, dB=False, log_prefix=20, log_reference=1, unit=None, ax=None,
     Parameters
     ----------
     signal : Signal, TimeData
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     dB : bool
         Indicate if the data should be plotted in dB in which case
         ``log_prefix * np.log10(signal.time / log_reference)`` is used. The
@@ -57,7 +59,7 @@ def time(signal, dB=False, log_prefix=20, log_reference=1, unit=None, ax=None,
     """
 
     with context(style):
-        ax = _line._time(signal, dB, log_prefix, log_reference, unit,
+        ax = _line._time(signal.flatten(), dB, log_prefix, log_reference, unit,
                          ax, **kwargs)
     plt.tight_layout()
 
@@ -82,7 +84,9 @@ def freq(signal, dB=True, log_prefix=20, log_reference=1, xscale='log',
     Parameters
     ----------
     signal : Signal, FrequencyData
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     dB : bool
         Indicate if the data should be plotted in dB in which case
         ``log_prefix * np.log10(abs(signal.freq) / log_reference)`` is used.
@@ -121,8 +125,8 @@ def freq(signal, dB=True, log_prefix=20, log_reference=1, xscale='log',
     """
 
     with context(style):
-        ax = _line._freq(signal, dB, log_prefix, log_reference, xscale, ax,
-                         **kwargs)
+        ax = _line._freq(signal.flatten(), dB, log_prefix, log_reference,
+                         xscale, ax, **kwargs)
     plt.tight_layout()
 
     # manage interaction
@@ -145,7 +149,9 @@ def phase(signal, deg=False, unwrap=False, xscale='log', ax=None,
     Parameters
     ----------
     signal : Signal, FrequencyData
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     deg : bool
         Plot the phase in degrees. The default is ``False``, which plots the
         phase in radians.
@@ -180,7 +186,7 @@ def phase(signal, deg=False, unwrap=False, xscale='log', ax=None,
     """
 
     with context(style):
-        ax = _line._phase(signal, deg, unwrap, xscale, ax, **kwargs)
+        ax = _line._phase(signal.flatten(), deg, unwrap, xscale, ax, **kwargs)
     plt.tight_layout()
 
     # manage interaction
@@ -201,7 +207,9 @@ def group_delay(signal, unit=None, xscale='log', ax=None, style='light',
     Parameters
     ----------
     signal : Signal
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     unit : str, None
         Unit of the group delay. Can be ``s``, ``ms``, ``mus``, or ``samples``.
         The default is ``None``, which sets the unit to ``s`` (seconds), ``ms``
@@ -234,7 +242,7 @@ def group_delay(signal, unit=None, xscale='log', ax=None, style='light',
     """
 
     with context(style):
-        ax = _line._group_delay(signal, unit, xscale, ax, **kwargs)
+        ax = _line._group_delay(signal.flatten(), unit, xscale, ax, **kwargs)
     plt.tight_layout()
 
     # manage interaction
@@ -255,7 +263,9 @@ def spectrogram(signal, dB=True, log_prefix=20, log_reference=1,
     Parameters
     ----------
     signal : Signal
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     dB : bool
         Indicate if the data should be plotted in dB in which case
         ``log_prefix * np.log10(abs(signal.freq) / log_reference)`` is used.
@@ -311,7 +321,7 @@ def spectrogram(signal, dB=True, log_prefix=20, log_reference=1,
 
     with context(style):
         ax = _line._spectrogram_cb(
-            signal, dB, log_prefix, log_reference, yscale, unit,
+            signal.flatten(), dB, log_prefix, log_reference, yscale, unit,
             window, window_length, window_overlap_fct,
             cmap, ax)
     plt.tight_layout()
@@ -338,7 +348,9 @@ def time_freq(signal, dB_time=False, dB_freq=True, log_prefix=20,
     Parameters
     ----------
     signal : Signal
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     dB_time : bool
         Indicate if the data should be plotted in dB in which case
         ``log_prefix * np.log10(signal.time / log_reference)`` is used. The
@@ -385,7 +397,7 @@ def time_freq(signal, dB_time=False, dB_freq=True, log_prefix=20,
     """
 
     with context(style):
-        ax = _line._time_freq(signal, dB_time, dB_freq, log_prefix,
+        ax = _line._time_freq(signal.flatten(), dB_time, dB_freq, log_prefix,
                               log_reference, xscale, unit, ax, **kwargs)
     plt.tight_layout()
 
@@ -407,7 +419,9 @@ def freq_phase(signal, dB=True, log_prefix=20, log_reference=1, xscale='log',
     Parameters
     ----------
     signal : Signal, FrequencyData
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     dB : bool
         Indicate if the data should be plotted in dB in which case
         ``log_prefix * np.log10(abs(signal.freq) / log_reference)`` is used.
@@ -446,8 +460,8 @@ def freq_phase(signal, dB=True, log_prefix=20, log_reference=1, xscale='log',
     """
 
     with context(style):
-        ax = _line._freq_phase(signal, dB, log_prefix, log_reference, xscale,
-                               deg, unwrap, ax, **kwargs)
+        ax = _line._freq_phase(signal.flatten(), dB, log_prefix, log_reference,
+                               xscale, deg, unwrap, ax, **kwargs)
     plt.tight_layout()
 
     # manage interaction
@@ -471,7 +485,9 @@ def freq_group_delay(signal, dB=True, log_prefix=20, log_reference=1,
     Parameters
     ----------
     signal : Signal, FrequencyData
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     dB : bool
         Flag to plot the logarithmic magnitude spectrum. The default is
         ``True``.
@@ -513,8 +529,9 @@ def freq_group_delay(signal, dB=True, log_prefix=20, log_reference=1,
     """
 
     with context(style):
-        ax = _line._freq_group_delay(signal, dB, log_prefix, log_reference,
-                                     unit, xscale, ax, **kwargs)
+        ax = _line._freq_group_delay(
+            signal.flatten(), dB, log_prefix, log_reference,
+            unit, xscale, ax, **kwargs)
     plt.tight_layout()
 
     # manage interaction
@@ -539,7 +556,9 @@ def custom_subplots(signal, plots, ax=None, style='light', **kwargs):
     Parameters
     ----------
     signal : Signal
-        The input data to be plotted.
+        The input data to be plotted. Multidimensional data are flattened for
+        plotting, e.g, a signal of ``signal.cshape = (2, 2)`` would be plotted
+        in the order ``(0, 0)``, ``(0, 1)``, ``(1, 0)``, ``(1, 1)``.
     plots : list, nested list
         Function handles for plotting.
     ax : matplotlib.pyplot.axes
@@ -572,7 +591,7 @@ def custom_subplots(signal, plots, ax=None, style='light', **kwargs):
     """
 
     with context(style):
-        ax = _line._custom_subplots(signal, plots, ax, **kwargs)
+        ax = _line._custom_subplots(signal.flatten(), plots, ax, **kwargs)
     plt.tight_layout()
 
     return ax
diff --git a/tests/test_interaction.py b/tests/test_interaction.py
index e0f0e87f..9c364ce3 100644
--- a/tests/test_interaction.py
+++ b/tests/test_interaction.py
@@ -317,8 +317,8 @@ def test_toggle_colormap():
     plt.close("all")
 
 
-def test_cycle_and_toggle_lines():
-    """Test toggling and cycling channels of a Signal."""
+def test_cycle_and_toggle_lines_1d_signal():
+    """Test toggling and cycling channels of a one-dimensional Signal."""
 
     # init and check start conditions
     signal = pf.Signal([[1, 0], [2, 0]], 44100)
@@ -350,6 +350,42 @@ def test_cycle_and_toggle_lines():
     plt.close("all")
 
 
+def test_cycle_and_toggle_lines_2d_signal():
+    """Test toggling and cycling channels of a two-dimensional Signal."""
+
+    # init and check start conditions
+    signal = pf.signals.impulse(10, [[1, 2], [3, 4]])
+    ax = pf.plot.time(signal)
+    for line in [0, 1, 2, 3]:
+        assert ax.lines[line].get_visible() is True
+    assert ax.interaction.txt is None
+    # toggle all
+    ax.interaction.select_action(ia.EventEmu(sc_ctr["toggle_all"]["key"][0]))
+    assert ax.lines[0].get_visible() is True
+    for line in [1, 2, 3]:
+        assert ax.lines[line].get_visible() is False
+    assert ax.interaction.txt.get_text() == "Ch. (0, 0)"
+    # next
+    ax.interaction.select_action(ia.EventEmu(sc_ctr["next"]["key"][0]))
+    for line in [0, 2, 3]:
+        assert ax.lines[line].get_visible() is False
+    assert ax.lines[1].get_visible() is True
+    assert ax.interaction.txt.get_text() == "Ch. (0, 1)"
+    # previous
+    ax.interaction.select_action(ia.EventEmu(sc_ctr["prev"]["key"][0]))
+    assert ax.lines[0].get_visible() is True
+    for line in [1, 2, 3]:
+        assert ax.lines[line].get_visible() is False
+    assert ax.interaction.txt.get_text() == "Ch. (0, 0)"
+    # toggle all
+    ax.interaction.select_action(ia.EventEmu(sc_ctr["toggle_all"]["key"][0]))
+    for line in [0, 1, 2, 3]:
+        assert ax.lines[line].get_visible() is True
+    assert ax.interaction.txt is None
+
+    plt.close("all")
+
+
 def test_cycle_and_toggle_signals():
     """Test toggling and cycling Signal slices."""