From 4507aaa78509da3b198e1f80c2c19d03c56d478f Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Mon, 2 Nov 2015 13:19:15 +0000
Subject: [PATCH] TimeSeriesPanel and PowerSpectrogramPanel now share a common
 base class, the OverlayPlotPanel, which derives from PlotPanel, and contains
 all that DataSeries caching logic. HistogramPanel will follow.

---
 fsl/fsleyes/views/plotpanel.py          | 183 ++++++++++++++---
 fsl/fsleyes/views/powerspectrumpanel.py | 145 +++----------
 fsl/fsleyes/views/timeseriespanel.py    | 262 ++++++------------------
 3 files changed, 242 insertions(+), 348 deletions(-)

diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py
index 6e04dd227..3ce9df49d 100644
--- a/fsl/fsleyes/views/plotpanel.py
+++ b/fsl/fsleyes/views/plotpanel.py
@@ -58,10 +58,7 @@ class PlotPanel(viewpanel.ViewPanel):
       3. Override the :meth:`draw` method, so it calls the
          :meth:`drawDataSeries` method.
 
-      4. Override the :meth:`getDataSeries` method, so it returns a
-         :class:`.DataSeries` instance associated with a given overlay.
-
-      5. If necessary, override the :meth:`destroy` method, but make
+      4. If necessary, override the :meth:`destroy` method, but make
          sure that the base-class implementation is called.
 
     
@@ -202,13 +199,17 @@ class PlotPanel(viewpanel.ViewPanel):
 
         figure = plt.Figure()
         axis   = figure.add_subplot(111)
-        canvas = Canvas(self, -1, figure) 
+        canvas = Canvas(self, -1, figure)
 
+        figure.subplots_adjust(top=1.0, bottom=0.0, left=0.0, right=1.0)
+        figure.patch.set_visible(False)
+        
         self.setCentrePanel(canvas)
 
         self.__figure = figure
         self.__axis   = axis
         self.__canvas = canvas
+        self.__name   = 'OverlayPlotPanel_{}'.format(self._name)
 
         if interactive:
             
@@ -237,10 +238,9 @@ class PlotPanel(viewpanel.ViewPanel):
                          'smooth',
                          'xlabel',
                          'ylabel']:
-            self.addListener(propName, self._name, self.draw)
+            self.addListener(propName, self.__name, self.draw)
             
         # custom listeners for a couple of properties
-        self.__name = '{}_{}'.format(self._name, id(self))        
         self.addListener('dataSeries',
                          self.__name,
                          self.__dataSeriesChanged)
@@ -248,7 +248,7 @@ class PlotPanel(viewpanel.ViewPanel):
                          self.__name,
                          self.__limitsChanged)
 
-        self.Bind(wx.EVT_SIZE, lambda *a: self.draw())
+        self.Bind(wx.EVT_SIZE, self.draw)
 
 
     def getFigure(self):
@@ -278,20 +278,6 @@ class PlotPanel(viewpanel.ViewPanel):
         raise NotImplementedError('The draw method must be '
                                   'implemented by PlotPanel subclasses')
 
-
-    def getDataSeries(self, overlay):
-        """This method must be overridden by ``PlotPanel`` sub-classes.
-
-        It may be called by the :class:`.PlotControlPanel` and
-        :class:`.PlotListPanel` to display controls allowing the user
-        to change :class:`.DataSeries` display properties.
-
-        It should return the :class:`.DataSeries` instance associated with
-        the given overlay, or ``None`` if there is no ``DataSeries`` instance.
-        """
-        raise NotImplementedError('The getDataSeries method must be '
-                                  'implemented by PlotPanel subclasses')
-
         
     def destroy(self):
         """Removes some property listeners, and then calls
@@ -310,7 +296,7 @@ class PlotPanel(viewpanel.ViewPanel):
                          'smooth',
                          'xlabel',
                          'ylabel']:
-            self.removeListener(propName, self._name)
+            self.removeListener(propName, self.__name)
         viewpanel.ViewPanel.destroy(self)
 
     
@@ -696,3 +682,154 @@ class PlotPanel(viewpanel.ViewPanel):
         self.enableListener('limits', self.__name)            
  
         return (xmin, xmax), (ymin, ymax)
+
+
+class OverlayPlotPanel(PlotPanel):
+
+    
+    showMode = props.Choice(('current', 'all', 'none'))
+    """Defines which data series to plot.
+
+    =========== =====================================================
+    ``current`` The time course for the currently selected overlay is
+                plotted.
+    ``all``     The time courses for all compatible overlays in the
+                :class:`.OverlayList` are plotted.
+    ``none``    Only the ``TimeSeries`` that are in the
+                :attr:`.PlotPanel.dataSeries` list will be plotted.
+    =========== =====================================================
+    """ 
+
+    def __init__(self, *args, **kwargs):
+
+        PlotPanel.__init__(self, *args, **kwargs)
+        
+        self.__name = 'OverlayPlotPanel_{}'.format(self._name)
+
+        # The dataSeries attribute is a dictionary of
+        #
+        #   {overlay : DataSeries}
+        #
+        # mappings, containing a DataSeries instance for
+        # each compatible overlay in the overlay list.
+        # 
+        # Different DataSeries types need to be re-drawn
+        # when different properties change. For example,
+        # a TimeSeries instance needs to be redrawn when
+        # the DisplayContext.location property changes,
+        # whereas a MelodicTimeSeries instance needs to
+        # be redrawn when the VolumeOpts.volume property
+        # changes.
+        #
+        # Therefore, the refreshProps dictionary contains
+        # a set of
+        #
+        #   {overlay : ([targets], [propNames])}
+        #
+        # mappings - for each overlay, a list of
+        # target objects (e.g. DisplayContext, VolumeOpts,
+        # etc), and a list of property names on each,
+        # defining the properties that need to trigger a
+        # redraw.
+        self.__dataSeries   = {}
+        self.__refreshProps = {}
+
+        self             .addListener('showMode',
+                                      self.__name,
+                                      self.draw)
+        self._displayCtx .addListener('selectedOverlay',
+                                      self.__name,
+                                      self.draw)
+        self._overlayList.addListener('overlays',
+                                      self.__name,
+                                      self.__overlayListChanged)
+        
+        self.updateDataSeries()
+
+
+    def destroy(self):
+        self             .removeListener('showMode',        self.__name)
+        self._overlayList.removeListener('overlays',        self.__name)
+        self._displayCtx .removeListener('selectedOverlay', self.__name)
+        
+
+    def getDataSeries(self, overlay):
+        """
+
+        It may be called by the :class:`.PlotControlPanel` and
+        :class:`.PlotListPanel` to display controls allowing the user
+        to change :class:`.DataSeries` display properties.
+
+        It should return the :class:`.DataSeries` instance associated with
+        the given overlay, or ``None`` if there is no ``DataSeries`` instance.
+        """
+        return self.__dataSeries.get(overlay)
+ 
+
+    def createDataSeries(self, overlay):
+        """
+        """
+        raise NotImplementedError('createDataSeries must be '
+                                  'implemented by sub-classes')
+
+    
+    def clearDataSeries(self, overlay):
+        """Destroys the internally cached :class:`.DataSeries` for the given
+        overlay.
+        """
+        
+        ts                 = self.__dataSeries  .pop(overlay, None)
+        targets, propNames = self.__refreshProps.pop(overlay, ([], []))
+
+        if ts is not None:
+            ts.destroy()
+
+        for t, p in zip(targets, propNames):
+            t.removeListener(p, self.__name)
+
+        
+    def updateDataSeries(self):
+        
+        for ovl in self._overlayList:
+            if ovl not in self.__dataSeries:
+                
+                ds, refreshTargets, refreshProps = self.createDataSeries(ovl)
+
+                if ds is None:
+                    continue
+
+                self.__dataSeries[  ovl] = ds
+                self.__refreshProps[ovl] = (refreshTargets, refreshProps)
+                
+                ds.addGlobalListener(self.__name, self.draw, overwrite=True)
+        
+        for targets, propNames in self.__refreshProps.values():
+            for target, propName in zip(targets, propNames):
+                target.addListener(propName,
+                                   self.__name,
+                                   self.draw,
+                                   overwrite=True) 
+
+
+    def __overlayListChanged(self, *a):
+        """Called when the :class:`.OverlayList` changes. Makes sure that
+        there are no :class:`.TimeSeries` instances in the
+        :attr:`.PlotPanel.dataSeries` list, or in the internal cache, which
+        refer to overlays that no longer exist.
+
+        Also calls :meth:`__updateCurrentTimeSeries`, whic ensures that a
+        :class:`.TimeSeries` instance for every compatiblew overlay is
+        cached internally.
+        """
+
+        for ds in list(self.dataSeries):
+            if ds.overlay not in self._overlayList:
+                self.dataSeries.remove(ds)
+                ds.destroy()
+        
+        for overlay in list(self.__dataSeries.keys()):
+            if overlay not in self._overlayList:
+                self.clearDataSeries(overlay)
+
+        self.updateDataSeries()
+        self.draw()
diff --git a/fsl/fsleyes/views/powerspectrumpanel.py b/fsl/fsleyes/views/powerspectrumpanel.py
index 1022394f5..becef85ec 100644
--- a/fsl/fsleyes/views/powerspectrumpanel.py
+++ b/fsl/fsleyes/views/powerspectrumpanel.py
@@ -28,10 +28,10 @@ import fsl.data.melodicimage                          as fslmelimage
 log = logging.getLogger(__name__)
 
 
-class PowerSpectrumPanel(plotpanel.PlotPanel):
-    """The ``PowerSpectrumPanel`` class is a :class:`.PlotPanel` which plots
-    power spectra of overlay data. The ``PowerSpectrumPanel`` shares much of
-    its design with the :class:`.TimeSeriesPanel`.
+class PowerSpectrumPanel(plotpanel.OverlayPlotPanel):
+    """The ``PowerSpectrumPanel`` class is an :class:`.OverlayPlotPanel` which
+    plots power spectra of overlay data. The ``PowerSpectrumPanel`` shares
+    much of its design with the :class:`.TimeSeriesPanel`.
 
 
     The ``PowerSpectrumPanel`` uses :class:`.PowerSpectrumSeries` to plot
@@ -51,20 +51,6 @@ class PowerSpectrumPanel(plotpanel.PlotPanel):
     """If ``True``, the x axis is scaled so that it represents frequency.
     """
 
-
-    showMode = props.Choice(('current', 'all', 'none'))
-    """Defines which power spectra to plot.
-
-    =========== ========================================================
-    ``current`` The power spectrum for the currently selected overlay is
-                plotted.
-    ``all``     The power spectra for all compatible overlays in the
-                :class:`.OverlayList` are plotted.
-    ``none``    Only the ``PowerSpectrumSeries`` that are in the
-                :attr:`.PlotPanel.dataSeries` list will be plotted.
-    =========== ========================================================
-    """
-
     
     def __init__(self, parent, overlayList, displayCtx):
         """
@@ -77,53 +63,27 @@ class PowerSpectrumPanel(plotpanel.PlotPanel):
                 location=wx.TOP)
         }
         
-        plotpanel.PlotPanel.__init__(self,
-                                     parent,
-                                     overlayList,
-                                     displayCtx,
-                                     actionz=actionz)
-
-        figure = self.getFigure()
-
-        figure.subplots_adjust(
-            top=1.0, bottom=0.0, left=0.0, right=1.0)
-
-        figure.patch.set_visible(False) 
-
-        # A dictionary of
-        # 
-        #   {overlay : PowerSpectrumSeries}
-        #
-        # instances, one for each (compatible)
-        # overlay in the overlay list
-        self.__spectra      = {}
-        self.__refreshProps = {}
-
-        self       .addListener('plotFrequencies', self._name, self.draw)
-        self       .addListener('showMode',        self._name, self.draw)
-        self       .addListener('plotMelodicICs',
+        plotpanel.OverlayPlotPanel.__init__(self,
+                                            parent,
+                                            overlayList,
+                                            displayCtx,
+                                            actionz=actionz)
+
+
+        self.addListener('plotFrequencies', self._name, self.draw)
+        self.addListener('plotMelodicICs',
                                 self._name,
                                 self.__plotMelodicICsChanged)
-        overlayList.addListener('overlays',
-                                self._name,
-                                self.__overlayListChanged)
-        
-        displayCtx .addListener('selectedOverlay',
-                                self._name,
-                                self.__selectedOverlayChanged)
 
-        self.__overlayListChanged()
+        self.draw()
 
 
     def destroy(self):
-        self._overlayList.removeListener('overlays',        self._name)
-        self._displayCtx .removeListener('selectedOverlay', self._name)
-
-        plotpanel.PlotPanel.destroy(self)
-
+        
+        self.removeListener('plotFrequencies', self._name)
+        self.removeListener('plotMelodicICs',  self._name)
+        plotpanel.OverlayPlotPanel.destroy(self)
 
-    def getDataSeries(self, overlay):
-        return self.__spectra.get(overlay)
         
 
     def draw(self, *a):
@@ -135,32 +95,12 @@ class PowerSpectrumPanel(plotpanel.PlotPanel):
         else:
             overlays = []
 
-        pss = [self.__spectra.get(o) for o in overlays]
+        pss = [self.getDataSeries(o) for o in overlays]
         pss = [ps for ps in pss if ps is not None]
 
         self.drawDataSeries(extraSeries=pss,
                             preproc=self.__prepareSpectrumData)
 
-        
-    def __overlayListChanged(self, *a):
-
-        # Destroy any spectrum series for overlays
-        # that have been removed from the list
-        for ds in list(self.dataSeries):
-            if ds.overlay not in self._overlayList:
-                self.dataSeries.remove(ds)
-                ds.destroy()
-
-        for overlay, ds in list(self.__spectra.items()):
-            if overlay not in self._overlayList:
-                self.__clearCacheForOverlay(overlay)
-
-        self.__updateCachedSpectra()
-        self.draw()
-
-        
-    def __selectedOverlayChanged(self, *a):
-        self.draw()
 
         
     def __plotMelodicICsChanged(self, *a):
@@ -168,55 +108,16 @@ class PowerSpectrumPanel(plotpanel.PlotPanel):
         the internally cached :class:`.TimeSeries` instances for all
         :class:`.MelodicImage` overlays in the :class:`.OverlayList`.
         """
-
+        
         for overlay in self._overlayList:
             if isinstance(overlay, fslmelimage.MelodicImage):
-                self.__clearCacheForOverlay(overlay)
+                self.clearDataSeries(overlay)
 
-        self.__updateCachedSpectra()
+        self.updateDataSeries()
         self.draw()
 
 
-    def __clearCacheForOverlay(self, overlay):
-        """Destroys the internally cached :class:`.TimeSeries` for the given
-        overlay.
-        """
-        
-        ts                 = self.__spectra     .pop(overlay, None)
-        targets, propNames = self.__refreshProps.pop(overlay, ([], []))
-
-        if ts is not None:
-            ts.destroy()
-
-        for t, p in zip(targets, propNames):
-            t.removeListener(p, self._name)
-
-
-    def __updateCachedSpectra(self):
-        # Create a new spectrum series for overlays
-        # which have been added to the list
-        for overlay in self._overlayList:
-            
-            ss = self.__spectra.get(overlay)
-            
-            if ss is None:
-
-                ss, targets, propNames = self.__createSpectrumSeries(overlay)
-
-                if ss is None:
-                    continue
-
-                self.__spectra[     overlay] = ss
-                self.__refreshProps[overlay] = targets, propNames
-
-            ss.addGlobalListener(self._name, self.draw, overwrite=True)
-
-        for targets, propNames in self.__refreshProps.values():
-            for t, p in zip(targets, propNames):
-                t.addListener(p, self._name, self.draw, overwrite=True)
-                
-
-    def __createSpectrumSeries(self, overlay):
+    def createDataSeries(self, overlay):
 
         if self.plotMelodicICs and \
            isinstance(overlay, fslmelimage.MelodicImage):
diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py
index 8e7767246..3fcc26336 100644
--- a/fsl/fsleyes/views/timeseriespanel.py
+++ b/fsl/fsleyes/views/timeseriespanel.py
@@ -28,7 +28,7 @@ import fsl.fsleyes.controls.timeserieslistpanel    as timeserieslistpanel
 log = logging.getLogger(__name__)
 
 
-class TimeSeriesPanel(plotpanel.PlotPanel):
+class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
     """The ``TimeSeriesPanel`` is a :class:`.PlotPanel` which plots time series
     data from :class:`.Image` overlays. A ``TimeSeriesPanel`` looks something
     like the following:
@@ -124,20 +124,6 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
     to the TR time).
     """
 
-
-    showMode = props.Choice(('current', 'all', 'none'))
-    """Defines which time series to plot.
-
-    =========== =====================================================
-    ``current`` The time course for the currently selected overlay is
-                plotted.
-    ``all``     The time courses for all compatible overlays in the
-                :class:`.OverlayList` are plotted.
-    ``none``    Only the ``TimeSeries`` that are in the
-                :attr:`.PlotPanel.dataSeries` list will be plotted.
-    =========== =====================================================
-    """
-
     
     plotMode = props.Choice(('normal', 'demean', 'normalise', 'percentChange'))
     """Options to scale/offset the plotted time courses.
@@ -178,87 +164,33 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
                 location=wx.TOP) 
         }
 
-        plotpanel.PlotPanel.__init__(
+        plotpanel.OverlayPlotPanel.__init__(
             self, parent, overlayList, displayCtx, actionz=actionz)
 
-        figure = self.getFigure()
-
-        figure.subplots_adjust(
-            top=1.0, bottom=0.0, left=0.0, right=1.0)
-
-        figure.patch.set_visible(False)
-
-        self       .addListener('plotMode',        self._name, self.draw)
-        self       .addListener('usePixdim',       self._name, self.draw)
-        self       .addListener('showMode',        self._name, self.draw)
-        displayCtx .addListener('selectedOverlay', self._name, self.draw)
-        self       .addListener('plotMelodicICs',
-                                self._name,
-                                self.__plotMelodicICsChanged)
-        overlayList.addListener('overlays',
-                                self._name,
-                                self.__overlayListChanged) 
-
-        # The currentTss attribute is a dictionary of
-        #
-        #   {overlay : TimeSeries}
-        #
-        # mappings, containing a TimeSeries instance for
-        # each compatible overlay in the overlay list.
-        # 
-        # Different TimeSeries types need to be re-drawn
-        # when different properties change. For example,
-        # a TimeSeries instance needs to be redrawn when
-        # the DisplayContext.location property changes,
-        # whereas a MelodicTimeSeries instance needs to
-        # be redrawn when the VolumeOpts.volume property
-        # changes.
-        #
-        # Therefore, the refreshProps dictionary contains
-        # a set of
-        #
-        #   {overlay : ([targets], [propNames])}
-        #
-        # mappings - for each overlay, a list of
-        # target objects (e.g. DisplayContext, VolumeOpts,
-        # etc), and a list of property names on each,
-        # defining the properties that need to trigger a
-        # redraw.
-        self.__currentTss   = {}
-        self.__refreshProps = {} 
+        self.addListener('plotMode',  self._name, self.draw)
+        self.addListener('usePixdim', self._name, self.draw)
+        self.addListener('plotMelodicICs',
+                         self._name,
+                         self.__plotMelodicICsChanged)
 
         def addPanels():
             self.run('toggleTimeSeriesControl') 
             self.run('toggleTimeSeriesList') 
 
         wx.CallAfter(addPanels)
-
-        self.__overlayListChanged()
-
+        self.draw()
+        
 
     def destroy(self):
         """Removes some listeners, and calls the :meth:`.PlotPanel.destroy`
         method.
         """
         
-        self.removeListener('plotMode',  self._name)
-        self.removeListener('usePixdim', self._name)
-        self.removeListener('showMode',  self._name)
+        self.removeListener('plotMode',       self._name)
+        self.removeListener('usePixdim',      self._name)
+        self.removeListener('plotMelodicICs', self._name)
         
-        self._overlayList.removeListener('overlays',        self._name)
-        self._displayCtx .removeListener('selectedOverlay', self._name)
-
-        for (targets, propNames) in self.__refreshProps.values():
-            for target, propName in zip(targets, propNames):
-                target.removeListener(propName, self._name)
-
-        for ts in self.__currentTss.values():
-            ts.removeGlobalListener(self)
-
-        self.__currentTss   = None
-        self.__refreshProps = None
-
-        plotpanel.PlotPanel.destroy(self) 
+        plotpanel.OverlayPlotPanel.destroy(self) 
 
 
     def draw(self, *a):
@@ -275,7 +207,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         else:
             overlays = []
 
-        tss = [self.__currentTss.get(o) for o in overlays]
+        tss = [self.getDataSeries(o) for o in overlays]
         tss = [ts for ts in tss if ts is not None]
 
         for i, ts in enumerate(list(tss)):
@@ -290,12 +222,50 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
                             preproc=self.__prepareTimeSeriesData)
 
 
-    def getDataSeries(self, overlay):
-        """Overrides :meth:`.PlotPanel.getDataSeries`. Returns the
-        :class:`.TimeSeries` instance for the specified overlay, or ``None``
-        if there is none.
+    def createDataSeries(self, overlay):
+        """Creates and returns a :class:`.TimeSeries` instance (or an
+        instance of one of the :class:`.TimeSeries` sub-classes) for the
+        specified overlay.
+
+        Returns a tuple containing the following:
+        
+          - A :class:`.TimeSeries` instance for the given overlay
+        
+          - A list of *targets* - objects which have properties that
+            influence the state of the ``TimeSeries`` instance.
+        
+          - A list of *property names*, one for each target.
+
+        If the given overlay is not compatible (i.e. it has no time series
+        data to be plotted), a tuple of ``None`` values is returned.
         """
-        return self.__currentTss.get(overlay)
+
+        if not (isinstance(overlay, fslimage.Image) and overlay.is4DImage()):
+            return None, None, None
+
+        if isinstance(overlay, fslfeatimage.FEATImage):
+            ts = plotting.FEATTimeSeries(self, overlay, self._displayCtx)
+            targets   = [self._displayCtx]
+            propNames = ['location']
+            
+        elif isinstance(overlay, fslmelimage.MelodicImage) and \
+             self.plotMelodicICs:
+            ts = plotting.MelodicTimeSeries(self, overlay, self._displayCtx)
+            targets   = [self._displayCtx.getOpts(overlay)]
+            propNames = ['volume'] 
+            
+        else:
+            ts = plotting.VoxelTimeSeries(self, overlay, self._displayCtx)
+            targets   = [self._displayCtx]
+            propNames = ['location'] 
+
+        ts.colour    = fslcmaps.randomDarkColour()
+        ts.alpha     = 1
+        ts.lineWidth = 1
+        ts.lineStyle = '-'
+        ts.label     = ts.makeLabel()
+                
+        return ts, targets, propNames
 
 
     def __prepareTimeSeriesData(self, ts):
@@ -331,45 +301,6 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         return xdata, ydata 
 
 
-    def __overlayListChanged(self, *a):
-        """Called when the :class:`.OverlayList` changes. Makes sure that
-        there are no :class:`.TimeSeries` instances in the
-        :attr:`.PlotPanel.dataSeries` list, or in the internal cache, which
-        refer to overlays that no longer exist.
-
-        Also calls :meth:`__updateCurrentTimeSeries`, whic ensures that a
-        :class:`.TimeSeries` instance for every compatiblew overlay is
-        cached internally.
-        """
-
-        for ds in list(self.dataSeries):
-            if ds.overlay not in self._overlayList:
-                self.dataSeries.remove(ds)
-                ds.destroy()
-        
-        for overlay in list(self.__currentTss.keys()):
-            if overlay not in self._overlayList:
-                self.__clearCacheForOverlay(overlay)
-
-        self.__updateCurrentTimeSeries()
-        self.draw()
-
-
-    def __clearCacheForOverlay(self, overlay):
-        """Destroys the internally cached :class:`.TimeSeries` for the given
-        overlay.
-        """
-        
-        ts                 = self.__currentTss  .pop(overlay, None)
-        targets, propNames = self.__refreshProps.pop(overlay, ([], []))
-
-        if ts is not None:
-            ts.destroy()
-
-        for t, p in zip(targets, propNames):
-            t.removeListener(p, self._name)
-
-        
     def __plotMelodicICsChanged(self, *a):
         """Called when the :attr:`plotMelodicICs` property changes. Re-creates
         the internally cached :class:`.TimeSeries` instances for all
@@ -378,82 +309,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
 
         for overlay in self._overlayList:
             if isinstance(overlay, fslmelimage.MelodicImage):
-                self.__clearCacheForOverlay(overlay)
+                self.clearDataSeries(overlay)
 
-        self.__updateCurrentTimeSeries()
+        self.updateDataSeries()
         self.draw()
-
-        
-    def __updateCurrentTimeSeries(self, *a):
-        """Makes sure that a :class:`.TimeSeries` instance exists for every
-        compatible overlay in the :class:`.OverlayList`, and that
-        relevant property listeners are registered so they are redrawn as
-        needed.
-        """
-
-        for ovl in self._overlayList:
-            if ovl not in self.__currentTss:
-                
-                ts, refreshTargets, refreshProps = self.__genOneTimeSeries(ovl)
-
-                if ts is None:
-                    continue
-
-                self.__currentTss[  ovl] = ts
-                self.__refreshProps[ovl] = (refreshTargets, refreshProps)
-                
-                ts.addGlobalListener(self._name, self.draw, overwrite=True)
-        
-        for targets, propNames in self.__refreshProps.values():
-            for target, propName in zip(targets, propNames):
-                target.addListener(propName,
-                                   self._name,
-                                   self.draw,
-                                   overwrite=True)
-
-
-    
-    def __genOneTimeSeries(self, overlay):
-        """Creates and returns a :class:`.TimeSeries` instance (or an
-        instance of one of the :class:`.TimeSeries` sub-classes) for the
-        specified overlay.
-
-        Returns a tuple containing the following:
-        
-          - A :class:`.TimeSeries` instance for the given overlay
-        
-          - A list of *targets* - objects which have properties that
-            influence the state of the ``TimeSeries`` instance.
-        
-          - A list of *property names*, one for each target.
-
-        If the given overlay is not compatible (i.e. it has no time series
-        data to be plotted), a tuple of ``None`` values is returned.
-        """
-
-        if not (isinstance(overlay, fslimage.Image) and overlay.is4DImage()):
-            return None, None, None
-
-        if isinstance(overlay, fslfeatimage.FEATImage):
-            ts = plotting.FEATTimeSeries(self, overlay, self._displayCtx)
-            targets   = [self._displayCtx]
-            propNames = ['location']
-            
-        elif isinstance(overlay, fslmelimage.MelodicImage) and \
-             self.plotMelodicICs:
-            ts = plotting.MelodicTimeSeries(self, overlay, self._displayCtx)
-            targets   = [self._displayCtx.getOpts(overlay)]
-            propNames = ['volume'] 
-            
-        else:
-            ts = plotting.VoxelTimeSeries(self, overlay, self._displayCtx)
-            targets   = [self._displayCtx]
-            propNames = ['location'] 
-
-        ts.colour    = fslcmaps.randomDarkColour()
-        ts.alpha     = 1
-        ts.lineWidth = 1
-        ts.lineStyle = '-'
-        ts.label     = ts.makeLabel()
-                
-        return ts, targets, propNames
-- 
GitLab