From 234aa550e92b73a08a8e3fcd0ce62d7ade593e88 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Thu, 9 Jul 2015 11:30:25 +0100
Subject: [PATCH] Removed 'demean' option - replaced with 'plotMode' option
 which allows demeaning, or scaling to percent-changed. Added ability to plot
 data reduced against a PE/COPE, and the ability to hide the raw data plot for
 a currently-selected FEAT time series (as the plot can get pretty messy).

---
 fsl/data/featimage.py                         |  27 ++-
 fsl/data/strings.py                           |  24 ++-
 .../controls/timeseriescontrolpanel.py        |  17 +-
 fsl/fslview/controls/timeserieslistpanel.py   |  17 +-
 fsl/fslview/views/histogrampanel.py           |   5 +-
 fsl/fslview/views/timeseriespanel.py          | 184 ++++++++++++------
 6 files changed, 180 insertions(+), 94 deletions(-)

diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py
index b5eb96ea8..4346d51c8 100644
--- a/fsl/data/featimage.py
+++ b/fsl/data/featimage.py
@@ -221,6 +221,13 @@ class FEATImage(fslimage.Image):
         
 
     def fit(self, contrast, xyz, fullmodel=False):
+        """
+
+        Passing in a contrast of all 1s, and ``fullmodel=True`` will
+        get you the full model fit. Pass in ``fullmodel=False`` for
+        all other contrasts, otherwise the model fit values will not
+        be scaled correctly.
+        """
 
         if not fullmodel:
             contrast  = np.array(contrast)
@@ -245,24 +252,14 @@ class FEATImage(fslimage.Image):
 
 
     def reducedData(self, xyz, contrast, fullmodel=False):
+        """
+
+        Passing in a contrast of all 1s, and ``fullmodel=True`` will
+        get you the model fit residuals.
+        """
 
         x, y, z   = xyz
         residuals = self.getResiduals()[x, y, z, :]
         modelfit  = self.fit(contrast, xyz, fullmodel)
 
         return residuals + modelfit
-
-    
-    # def getThresholdedZStats(self):
-    #     pass
-
-    
-    # def getSomethingForClusters(self):
-    #     pass
-
-
-    # # Return a copy of this image, transformed
-    # # to the specified spaced (e.g. MNI152,
-    # # structural, functional, etc)
-    # def getInSpace(self, space):
-    #     pass
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index d3f168057..3ef12575f 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -218,10 +218,16 @@ labels = TypeDict({
     'TimeSeriesControlPanel.currentFEATSettings' : 'FEAT settings for '
                                                    'selected overlay ({})',
 
+    'TimeSeriesListPanel.featReduced' : 'Reduced against {}',
+
     'FEATModelFitTimeSeries.full' : 'Full model fit',
     'FEATModelFitTimeSeries.cope' : 'COPE{} fit: {}',
     'FEATModelFitTimeSeries.pe'   : 'PE{} fit',
-    
+
+    'FEATReducedTimeSeries.cope' : 'Reduced against COPE{}: {}',
+    'FEATReducedTimeSeries.pe'   : 'Reduced against PE{}',
+
+    'FEATResidualTimeSeries'     : 'Residuals',
 })
 
 
@@ -265,7 +271,7 @@ properties = TypeDict({
     'PlotPanel.xlabel'    : 'X label',
     'PlotPanel.ylabel'    : 'Y label',
     
-    'TimeSeriesPanel.demean'           : 'Demean',
+    'TimeSeriesPanel.plotMode'         : 'Plotting mode',
     'TimeSeriesPanel.usePixdim'        : 'Use pixdims',
     'TimeSeriesPanel.showCurrent'      : 'Plot time series for current voxel',
     'TimeSeriesPanel.currentColour'    : 'Colour for current time course',
@@ -290,8 +296,9 @@ properties = TypeDict({
     'FEATTimeSeries.plotFullModelFit' : 'Plot full model fit',
     'FEATTimeSeries.plotPEFits'       : 'Plot PE{} fit',
     'FEATTimeSeries.plotCOPEFits'     : 'Plot COPE{} fit ({})',
-    'FEATTimeSeries.plotResiduals'    : 'Show residuals',
-    'FEATTimeSeries.reduceAgainst'    : 'Reduce data against',
+    'FEATTimeSeries.plotResiduals'    : 'Plot residuals',
+    'FEATTimeSeries.plotReduced'      : 'Plot data reduced against',
+    'FEATTimeSeries.plotData'         : 'Plot data',
 
     'OrthoEditProfile.selectionSize'          : 'Selection size',
     'OrthoEditProfile.selectionIs3D'          : '3D selection',
@@ -420,7 +427,14 @@ choices = TypeDict({
     'Display.overlayType.label'      : 'Label image',
     'Display.overlayType.rgbvector'  : '3-direction vector image (RGB)',
     'Display.overlayType.linevector' : '3-direction vector image (Line)',
-    'Display.overlayType.model'      : '3D model' 
+    'Display.overlayType.model'      : '3D model',
+
+    'HistogramPanel.histType.probability' : 'Probability',
+    'HistogramPanel.histType.count'       : 'Count',
+    
+    'TimeSeriesPanel.plotMode.normal'        : 'Normal',
+    'TimeSeriesPanel.plotMode.demean'        : 'Demeaned',
+    'TimeSeriesPanel.plotMode.percentChange' : 'Percent changed',
 })
 
 
diff --git a/fsl/fslview/controls/timeseriescontrolpanel.py b/fsl/fslview/controls/timeseriescontrolpanel.py
index f3302a830..bf4a6082c 100644
--- a/fsl/fslview/controls/timeseriescontrolpanel.py
+++ b/fsl/fslview/controls/timeseriescontrolpanel.py
@@ -27,7 +27,7 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel):
         self.SetSizer(self.__sizer)
         self.__sizer.Add(self.__widgets, flag=wx.EXPAND, proportion=1)
 
-        tsProps   = ['demean',
+        tsProps   = ['plotMode',
                      'usePixdim',
                      'showCurrent']
         plotProps = ['xLogScale',
@@ -201,8 +201,13 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel):
         res     = props.makeWidget(     self.__widgets, ts, 'plotResiduals')
         pes     = props.makeListWidgets(self.__widgets, ts, 'plotPEFits')
         copes   = props.makeListWidgets(self.__widgets, ts, 'plotCOPEFits')
-        reduce  = props.makeWidget(     self.__widgets, ts, 'reduceAgainst')
-        
+        reduced = props.makeWidget(     self.__widgets, ts, 'plotReduced')
+        data    = props.makeWidget(     self.__widgets, ts, 'plotData') 
+
+        self.__widgets.AddWidget(
+            data,
+            displayName=strings.properties[ts, 'plotData'],
+            groupName='currentFEATSettings') 
         self.__widgets.AddWidget(
             full,
             displayName=strings.properties[ts, 'plotFullModelFit'],
@@ -214,9 +219,9 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel):
             groupName='currentFEATSettings')
         
         self.__widgets.AddWidget(
-            reduce,
-            displayName=strings.properties[ts, 'reduceAgainst'],
-            groupName='currentFEATSettings') 
+            reduced,
+            displayName=strings.properties[ts, 'plotReduced'],
+            groupName='currentFEATSettings')
 
         for i, pe in enumerate(pes):
             self.__widgets.AddWidget(
diff --git a/fsl/fslview/controls/timeserieslistpanel.py b/fsl/fslview/controls/timeserieslistpanel.py
index df7b1ea55..7fbefc46d 100644
--- a/fsl/fslview/controls/timeserieslistpanel.py
+++ b/fsl/fslview/controls/timeserieslistpanel.py
@@ -102,13 +102,21 @@ class TimeSeriesListPanel(fslpanel.FSLViewPanel):
 
 
     def __makeLabel(self, ts):
+
         return '{} [{} {} {}]'.format(ts.overlay.name,
                                       ts.coords[0],
                                       ts.coords[1],
                                       ts.coords[2])
 
 
-    def __makeFEATModelFitLabel(self, parentTs, modelTs):
+    def __makeFEATModelTSLabel(self, parentTs, modelTs):
+
+        import fsl.fslview.views.timeseriespanel as tsp
+
+        if isinstance(modelTs, tsp.FEATResidualTimeSeries):
+            return '{} ({})'.format(
+                parentTs.label,
+                strings.labels[modelTs])
 
         label = '{} ({})'.format(
             parentTs.label,
@@ -156,7 +164,7 @@ class TimeSeriesListPanel(fslpanel.FSLViewPanel):
         if ts is None:
             return
 
-        ts = copy.copy(ts)
+        ts           = copy.copy(ts)
 
         ts.alpha     = 1
         ts.lineWidth = 2
@@ -167,13 +175,16 @@ class TimeSeriesListPanel(fslpanel.FSLViewPanel):
         self.__tsPanel.dataSeries.append(ts)
 
         if isinstance(ts, tsp.FEATTimeSeries):
+            
             modelTs = ts.getModelTimeSeries()
+            modelTs.remove(ts)
 
             for mts in modelTs:
+
                 mts.alpha     = 1
                 mts.lineWidth = 2
                 mts.lineStyle = '-'
-                mts.label     = self.__makeFEATModelFitLabel(ts, mts)
+                mts.label     = self.__makeFEATModelTSLabel(ts, mts)
 
             self.__tsPanel.dataSeries.extend(modelTs)
 
diff --git a/fsl/fslview/views/histogrampanel.py b/fsl/fslview/views/histogrampanel.py
index 865b4960d..cfcf7c998 100644
--- a/fsl/fslview/views/histogrampanel.py
+++ b/fsl/fslview/views/histogrampanel.py
@@ -341,7 +341,10 @@ class HistogramPanel(plotpanel.PlotPanel):
 
     autoBin     = props.Boolean(default=True)
     showCurrent = props.Boolean(default=True)
-    histType    = props.Choice(('probability', 'count'))
+    histType    = props.Choice(
+        ('probability', 'count'),
+        labels=[strings.choices['HistogramPanel.histType.probability'],
+                strings.choices['HistogramPanel.histType.count']])
 
     selectedSeries = props.Int(minval=0, clamped=True)
     
diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py
index 4ddb0a88a..aad982f89 100644
--- a/fsl/fslview/views/timeseriespanel.py
+++ b/fsl/fslview/views/timeseriespanel.py
@@ -22,6 +22,7 @@ import                               props
 import                               plotpanel
 import fsl.data.featimage         as fslfeatimage
 import fsl.data.image             as fslimage
+import fsl.data.strings           as strings
 import fsl.fslview.displaycontext as fsldisplay
 import fsl.fslview.controls       as fslcontrols
 import fsl.utils.transform        as transform
@@ -70,9 +71,13 @@ class TimeSeries(plotpanel.DataSeries):
         if self.tsPanel.usePixdim:
             xdata *= self.overlay.pixdim[3]
         
-        if self.tsPanel.demean:
+        if self.tsPanel.plotMode == 'demean':
             ydata = ydata - ydata.mean()
             
+        elif self.tsPanel.plotMode == 'percentChange':
+            mean  = ydata.mean()
+            ydata =  100 * (ydata / mean) - 100
+            
         return xdata, ydata
     
 
@@ -82,13 +87,14 @@ class FEATTimeSeries(TimeSeries):
     containing some extra FEAT specific options.
     """
 
-    
+
+    plotData         = props.Boolean(default=True)
     plotFullModelFit = props.Boolean(default=False)
     plotResiduals    = props.Boolean(default=False)
     plotPEFits       = props.List(props.Boolean(default=False))
     plotCOPEFits     = props.List(props.Boolean(default=False))
-    reduceAgainst    = props.Choice()
-
+    plotReduced      = props.Choice()
+    
 
     def __init__(self, *args, **kwargs):
         TimeSeries.__init__(self, *args, **kwargs)
@@ -105,7 +111,7 @@ class FEATTimeSeries(TimeSeries):
             name = 'COPE{} ({})'.format(i + 1, copeNames[i])
             reduceOpts.append(name)
 
-        self.getProp('reduceAgainst').setChoices(reduceOpts, instance=self)
+        self.getProp('plotReduced').setChoices(reduceOpts, instance=self)
 
         for i in range(numEVs):
             self.plotPEFits.append(False)
@@ -114,6 +120,7 @@ class FEATTimeSeries(TimeSeries):
             self.plotCOPEFits.append(False) 
 
         self.__fullModelTs =  None
+        self.__reducedTs   =  None
         self.__resTs       =  None
         self.__peTs        = [None] * numEVs
         self.__copeTs      = [None] * numCOPEs
@@ -121,10 +128,12 @@ class FEATTimeSeries(TimeSeries):
         self.addListener('plotFullModelFit',
                          self.name,
                          self.__plotFullModelFitChanged)
-
         self.addListener('plotResiduals',
                          self.name,
                          self.__plotResidualsChanged)
+        self.addListener('plotReduced',
+                         self.name,
+                         self.__plotReducedChanged)
         
         for i, plotPEFit in enumerate(
                 self.plotPEFits.getPropertyValueList()):
@@ -142,30 +151,9 @@ class FEATTimeSeries(TimeSeries):
 
             plotCOPEFit.addListener(self.name, onChange)
 
-            
-    def getData(self):
-        
-        reduce = self.reduceAgainst
-
-        if reduce == 'none':
-            data = None
-
-        else:
-            idx    = int(reduce.split()[0][-1]) - 1
-            numEVs = self.overlay.numEVs()
-
-            if reduce.startswith('PE'):
-                contrast      = [0] * numEVs
-                contrast[idx] = 1
-            else:
-                contrast      = self.overlay.contrasts()[idx]
- 
-            data = self.overlay.reducedData(self.coords, contrast, False)
-
-        return TimeSeries.getData(self, ydata=data)
-
 
     def __copy__(self):
+        
         copy = type(self)(self.tsPanel, self.overlay, self.coords)
 
         copy.colour           = self.colour
@@ -181,31 +169,77 @@ class FEATTimeSeries(TimeSeries):
         copy.plotFullModelFit = self.plotFullModelFit
         copy.plotPEFits[  :]  = self.plotPEFits[  :]
         copy.plotCOPEFits[:]  = self.plotCOPEFits[:]
+        copy.plotReduced      = self.plotReduced
+        copy.plotResiduals    = self.plotResiduals
 
         return copy
  
 
     def getModelTimeSeries(self):
+        
         modelts = []
 
-        if self.plotFullModelFit:
-            modelts.append(self.__fullModelTs)
-
-        if self.plotResiduals:
-            modelts.append(self.__resTs)
-
+        if self.plotData:              modelts.append(self)
+        if self.plotFullModelFit:      modelts.append(self.__fullModelTs)
+        if self.plotResiduals:         modelts.append(self.__resTs)
+        if self.plotReduced != 'none': modelts.append(self.__reducedTs)
+        
         for i in range(self.overlay.numEVs()):
             if self.plotPEFits[i]:
                 modelts.append(self.__peTs[i])
 
         for i in range(self.overlay.numContrasts()):
             if self.plotCOPEFits[i]:
-                modelts.append(self.__copeTs[i]) 
-        
+                modelts.append(self.__copeTs[i])
+
         return modelts
 
 
+    def __getContrast(self, fitType, idx):
+
+        if fitType == 'full':
+            return [1] * self.overlay.numEVs()
+        elif fitType == 'pe':
+            con      = [0] * self.overlay.numEVs()
+            con[idx] = 1
+            return con
+        elif fitType == 'cope':
+            return self.overlay.getContrasts()[idx]
+
+
+    def __plotReducedChanged(self, *a):
+            
+        reduced = self.plotReduced
+
+        if reduced == 'none' and self.__reducedTs is not None:
+            self.__reducedTs = None
+            return
+
+        reduced = reduced.split()[0]
+
+        # fitType is either 'cope' or 'pe'
+        fitType = reduced[:-1].lower()
+        idx     = int(reduced[-1]) - 1
+
+        rts = FEATReducedTimeSeries(
+            self.__getContrast(fitType, idx),
+            fitType,
+            idx,
+            self.tsPanel,
+            self.overlay,
+            self.coords)
+
+        rts.colour    = (0, 0.6, 0.6)
+        rts.alpha     = self.alpha
+        rts.label     = self.label
+        rts.lineWidth = self.lineWidth
+        rts.lineStyle = self.lineStyle 
+
+        self.__reducedTs = rts
+
+
     def __plotResidualsChanged(self, *a):
+        
         if not self.plotResiduals:
             self.__resTs = None
             return
@@ -225,14 +259,13 @@ class FEATTimeSeries(TimeSeries):
             
     
     def __plotCOPEFitChanged(self, copenum):
+        
         if not self.plotCOPEFits[copenum]:
             self.__copeTs[copenum] = None
             return
 
-        con  = self.overlay.contrasts()[copenum]
-
         copets = FEATModelFitTimeSeries(
-            con,
+            self.__getContrast('cope', copenum),
             'cope',
             copenum,
             self.tsPanel,
@@ -249,21 +282,19 @@ class FEATTimeSeries(TimeSeries):
 
 
     def __plotPEFitChanged(self, evnum):
+        
         if not self.plotPEFits[evnum]:
             self.__peTs[evnum] = None
             return
 
-        con        = [0] * self.overlay.numEVs()
-        con[evnum] = 1
-
         pets = FEATModelFitTimeSeries(
-            con,
+            self.__getContrast('pe', evnum),
             'pe',
             evnum, 
             self.tsPanel,
             self.overlay,
             self.coords)
-        
+
         pets.colour    = (0.7, 0, 0)
         pets.alpha     = self.alpha
         pets.label     = self.label
@@ -274,34 +305,55 @@ class FEATTimeSeries(TimeSeries):
 
 
     def __plotFullModelFitChanged(self, *a):
+        
         if not self.plotFullModelFit:
             self.__fullModelTs = None
             return
 
-        self.__fullModelTs = FEATModelFitTimeSeries(
-            [1] * self.overlay.numEVs(),
+        fts = FEATModelFitTimeSeries(
+            self.__getContrast('full', -1),
             'full',
             -1, 
             self.tsPanel,
             self.overlay,
             self.coords)
-        self.__fullModelTs.colour    = (0, 0, 1)
-        self.__fullModelTs.alpha     = self.alpha
-        self.__fullModelTs.label     = self.label
-        self.__fullModelTs.lineWidth = self.lineWidth
-        self.__fullModelTs.lineStyle = self.lineStyle
+
+        fts.colour    = (0, 0, 1)
+        fts.alpha     = self.alpha
+        fts.label     = self.label
+        fts.lineWidth = self.lineWidth
+        fts.lineStyle = self.lineStyle
+
+        self.__fullModelTs = fts
 
         
     def update(self, coords):
+        
         if not TimeSeries.update(self, coords):
             return False
             
         for modelTs in self.getModelTimeSeries():
+            if modelTs is self:
+                continue
             modelTs.update(coords)
 
         return True
 
 
+class FEATReducedTimeSeries(TimeSeries):
+    def __init__(self, contrast, fitType, idx, *args, **kwargs):
+        TimeSeries.__init__(self, *args, **kwargs)
+
+        self.contrast = contrast
+        self.fitType  = fitType
+        self.idx      = idx
+
+    def getData(self):
+        
+        data = self.overlay.reducedData(self.coords, self.contrast, False)
+        return TimeSeries.getData(self, ydata=data)
+    
+
 class FEATResidualTimeSeries(TimeSeries):
     def getData(self):
         x, y, z = self.coords
@@ -311,7 +363,6 @@ class FEATResidualTimeSeries(TimeSeries):
             
 
 class FEATModelFitTimeSeries(TimeSeries):
-    
 
     def __init__(self, contrast, fitType, idx, *args, **kwargs):
         
@@ -348,12 +399,13 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
     """
 
     
-    demean        = props.Boolean(default=True)
     usePixdim     = props.Boolean(default=False)
     showCurrent   = props.Boolean(default=True)
-
-    # TODO
-    percentChange = props.Boolean(default=False)
+    plotMode      = props.Choice(
+        ('normal', 'demean', 'percentChange'),
+        labels=[strings.choices['TimeSeriesPanel.plotMode.normal'],
+                strings.choices['TimeSeriesPanel.plotMode.demean'],
+                strings.choices['TimeSeriesPanel.plotMode.percentChange']])
 
     currentColour    = copy.copy(TimeSeries.colour)
     currentAlpha     = copy.copy(TimeSeries.alpha)
@@ -392,7 +444,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         displayCtx .addListener('selectedOverlay', self._name, self.draw) 
         displayCtx .addListener('location',        self._name, self.draw)
 
-        self.addListener('demean',      self._name, self.draw)
+        self.addListener('plotMode',    self._name, self.draw)
         self.addListener('usePixdim',   self._name, self.draw)
         self.addListener('showCurrent', self._name, self.draw)
 
@@ -413,11 +465,15 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             return
 
         tss = [self.__currentTs]
+        
         if isinstance(self.__currentTs, FEATTimeSeries):
-            tss.extend(self.__currentTs.getModelTimeSeries())
+            tss = self.__currentTs.getModelTimeSeries()
 
             for ts in tss:
 
+                if ts is self.__currentTs:
+                    continue
+
                 # Don't change the colour for associated
                 # time courses (e.g. model fits)
                 if ts is self.__currentTs:
@@ -431,7 +487,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
     def destroy(self):
         plotpanel.PlotPanel.destroy(self)
         
-        self.removeListener('demean',      self._name)
+        self.removeListener('plotMode',    self._name)
         self.removeListener('usePixdim',   self._name)
         self.removeListener('showCurrent', self._name)
         
@@ -524,12 +580,12 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
 
         if self.showCurrent and \
            current is not None:
-
-            extras = [current]
-
+            
             if isinstance(current, FEATTimeSeries):
-                extras += current.getModelTimeSeries()
-
+                extras = current.getModelTimeSeries()
+            else:
+                extras = [current]
+                
             self.drawDataSeries(extras)
         else:
             self.drawDataSeries()
-- 
GitLab