From ee993e1daa4920cca79eafdbe8b6314df7b9351d Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Fri, 26 Jun 2015 17:07:43 +0100
Subject: [PATCH] Improved log scale plotting, and added neato option to apply
 smoothing to time courses. Improved time series control panel layout too.

---
 fsl/data/strings.py                           |  8 ++-
 .../controls/timeseriescontrolpanel.py        | 30 +++++++---
 fsl/fslview/controls/timeserieslistpanel.py   |  2 +-
 fsl/fslview/views/timeseriespanel.py          | 60 ++++++++++++-------
 4 files changed, 66 insertions(+), 34 deletions(-)

diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 02057b5b3..c220f46f3 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -203,8 +203,9 @@ labels = TypeDict({
 
     'TimeSeriesControlPanel.xlim'   : 'X limits',
     'TimeSeriesControlPanel.ylim'   : 'Y limits',
-    'TimeSeriesControlPanel.xlabel' : 'X label',
-    'TimeSeriesControlPanel.ylabel' : 'Y label',
+    'TimeSeriesControlPanel.labels' : 'Labels',
+    'TimeSeriesControlPanel.xlabel' : 'X',
+    'TimeSeriesControlPanel.ylabel' : 'Y',
 })
 
 
@@ -247,6 +248,7 @@ properties = TypeDict({
     'TimeSeriesPanel.legend'    : 'Show legend',
     'TimeSeriesPanel.ticks'     : 'Show ticks',
     'TimeSeriesPanel.grid'      : 'Show grid',
+    'TimeSeriesPanel.smooth'    : 'Smooth data',
     'TimeSeriesPanel.autoScale' : 'Auto-scale',
     'TimeSeriesPanel.xLogScale' : 'Log scale (x axis)',
     'TimeSeriesPanel.yLogScale' : 'Log scale (y axis)',
@@ -355,7 +357,7 @@ choices = TypeDict({
     'ColourBarCanvas.orientation.horizontal' : 'Horizontal',
     'ColourBarCanvas.orientation.vertical'   : 'Vertical',
     
-    'ColourBarCanvas.labelSide.top-left'     : 'Top / Left',
+   'ColourBarCanvas.labelSide.top-left'     : 'Top / Left',
     'ColourBarCanvas.labelSide.bottom-right' : 'Bottom / Right', 
 
     'VolumeOpts.displayRange.min' : 'Min.',
diff --git a/fsl/fslview/controls/timeseriescontrolpanel.py b/fsl/fslview/controls/timeseriescontrolpanel.py
index 7b4bc5b59..7a853d7fa 100644
--- a/fsl/fslview/controls/timeseriescontrolpanel.py
+++ b/fsl/fslview/controls/timeseriescontrolpanel.py
@@ -25,6 +25,7 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel):
         self.__usePixdim = props.makeWidget(self, tsPanel, 'usePixdim')
         self.__logx      = props.makeWidget(self, tsPanel, 'xLogScale')
         self.__logy      = props.makeWidget(self, tsPanel, 'yLogScale')
+        self.__smooth    = props.makeWidget(self, tsPanel, 'smooth')
         self.__legend    = props.makeWidget(self, tsPanel, 'legend')
         self.__ticks     = props.makeWidget(self, tsPanel, 'ticks')
         self.__grid      = props.makeWidget(self, tsPanel, 'grid')
@@ -38,6 +39,7 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel):
         self.__ymin      = props.makeWidget(self, tsPanel, 'ymin')
         self.__ymax      = props.makeWidget(self, tsPanel, 'ymax')
 
+        self.__lblLabel  = wx.StaticText(self)
         self.__xlblLabel = wx.StaticText(self)
         self.__ylblLabel = wx.StaticText(self)
         self.__xlimLabel = wx.StaticText(self)
@@ -47,42 +49,56 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel):
         self.__usePixdim.SetLabel(strings.properties[tsPanel, 'usePixdim'])
         self.__logx     .SetLabel(strings.properties[tsPanel, 'xLogScale'])
         self.__logy     .SetLabel(strings.properties[tsPanel, 'yLogScale'])
+        self.__smooth   .SetLabel(strings.properties[tsPanel, 'smooth'])
         self.__legend   .SetLabel(strings.properties[tsPanel, 'legend'])
         self.__ticks    .SetLabel(strings.properties[tsPanel, 'ticks'])
         self.__grid     .SetLabel(strings.properties[tsPanel, 'grid'])
         self.__autoScale.SetLabel(strings.properties[tsPanel, 'autoScale'])
         self.__xlimLabel.SetLabel(strings.labels[    self,    'xlim'])
         self.__ylimLabel.SetLabel(strings.labels[    self,    'ylim'])
+        self.__lblLabel .SetLabel(strings.labels[    self,    'labels'])
         self.__xlblLabel.SetLabel(strings.labels[    self,    'xlabel'])
         self.__ylblLabel.SetLabel(strings.labels[    self,    'ylabel'])
  
-        self.__sizer = wx.GridSizer(10, 2)
+        self.__sizer = wx.GridSizer(6, 3)
         self.SetSizer(self.__sizer)
 
+        self.__xlblSizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.__xlblSizer.Add(self.__xlblLabel)
+        self.__xlblSizer.Add(self.__xlabel, flag=wx.EXPAND, proportion=1)
+        
+        self.__ylblSizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.__ylblSizer.Add(self.__ylblLabel)
+        self.__ylblSizer.Add(self.__ylabel, flag=wx.EXPAND, proportion=1) 
+
         self.__sizer.Add(self.__demean,    flag=wx.EXPAND)
         self.__sizer.Add(self.__usePixdim, flag=wx.EXPAND)
+        self.__sizer.Add(self.__smooth,    flag=wx.EXPAND)
+        
         self.__sizer.Add(self.__logx,      flag=wx.EXPAND)
         self.__sizer.Add(self.__logy,      flag=wx.EXPAND)
         self.__sizer.Add(self.__legend,    flag=wx.EXPAND)
+        
         self.__sizer.Add(self.__ticks,     flag=wx.EXPAND)
         self.__sizer.Add(self.__grid,      flag=wx.EXPAND)
         self.__sizer.Add(self.__autoScale, flag=wx.EXPAND)
-        self.__sizer.Add(self.__xlblLabel, flag=wx.EXPAND)
-        self.__sizer.Add(self.__ylblLabel, flag=wx.EXPAND)
-        self.__sizer.Add(self.__xlabel,    flag=wx.EXPAND)
-        self.__sizer.Add(self.__ylabel,    flag=wx.EXPAND)
+        
+        self.__sizer.Add(self.__lblLabel,  flag=wx.EXPAND)
+        self.__sizer.Add(self.__xlblSizer, flag=wx.EXPAND)
+        self.__sizer.Add(self.__ylblSizer, flag=wx.EXPAND)
+        
         self.__sizer.Add(self.__xlimLabel, flag=wx.EXPAND)
-        self.__sizer.Add((-1, -1),         flag=wx.EXPAND)
         self.__sizer.Add(self.__xmin,      flag=wx.EXPAND)
         self.__sizer.Add(self.__xmax,      flag=wx.EXPAND)
+        
         self.__sizer.Add(self.__ylimLabel, flag=wx.EXPAND)
-        self.__sizer.Add((-1, -1),         flag=wx.EXPAND)
         self.__sizer.Add(self.__ymin,      flag=wx.EXPAND)
         self.__sizer.Add(self.__ymax,      flag=wx.EXPAND)
 
         self.Layout()
 
         self.SetMinSize(self.__sizer.GetMinSize())
+        self.SetMaxSize(self.__sizer.GetMinSize())
 
         tsPanel.addListener('autoScale', self._name, self.__autoScaleChanged)
 
diff --git a/fsl/fslview/controls/timeserieslistpanel.py b/fsl/fslview/controls/timeserieslistpanel.py
index cea9384e4..21e6670dc 100644
--- a/fsl/fslview/controls/timeserieslistpanel.py
+++ b/fsl/fslview/controls/timeserieslistpanel.py
@@ -133,7 +133,7 @@ class TimeSeriesListPanel(fslpanel.FSLViewPanel):
             return
         
         ts.alpha     = 1
-        ts.lineWidth = 1
+        ts.lineWidth = 2
         ts.lineStyle = '-'
         ts.colour    = fslcm.randomColour()
         ts.label     = self.__makeLabel(ts)
diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py
index 4765436ca..0c7a313c5 100644
--- a/fsl/fslview/views/timeseriespanel.py
+++ b/fsl/fslview/views/timeseriespanel.py
@@ -15,6 +15,7 @@ of overlay objects stored in an :class:`.OverlayList`.
 import logging
 
 import                               wx
+import scipy.interpolate          as interp
 import numpy                      as np
 
 import                               props
@@ -66,6 +67,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
     yLogScale = props.Boolean(default=False) 
     ticks     = props.Boolean(default=True)
     grid      = props.Boolean(default=True)
+    smooth    = props.Boolean(default=False)
     xlabel    = props.String()
     ylabel    = props.String()
     xmin      = props.Real()
@@ -239,21 +241,9 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             xlims.append(xlim)
             ylims.append(ylim)
 
-        if self.xLogScale: axis.set_xscale('log')
-        else:              axis.set_xscale('linear')
-        if self.yLogScale: axis.set_yscale('log')
-        else:              axis.set_yscale('linear')
-
-        xlim, ylim = self.__calcLimits(xlims, ylims)
-
-        bPad = (ylim[1] - ylim[0]) * (50.0 / height)
-        tPad = (ylim[1] - ylim[0]) * (20.0 / height)
-        lPad = (xlim[1] - xlim[0]) * (50.0 / width)
-        rPad = (xlim[1] - xlim[0]) * (20.0 / width)
-
-        axis.set_xlim((xlim[0] - lPad, xlim[1] + rPad))
-        axis.set_ylim((ylim[0] - bPad, ylim[1] + tPad))
+        (xmin, xmax), (ymin, ymax) = self.__calcLimits(xlims, ylims)
 
+        # x/y axis labels
         xlabel = self.xlabel 
         ylabel = self.ylabel
 
@@ -271,9 +261,10 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             axis.set_ylabel(self.ylabel, va='top')
             axis.yaxis.set_label_coords(10.0 / width, 0.5)
 
+        # Ticks
         if self.ticks:
-            xticks = np.linspace(xlim[0], xlim[1], 4)
-            yticks = np.linspace(ylim[0], ylim[1], 4)
+            xticks = np.linspace(xmin, xmax, 4)
+            yticks = np.linspace(ymin, ymax, 4)
 
             axis.tick_params(direction='in', pad=-5)
 
@@ -289,6 +280,15 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             axis.set_xticks([])
             axis.set_yticks([])
 
+        # Limits
+        bPad = (ymax - ymin) * (50.0 / height)
+        tPad = (ymax - ymin) * (20.0 / height)
+        lPad = (xmax - xmin) * (50.0 / width)
+        rPad = (xmax - xmin) * (20.0 / width)
+        
+        axis.set_xlim((xmin - lPad, xmax + rPad))
+        axis.set_ylim((ymin - bPad, ymax + tPad))
+
         # legend - don't show if we're only
         # plotting the current location
         if len(self.timeSeries) > 0 and self.legend:
@@ -300,7 +300,6 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
                 fancybox=True)
             legend.get_frame().set_alpha(0.3)
 
-
         if self.grid:
             axis.grid()
 
@@ -312,14 +311,29 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
 
         if ts.alpha == 0:
             return (0, 0), (0, 0)
-        
-        ydata = ts.data
 
+        ydata   = np.array(ts.data, dtype=np.float32)
+        npoints = len(ydata)
+        
         if self.demean:
             ydata = ydata - ydata.mean()
 
-        if self.usePixdim: xdata = np.arange(len(ydata)) * ts.overlay.pixdim[3]
-        else:              xdata = np.arange(len(ydata))
+        if self.smooth:
+            tck   = interp.splrep(np.arange(npoints), ydata)
+            ydata = interp.splev(np.linspace(0, npoints - 1, 5 * npoints), tck)
+
+        xdata = np.linspace(0, npoints - 1, len(ydata), dtype=np.float32)
+
+        if self.usePixdim:
+            xdata *= ts.overlay.pixdim[3]
+
+        if self.xLogScale: xdata = np.log10(xdata)
+        if self.yLogScale: ydata = np.log10(ydata)
+
+        nans = ~(np.isfinite(xdata) & np.isfinite(ydata))
+
+        xdata[nans] = np.nan
+        ydata[nans] = np.nan
 
         kwargs = {}
         kwargs['lw']    = ts.lineWidth
@@ -330,5 +344,5 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         
         self.getAxis().plot(xdata, ydata, **kwargs)
 
-        return ((xdata.min(), xdata.max()), 
-                (ydata.min(), ydata.max()))
+        return ((np.nanmin(xdata), np.nanmax(xdata)),
+                (np.nanmin(ydata), np.nanmax(ydata)))
-- 
GitLab