Repository: pyfar/pyfar
Pull request #68 (2020-12-20 08:41:05)

add time units for time plots
### Which issue(s) are closed by this pull request?

Closes nothing

### Changes proposed in this pull request:

- added unit for time plots. Time data can now be plotted on time axis in seconds, mili seconds, micro seconds, and samples
- default behavior detects the best unit (as already implemented in group delay plot)


Patch:
diff --git a/pyfar/plot/_line.py b/pyfar/plot/_line.py
index 0428ac49..86a3610f 100644
--- a/pyfar/plot/_line.py
+++ b/pyfar/plot/_line.py
@@ -133,7 +133,7 @@ def _return_default_colors_rgb(**kwargs):
     return kwargs
 
 
-def _time(signal, dB=False, log_prefix=20, log_reference=1,
+def _time(signal, dB=False, log_prefix=20, log_reference=1, unit=None,
           ax=None, **kwargs):
     """Plot the time logairhmic data of a signal."""
 
@@ -152,19 +152,29 @@ def _time(signal, dB=False, log_prefix=20, log_reference=1,
         ymin = ymax - 90
         ymax = ymax + 10
 
+    # auto detect the time unit
+    if unit is None:
+        unit = _time_auto_unit(signal.times[..., -1])
+    # set the unit
+    if unit == 'samples':
+        times = np.arange(signal.n_samples)
+    else:
+        factor, unit = _deal_time_units(unit)
+        times = signal.times * factor
+
     # prepare figure
     _, ax = _prepare_plot(ax)
-    ax.set_xlabel("Time in s")
+    ax.set_xlabel(f"Time in {unit}")
     if dB:
         ax.set_ylabel("Amplitude in dB")
         _set_axlim(ax, ax.set_ylim, ymin, ymax, ax.get_ylim())
     else:
         ax.set_ylabel("Amplitude")
-    _set_axlim(ax, ax.set_xlim, signal.times[0], signal.times[-1],
+    _set_axlim(ax, ax.set_xlim, times[0], times[-1],
                ax.get_xlim())
 
     # plot data
-    ax.plot(signal.times, data, **kwargs)
+    ax.plot(times, data, **kwargs)
     plt.tight_layout()
 
     return ax
@@ -288,16 +298,12 @@ def _group_delay(signal, unit=None, xscale='log', ax=None, **kwargs):
     data = dsp.group_delay(signal)
     # auto detect the unit
     if unit is None:
-        unit = _group_delay_auto_unit(
+        unit = _time_auto_unit(
             np.nanmax(np.abs(data) / signal.sampling_rate))
     # set the unit
-    if unit == 's':
-        data = data / signal.sampling_rate
-    elif unit == 'ms':
-        data = data / signal.sampling_rate * 1e3
-    elif unit == 'mus':
-        data = data / signal.sampling_rate * 1e6
-        unit = 'micro s'
+    if unit != "samples":
+        factor, unit = _deal_time_units(unit)
+        data = data / signal.sampling_rate * factor
 
     # prepare figure
     _, ax = _prepare_plot(ax)
@@ -324,24 +330,24 @@ def _group_delay(signal, unit=None, xscale='log', ax=None, **kwargs):
     return ax
 
 
-def _group_delay_auto_unit(gd_max):
+def _time_auto_unit(time_max):
     """
-    Automatically set the unit for the group delay plot according to the
-    absolute maximum of the input data. This is a separate function for ease of
-    testing.
+    Automatically set the unit for time axis according to the absolute maximum
+    of the input data. This is a separate function for ease of testing and for
+    use across different plots.
 
     Parameters
     ----------
 
-    gd_max : float
-        Absolute maximum of the group delay in seconds
+    time_max : float
+        Absolute maximum of the time data in seconds
     """
 
-    if gd_max == 0:
+    if time_max == 0:
         unit = 's'
-    elif gd_max < 1e-3:
+    elif time_max < 1e-3:
         unit = 'mus'
-    elif gd_max < 1:
+    elif time_max < 1:
         unit = 'ms'
     else:
         unit = 's'
@@ -349,6 +355,40 @@ def _group_delay_auto_unit(gd_max):
     return unit
 
 
+def _deal_time_units(unit='s'):
+    """Return scaling factor and string representation for unit multiplier
+    modifications.
+
+    Parameters
+    ----------
+    unit : 'str'
+        The unit to be used
+
+    Returns
+    -------
+    factor : float
+        Factor the data is to be multiplied with, i.e. 1e-3 for milliseconds
+    string : str
+        String representation of the unit using LaTeX formatting.
+    """
+    if unit == 's':
+        factor = 1
+        string = 's'
+    elif unit == 'ms':
+        factor = 1 / 1e-3
+        string = 'ms'
+    elif unit == 'mus':
+        factor = 1 / 1e-6
+        string = r'$\mathrm{\mu s}$'
+    elif unit == 'samples':
+        factor = 1
+        string = 'samples'
+    else:
+        factor = 1
+        string = ''
+    return factor, string
+
+
 def _spectrogram(signal, dB=True, log_prefix=20, log_reference=1,
                  yscale='linear',
                  window='hann', window_length=1024, window_overlap_fct=0.5,
@@ -454,7 +494,7 @@ def _spectrogram_cb(signal, dB=True, log_prefix=20, log_reference=1,
 
 
 def _time_freq(signal, dB_time=False, dB_freq=True, log_prefix=20,
-               log_reference=1, xscale='log', ax=None, **kwargs):
+               log_reference=1, xscale='log', unit=None, ax=None, **kwargs):
     """
     Plot the time signal and magnitude spectrum in a 2 by 1 subplot layout.
     """
@@ -465,7 +505,7 @@ def _time_freq(signal, dB_time=False, dB_freq=True, log_prefix=20,
     fig, ax = _prepare_plot(ax, (2, 1))
     kwargs = _return_default_colors_rgb(**kwargs)
 
-    _time(signal, dB_time, log_prefix, log_reference, ax[0], **kwargs)
+    _time(signal, dB_time, log_prefix, log_reference, unit, ax[0], **kwargs)
     _freq(signal, dB_freq, log_prefix, log_reference, xscale, ax[1], **kwargs)
     fig.align_ylabels()
     plt.tight_layout()
diff --git a/pyfar/plot/line.py b/pyfar/plot/line.py
index 79f20dfe..5a4f01ae 100644
--- a/pyfar/plot/line.py
+++ b/pyfar/plot/line.py
@@ -6,7 +6,7 @@ from . import _line
 from ._interaction import Interaction
 
 
-def time(signal, dB=False, log_prefix=20, log_reference=1, ax=None,
+def time(signal, dB=False, log_prefix=20, log_reference=1, unit=None, ax=None,
          style='light', **kwargs):
     """Plot the time data of a signal.
 
@@ -23,6 +23,10 @@ def time(signal, dB=False, log_prefix=20, log_reference=1, ax=None,
         Prefix for calculating the logarithmic time data. The default is 20.
     log_reference : integer
         Reference for calculating the logarithmic time data. The default is 1.
+    unit : str, None
+        Unit of the time axis. Can be 's', 'ms', 'mus', or 'samples'.
+        The default is None, which sets the unit to 's' (seconds), 'ms'
+        (milli seconds), or 'mus' (micro seconds) depending on the maximum.
     ax : matplotlib.pyplot.axes object
         Axes to plot on. The default is None, which uses the current figure
         ore creates a new one if no figure exists.
@@ -46,7 +50,7 @@ def time(signal, dB=False, log_prefix=20, log_reference=1, ax=None,
         raise TypeError('Input data has to be of type: Signal.')
 
     with plt.style.context(plotstyle(style)):
-        ax = _line._time(signal, dB, log_prefix, log_reference,
+        ax = _line._time(signal, dB, log_prefix, log_reference, unit,
                          ax, **kwargs)
 
     plt.tight_layout()
@@ -164,7 +168,7 @@ def group_delay(signal, unit=None, xscale='log', ax=None, style='light',
     ----------
     signal : Signal
         pyfar Signal object.
-    unit : str
+    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'
         (milli seconds), or 'mus' (micro seconds) depending on the maximum.
@@ -261,7 +265,8 @@ def spectrogram(signal, dB=True, log_prefix=20, log_reference=1,
 
 
 def time_freq(signal, dB_time=False, dB_freq=True, log_prefix=20,
-              log_reference=1, xscale='log', ax=None, style='light', **kwargs):
+              log_reference=1, xscale='log', unit=None,
+              ax=None, style='light', **kwargs):
     """
     Plot the time signal and magnitude spectrum in a 2 by 1 subplot layout.
 
@@ -280,8 +285,13 @@ def time_freq(signal, dB_time=False, dB_freq=True, log_prefix=20,
         Reference for calculating the logarithmic time/frequency data.
         The default is 1.
     xscale : str
-        'linear' or 'log' to plot on a linear or logarithmic x-axis. The
-        default is 'log'.
+        'linear' or 'log' to plot on a linear or logarithmic x-axis for the
+        frequency plot. The default is 'log'.
+    unit : str
+        Unit of the time axis. Can be 's', 'ms', 'mus', or 'samples'.
+        The default is None, which sets the unit to 's' (seconds), 'ms'
+        (milli seconds), or 'mus' (micro seconds) depending on the maximum.
+    ax : matplotlib.pyplot.axes object
     ax : matplotlib.pyplot.axes object
         Axes to plot on. The default is None, which uses the current figure
         ore creates a new one if no figure exists.
@@ -306,7 +316,7 @@ def time_freq(signal, dB_time=False, dB_freq=True, log_prefix=20,
 
     with plt.style.context(plotstyle(style)):
         ax = _line._time_freq(signal, dB_time, dB_freq, log_prefix,
-                              log_reference, xscale, ax, **kwargs)
+                              log_reference, xscale, unit, ax, **kwargs)
     plt.tight_layout()
     Interaction('LineXLin', ax[0], signal, style, **kwargs)
 
diff --git a/pyfar/plot/plotstyles/dark.mplstyle b/pyfar/plot/plotstyles/dark.mplstyle
index aff3a215..dc2e6c16 100644
--- a/pyfar/plot/plotstyles/dark.mplstyle
+++ b/pyfar/plot/plotstyles/dark.mplstyle
@@ -20,9 +20,8 @@ font.style          : normal
 font.variant        : normal
 font.weight         : normal
 font.stretch        : normal
-font.size           : 10.0
-font.sans-serif     : Arial, DejaVu Sans, Bitstream Vera Sans, Computer Modern Sans Serif, Lucida Grande, Verdana, Geneva, Lucid, Avant Garde, sans-serif
-
+font.sans-serif     : DejaVu Sans, Arial, Bitstream Vera Sans, Computer Modern Sans Serif, Lucida Grande, Verdana, Geneva, Lucid, Avant Garde, sans-serif
+mathtext.fontset    : dejavusans
 
 #### TEXT
 text.color          : white
@@ -32,9 +31,7 @@ text.color          : white
 axes.facecolor      : [.1,.1,.1]    ## axes background color
 axes.edgecolor      : white         ## axes edge color
 axes.grid           : True          ## display grid or not
-axes.titlesize      : 10            ## fontsize of the axes title
 axes.titleweight    : bold          ## font weight of title
-axes.labelsize      : 10            ## fontsize of the x any y labels
 axes.labelcolor     : white
 # colors from palettable.cartocolors.qualitative.Prism_8
 # slightly changed colors to work with dark and light theme
@@ -47,12 +44,10 @@ axes.prop_cycle     : cycler('color', ['1471B9','D83C27','ECAD20','5F4690','0785
 ## see http://matplotlib.org/api/axis_api.html#matplotlib.axis.Tick
 xtick.bottom         : False    ## draw ticks on the bottom side
 xtick.color          : white    ## color of the tick labels
-xtick.labelsize      : 10       ## fontsize of the tick labels
 
 
 ytick.left           : False    ## draw ticks on the left side
 ytick.color          : white    ## color of the tick labels
-ytick.labelsize      : 10       ## fontsize of the tick labels
 
 
 #### GRIDS
diff --git a/pyfar/plot/plotstyles/light.mplstyle b/pyfar/plot/plotstyles/light.mplstyle
index 9987baa7..091d1679 100644
--- a/pyfar/plot/plotstyles/light.mplstyle
+++ b/pyfar/plot/plotstyles/light.mplstyle
@@ -20,8 +20,8 @@ font.style          : normal
 font.variant        : normal
 font.weight         : normal
 font.stretch        : normal
-font.size           : 10.0
-font.sans-serif     : Arial, DejaVu Sans, Bitstream Vera Sans, Computer Modern Sans Serif, Lucida Grande, Verdana, Geneva, Lucid, Avant Garde, sans-serif
+font.sans-serif     : DejaVu Sans, Arial, Bitstream Vera Sans, Computer Modern Sans Serif, Lucida Grande, Verdana, Geneva, Lucid, Avant Garde, sans-serif
+mathtext.fontset    : dejavusans
 
 
 #### TEXT
@@ -32,9 +32,7 @@ text.color          : [.2,.2,.2]
 axes.facecolor      : white        ## axes background color
 axes.edgecolor      : [.2,.2,.2]   ## axes edge color
 axes.grid           : True         ## display grid or not
-axes.titlesize      : 10           ## fontsize of the axes title
 axes.titleweight    : bold         ## font weight of title
-axes.labelsize      : 10           ## fontsize of the x any y labels
 axes.labelcolor     : [.2,.2,.2]
 # colors from palettable.cartocolors.qualitative.Prism_8
 # slightly changed colors to work with dark and light theme
@@ -47,12 +45,10 @@ axes.prop_cycle     : cycler('color', ['1471B9','D83C27','ECAD20','5F4690','0785
 ## see http://matplotlib.org/api/axis_api.html#matplotlib.axis.Tick
 xtick.bottom         : False        ## draw ticks on the bottom side
 xtick.color          : [.2,.2,.2]   ## color of the tick labels
-xtick.labelsize      : 10           ## fontsize of the tick labels
 
 
 ytick.left           : False        ## draw ticks on the left side
 ytick.color          : [.2,.2,.2]   ## color of the tick labels
-ytick.labelsize      : 10           ## fontsize of the tick labels
 
 
 #### GRIDS


Test patch:
diff --git a/tests/test_plot.py b/tests/test_plot.py
index afe53cfb..700d0691 100644
--- a/tests/test_plot.py
+++ b/tests/test_plot.py
@@ -219,42 +219,46 @@ def test_line_xscale_option(signal_mocks):
             compare_images(baseline, output, tol=10)
 
 
-def test_line_group_delay_unit(signal_mocks):
-    """Test plottin the group delay with different units."""
+def test_time_unit(signal_mocks):
+    """Test plottin with different units."""
 
-    for unit in [None, 's', 'ms', 'mus', 'samples']:
-        print(f"Testing: group_delay (unit={unit})")
-        # file names
-        filename = 'line_group_delay_unit_' + str(unit) + '.png'
-        baseline = os.path.join(baseline_path, filename)
-        output = os.path.join(output_path, filename)
+    function_list = [plot.line.time,
+                     plot.line.group_delay]
 
-        # plotting
-        matplotlib.use('Agg')
-        mpt.set_reproducibility_for_testing()
-        plt.figure(1, (f_width, f_height), f_dpi)  # force size/dpi
-        plot.line.group_delay(signal_mocks[1], unit=unit)
+    for function in function_list:
+        for unit in [None, 's', 'ms', 'mus', 'samples']:
+            print(f"Testing: {function.__name__} (unit={unit})")
+            # file names
+            filename = f'line_{function.__name__}_unit_{str(unit)}.png'
+            baseline = os.path.join(baseline_path, filename)
+            output = os.path.join(output_path, filename)
 
-        # save baseline if it does not exist
-        # make sure to visually check the baseline uppon creation
-        if create_baseline:
-            plt.savefig(baseline)
-        # safe test image
-        plt.savefig(output)
+            # plotting
+            matplotlib.use('Agg')
+            mpt.set_reproducibility_for_testing()
+            plt.figure(1, (f_width, f_height), f_dpi)  # force size/dpi
+            function(signal_mocks[1], unit=unit)
 
-        # close current figure
-        plt.close()
+            # save baseline if it does not exist
+            # make sure to visually check the baseline uppon creation
+            if create_baseline:
+                plt.savefig(baseline)
+            # safe test image
+            plt.savefig(output)
 
-        # testing
-        compare_images(baseline, output, tol=10)
+            # close current figure
+            plt.close()
+
+            # testing
+            compare_images(baseline, output, tol=10)
 
 
-def test_line_group_delay_auto_unit():
+def test_line_time_auto_unit():
     """Test automatically assigning the unit in group delay plots."""
-    assert plot._line._group_delay_auto_unit(0) == 's'
-    assert plot._line._group_delay_auto_unit(1e-4) == 'mus'
-    assert plot._line._group_delay_auto_unit(2e-2) == 'ms'
-    assert plot._line._group_delay_auto_unit(2) == 's'
+    assert plot._line._time_auto_unit(0) == 's'
+    assert plot._line._time_auto_unit(1e-4) == 'mus'
+    assert plot._line._time_auto_unit(2e-2) == 'ms'
+    assert plot._line._time_auto_unit(2) == 's'
 
 
 def test_line_custom_subplots(signal_mocks):