From e106f94166c90583d61855b3513a239b28f2b131 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 9 Sep 2015 17:28:48 +0100
Subject: [PATCH] Half documented HistogramPanel. More work to be done.

---
 fsl/fsleyes/views/histogrampanel.py | 587 ++++++++++++++++------------
 1 file changed, 340 insertions(+), 247 deletions(-)

diff --git a/fsl/fsleyes/views/histogrampanel.py b/fsl/fsleyes/views/histogrampanel.py
index 29436a43c..4e41a4b96 100644
--- a/fsl/fsleyes/views/histogrampanel.py
+++ b/fsl/fsleyes/views/histogrampanel.py
@@ -1,10 +1,22 @@
 #!/usr/bin/env python
 #
-# histogrampanel.py - A panel which plots a histogram for the data from the
-#                     currently selected overlay.
+# histogrampanel.py - The HistogramPanel class.
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
+"""This module provides the :class:`HistogramPanel`, which is a *FSLeyes view*
+that plots the histogram of data from :class:`.Image` overlays.  A
+``HistogramPanel`` looks something like this:
+
+.. image:: images/histogrampanel.png
+   :scale: 50%
+   :align: center
+
+
+``HistogramPanel`` instances use the :class:`HistogramSeries` class (a
+:class:`.DataSeries` sub-class) to encapsulate histogram data.
+"""
+
 
 import logging
 
@@ -23,32 +35,312 @@ import                           plotpanel
 
 log = logging.getLogger(__name__)
 
-        
-def autoBin(data, dataRange):
 
-    # Automatic histogram bin calculation
-    # as implemented in the original FSLView
+class HistogramPanel(plotpanel.PlotPanel):
+    """A :class:`.PlotPanel` which plots histograms from :class:`.Image`
+    overlay data.
+
+    A ``HistogramPanel`` plots one or more :class:`HistogramSeries` instances,
+    each of which encapsulate histogram data from an :class:`.Image` overlay.
+
+    By default, a ``HistogramPanel`` plots a histogram from the currently
+    selected overlay (dictated by the :attr:`.DisplayContext.selectedOverlay`
+    property), if it is an :class:`.Image` instance.  In a similar manner to
+    the :class:`.TimeSeriesPanel`, this histogram is referred to as the
+    *current* histogram, and it can be enabled/disabled with the
+    :attr:`showCurrent` setting.
 
-    dMin, dMax = dataRange
-    dRange     = dMax - dMin
 
-    binSize = np.power(10, np.ceil(np.log10(dRange) - 1) - 1)
+    A couple of control panels may be shown on a ``HistogramPanel``::
+
+    .. autosummary::
+       :nosignatures:
+
+       ~fsl.fsleyes.controls.histogramlistpanel.HistogramListPanel
+       ~fsl.fsleyes.controls.histogramcontrolpanel.HistogramControlPanel
+
+    The following actions are provided, in addition to those already provided
+    by the :class:`.PlotPanel:
+
+    ========================== ===========================================
+    ``toggleHistogramList``    Show/hide a :cass:`.HistogramListPanel`.
+    ``toggleHistogramControl`` Show/hide a :cass:`.HistogramControlPanel`.
+    ========================== ===========================================
+    """
+
+
+    autoBin = props.Boolean(default=True)
+    """If ``True``, the number of bins used for each :class:`HistogramSeries`
+    is calculated automatically. Otherwise, :attr:`HistogramSeries.nbins` bins
+    are used.
+    """
 
-    nbins = dRange / binSize
     
-    while nbins < 100:
-        binSize /= 2
-        nbins    = dRange / binSize
+    showCurrent = props.Boolean(default=True)
+    """If ``True``, a histogram for the currently selected overlay (if it is
+    an :class:`.Image` instance) is always plotted.
+    """
+
+    
+    histType = props.Choice(('probability', 'count'))
+    """The histogram type:
+
+    =============== ==========================================================
+    ``count``       The y axis represents the absolute number of values within
+                    each bin 
+    ``probability`` The y axis represents the nuymber of values within each
+                    bin, divided by the total number of values.
+    =============== ==========================================================
+    """
+
+    
+    selectedSeries = props.Int(minval=0, clamped=True)
+    """The currently selected :class:`HistogramSeries` - an index into the
+    :attr:`.PlotPanel.dataSeries` list.
+
+    This property is used by the :class:`.HistogramListPanel` and the
+    :class:`.HistogramControlPanel`, to allow the user to change the settings
+    of individual :class:`HistogramSeries` instances. 
+    """
+    
+
+    def __init__(self, parent, overlayList, displayCtx):
+        """Create a ``HistogramPanel``.
+
+        :arg parent:      The :mod:`wx` parent.
+        :arg overlayList: The :class:`.OverlayList` instance.
+        :arg displayCtx:  The :class:`.DisplayContext` instance.
+        """
+
+        actionz = {
+            'toggleHistogramList'    : self.toggleHistogramList,
+            'toggleHistogramControl' : lambda *a: self.togglePanel(
+                fslcontrols.HistogramControlPanel, self, location=wx.TOP) 
+        }
+
+        plotpanel.PlotPanel.__init__(
+            self, parent, overlayList, displayCtx, 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._overlayList.addListener('overlays',
+                                      self._name,
+                                      self.__overlaysChanged)
+        self._displayCtx .addListener('selectedOverlay',
+                                      self._name,
+                                      self.__selectedOverlayChanged)
+
+        self.addListener('showCurrent', self._name, self.draw)
+        self.addListener('histType',    self._name, self.draw)
+        self.addListener('autoBin',     self._name, self.__autoBinChanged)
+        self.addListener('dataSeries',  self._name, self.__dataSeriesChanged)
+
+        # Creating a HistogramSeries is a bit expensive
+        # as it needs to, well, create a histogram. So
+        # we only create one HistogramSeries per overlay,
+        # and we cache them here so that the user only
+        # has to wait the first time they select an
+        # overlay for its histogram to be calculated.
+        #
+        # When a HistogramSeries is added to the dataSeries
+        # list, it is copied from the cached one so, again,
+        # the histogram calculation doesn't need to be done.
+        self.__histCache = {}
+        self.__current   = None
+        self.__updateCurrent()
+
+        self.Layout()
+
+
+    def destroy(self):
+        """Removes some property listeners, destroys all existing
+        :class:`HistogramSeries` instances, and calls
+        :meth:`.PlotPanel.destroy`.
+        """
+        
+        self.removeListener('showCurrent', self._name)
+        self.removeListener('histType',    self._name)
+        self.removeListener('autoBin',     self._name)
+        self.removeListener('dataSeries',  self._name)
+        
+        self._overlayList.removeListener('overlays',        self._name)
+        self._displayCtx .removeListener('selectedOverlay', self._name)
+
+        for hs in set(self.dataSeries[:] + self.__histCache.values()):
+            hs.destroy()
+
+        plotpanel.PlotPanel.destroy(self)
+
+
+    def getCurrent(self):
+        """Return the :class:`HistogramSeries` instance for the currently
+        selected overlay. Returns ``None`` if :attr:``showCurrent` is
+        ``False``, or the current overlay is not an :class:`.Image`.
+        """
+        if self.__current is None:
+            self.__updateCurrent()
+
+        if self.__current is None:
+            return None
+
+        return HistogramSeries(self.__current.overlay,
+                               self,
+                               self._displayCtx,
+                               self._overlayList,
+                               baseHs=self.__current) 
+
+
+    def draw(self, *a):
+        """Draws the current :class:`HistogramSeries` if there is one,, and
+        any ``HistogramSeries`` that are in the :attr:`.PlotPanel.dataSeries`
+        list, via a call to :meth:`.PlotPanel.drawDataSeries`.
+        """
+
+        extra = None
+
+        if self.showCurrent:
+            
+            if self.__current is not None:
+                extra = [self.__current]
+
+        if self.smooth: self.drawDataSeries(extra)
+        else:           self.drawDataSeries(extra, drawstyle='steps-pre')
+
+
+    def toggleHistogramList(self, *a):
+        """Shows/hides a :class:`.HistogramListPanel`. See the
+        :meth:`.ViewPanel.togglePanel` method.
+        """
+        self.togglePanel(fslcontrols.HistogramListPanel, self, location=wx.TOP)
+
+
+    def __dataSeriesChanged(self, *a):
+        """Called when the :attr:`.PlotPanel.dataSeries` property changes.
+        """
+        self.setConstraint('selectedSeries',
+                           'maxval',
+                           len(self.dataSeries) - 1)
+
+        listPanel = self.getPanel(fslcontrols.HistogramListPanel)
+
+        if listPanel is None:
+            self.selectedSeries = 0
+        else:
+            self.selectedSeries = listPanel.getListBox().GetSelection()
+
+            
+    def __overlaysChanged(self, *a):
+        
+        self.disableListener('dataSeries', self._name)
+        
+        for ds in self.dataSeries:
+            if ds.overlay not in self._overlayList:
+                self.dataSeries.remove(ds)
+                
+        self.enableListener('dataSeries', self._name)
+
+        # Remove any dead overlays
+        # from the histogram cache
+        for overlay in list(self.__histCache.keys()):
+            if overlay not in self._overlayList:
+                log.debug('Removing cached histogram series '
+                          'for overlay {}'.format(overlay.name))
+                hs = self.__histCache.pop(overlay)
+                hs.destroy()
+        
+        self.__selectedOverlayChanged()
 
-    if issubclass(data.dtype.type, np.integer):
-        binSize = max(1, np.ceil(binSize))
+        
+    def __selectedOverlayChanged(self, *a):
 
-    adjMin = np.floor(dMin / binSize) * binSize
-    adjMax = np.ceil( dMax / binSize) * binSize
+        self.__updateCurrent()
+        self.draw()
+
+        
+    def __autoBinChanged(self, *a):
+        """Called when the :attr:`autoBin` property changes. Makes sure that
+        all existing :class:`HistogramSeries` instances are updated before
+        the plot is refreshed.
+        """
 
-    nbins = int((adjMax - adjMin) / binSize) + 1
+        for ds in self.dataSeries:
+            ds.histPropsChanged()
+
+        if self.__current is not None:
+            self.__current.histPropsChanged()
+
+        self.draw()
+        
+
+    def __updateCurrent(self):
+
+        # Make sure that the previous HistogramSeries
+        # cleans up after itself, unless it has been
+        # cached
+        if self.__current is not None and \
+           self.__current not in self.__histCache.values():
+            self.__current.destroy()
+            
+        self.__current = None
+        overlay        = self._displayCtx.getSelectedOverlay()
+
+        if len(self._overlayList) == 0 or \
+           not isinstance(overlay, fslimage.Image):
+            return
+
+        # See if there is already a HistogramSeries based on the
+        # current overlay - if there is, use it as the 'base' HS
+        # for the new one, as it will save us some processing time
+        if overlay in self.__histCache:
+            log.debug('Creating new histogram series for overlay {} '
+                      'from cached copy'.format(overlay.name))
+            baseHs = self.__histCache[overlay]
+        else:
+            baseHs = None
+
+        def loadHs():
+            return HistogramSeries(overlay,
+                                   self,
+                                   self._displayCtx,
+                                   self._overlayList,
+                                   baseHs=baseHs)
+
+        # We are creating a new HS instance, so it
+        # needs to do some initla data range/histogram
+        # calculations. Show a message while this is
+        # happening.
+        if baseHs is None:
+            hs = fsldlg.ProcessingDialog(
+                None,
+                strings.messages[self, 'calcHist'].format(overlay.name),
+                loadHs).Run()
+
+            # Put the initial HS instance for this
+            # overlay in the cache so we don't have
+            # to re-calculate it later
+            log.debug('Caching histogram series for '
+                      'overlay {}'.format(overlay.name))
+            self.__histCache[overlay] = hs
+            
+        # The new HS instance is being based on the
+        # current instance, so it can just copy the
+        # histogram data over - no message dialog
+        # is needed
+        else:
+            hs = loadHs()
+
+        hs.colour      = [0, 0, 0]
+        hs.alpha       = 1
+        hs.lineWidth   = 1
+        hs.lineStyle   = '-'
+        hs.label       = None
 
-    return nbins
+        self.__current = hs
 
 
 class HistogramSeries(plotpanel.DataSeries):
@@ -139,7 +431,7 @@ class HistogramSeries(plotpanel.DataSeries):
         self.dataRange.xlo  = nzmin
         self.dataRange.xhi  = nzmax + dist
 
-        self.nbins = autoBin(nzData, self.dataRange.x)
+        self.nbins = self.autoBin(nzData, self.dataRange.x)
 
         if not self.overlay.is4DImage():
             self.finiteData  = finData
@@ -228,7 +520,7 @@ class HistogramSeries(plotpanel.DataSeries):
             else:                    data = self.clippedFiniteData 
         
         if self.hsPanel.autoBin:
-            nbins = autoBin(data, self.dataRange.x)
+            nbins = self.autoBin(data, self.dataRange.x)
 
             self.disableListener('nbins', self.name)
             self.nbins = nbins
@@ -304,6 +596,33 @@ class HistogramSeries(plotpanel.DataSeries):
             self.showOverlayChanged()
             self.enableListener('showOverlay', self.name)
 
+        
+    def autoBin(self, data, dataRange):
+
+        # Automatic histogram bin calculation
+        # as implemented in the original FSLView
+
+        dMin, dMax = dataRange
+        dRange     = dMax - dMin
+
+        binSize = np.power(10, np.ceil(np.log10(dRange) - 1) - 1)
+
+        nbins = dRange / binSize
+
+        while nbins < 100:
+            binSize /= 2
+            nbins    = dRange / binSize
+
+        if issubclass(data.dtype.type, np.integer):
+            binSize = max(1, np.ceil(binSize))
+
+        adjMin = np.floor(dMin / binSize) * binSize
+        adjMax = np.ceil( dMax / binSize) * binSize
+
+        nbins = int((adjMax - adjMin) / binSize) + 1
+
+        return nbins
+            
 
     def getData(self):
 
@@ -337,229 +656,3 @@ class HistogramSeries(plotpanel.DataSeries):
             
         if   histType == 'count':       return xdata, ydata
         elif histType == 'probability': return xdata, ydata / nvals
-
-    
-class HistogramPanel(plotpanel.PlotPanel):
-
-
-    autoBin     = props.Boolean(default=True)
-    showCurrent = props.Boolean(default=True)
-    histType    = props.Choice(('probability', 'count'))
-
-    selectedSeries = props.Int(minval=0, clamped=True)
-    
-
-    def __init__(self, parent, overlayList, displayCtx):
-
-        actionz = {
-            'toggleHistogramList'    : self.toggleHistogramList,
-            'toggleHistogramControl' : lambda *a: self.togglePanel(
-                fslcontrols.HistogramControlPanel, self, location=wx.TOP) 
-        }
-
-        plotpanel.PlotPanel.__init__(
-            self, parent, overlayList, displayCtx, 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._overlayList.addListener('overlays',
-                                      self._name,
-                                      self.__overlaysChanged)
-        self._displayCtx .addListener('selectedOverlay',
-                                      self._name,
-                                      self.__selectedOverlayChanged)
-
-        self.addListener('showCurrent', self._name, self.draw)
-        self.addListener('histType',    self._name, self.draw)
-        self.addListener('autoBin',     self._name, self.__autoBinChanged)
-        self.addListener('dataSeries',  self._name, self.__dataSeriesChanged)
-
-        self.__histCache = {}
-        self.__current   = None
-        self.__updateCurrent()
-
-        self.Layout()
-
-
-    def toggleHistogramList(self, *a):
-        self.togglePanel(fslcontrols.HistogramListPanel, self, location=wx.TOP)
-
-        panel = self.getPanel(fslcontrols.HistogramListPanel)
-
-        if panel is None:
-            return
-
-        def listSelect(ev):
-            ev.Skip()
-            self.selectedSeries = panel.GetSelection()
-
-
-    def destroy(self):
-        """De-registers property listeners. """
-        
-        self.removeListener('showCurrent', self._name)
-        self.removeListener('histType',    self._name)
-        self.removeListener('autoBin',     self._name)
-        self.removeListener('dataSeries',  self._name)
-        
-        self._overlayList.removeListener('overlays',        self._name)
-        self._displayCtx .removeListener('selectedOverlay', self._name)
-
-        for hs in set(self.dataSeries[:] + self.__histCache.values()):
-            hs.destroy()
-
-        plotpanel.PlotPanel.destroy(self)
-
-
-    def __dataSeriesChanged(self, *a):
-        self.setConstraint('selectedSeries',
-                           'maxval',
-                           len(self.dataSeries) - 1)
-
-        listPanel = self.getPanel(fslcontrols.HistogramListPanel)
-
-        if listPanel is None:
-            self.selectedSeries = 0
-        else:
-            self.selectedSeries = listPanel.getListBox().GetSelection()
-
-            
-    def __overlaysChanged(self, *a):
-        
-        self.disableListener('dataSeries', self._name)
-        
-        for ds in self.dataSeries:
-            if ds.overlay not in self._overlayList:
-                self.dataSeries.remove(ds)
-                
-        self.enableListener('dataSeries', self._name)
-
-        # Remove any dead overlays
-        # from the histogram cache
-        for overlay in list(self.__histCache.keys()):
-            if overlay not in self._overlayList:
-                log.debug('Removing cached histogram series '
-                          'for overlay {}'.format(overlay.name))
-                hs = self.__histCache.pop(overlay)
-                hs.destroy()
-        
-        self.__selectedOverlayChanged()
-
-        
-    def __selectedOverlayChanged(self, *a):
-
-        self.__updateCurrent()
-        self.draw()
-
-        
-    def __autoBinChanged(self, *a):
-        """Called when the :attr:`autoBin` property changes. Makes sure that
-        all existing :class:`HistogramSeries` instances are updated before
-        the plot is refreshed.
-        """
-
-        for ds in self.dataSeries:
-            ds.histPropsChanged()
-
-        if self.__current is not None:
-            self.__current.histPropsChanged()
-
-        self.draw()
-        
-
-    def __updateCurrent(self):
-
-        # Make sure that the previous HistogramSeries
-        # cleans up after itself, unless it has been
-        # cached
-        if self.__current is not None and \
-           self.__current not in self.__histCache.values():
-            self.__current.destroy()
-            
-        self.__current = None
-        overlay        = self._displayCtx.getSelectedOverlay()
-
-        if len(self._overlayList) == 0 or \
-           not isinstance(overlay, fslimage.Image):
-            return
-
-        # See if there is already a HistogramSeries based on the
-        # current overlay - if there is, use it as the 'base' HS
-        # for the new one, as it will save us some processing time
-        if overlay in self.__histCache:
-            log.debug('Creating new histogram series for overlay {} '
-                      'from cached copy'.format(overlay.name))
-            baseHs = self.__histCache[overlay]
-        else:
-            baseHs = None
-
-        def loadHs():
-            return HistogramSeries(overlay,
-                                   self,
-                                   self._displayCtx,
-                                   self._overlayList,
-                                   baseHs=baseHs)
-
-        # We are creating a new HS instance, so it
-        # needs to do some initla data range/histogram
-        # calculations. Show a message while this is
-        # happening.
-        if baseHs is None:
-            hs = fsldlg.ProcessingDialog(
-                None,
-                strings.messages[self, 'calcHist'].format(overlay.name),
-                loadHs).Run()
-
-            # Put the initial HS instance for this
-            # overlay in the cache so we don't have
-            # to re-calculate it later
-            log.debug('Caching histogram series for '
-                      'overlay {}'.format(overlay.name))
-            self.__histCache[overlay] = hs
-            
-        # The new HS instance is being based on the
-        # current instance, so it can just copy the
-        # histogram data over - no message dialog
-        # is needed
-        else:
-            hs = loadHs()
-
-        hs.colour      = [0, 0, 0]
-        hs.alpha       = 1
-        hs.lineWidth   = 1
-        hs.lineStyle   = '-'
-        hs.label       = None
-
-        self.__current = hs
-
-
-    def getCurrent(self):
-        if self.__current is None:
-            self.__updateCurrent()
-
-        if self.__current is None:
-            return None
-
-        return HistogramSeries(self.__current.overlay,
-                               self,
-                               self._displayCtx,
-                               self._overlayList,
-                               baseHs=self.__current) 
-
-
-    def draw(self, *a):
-
-        extra = None
-
-        if self.showCurrent:
-            
-            if self.__current is not None:
-                extra = [self.__current]
-
-        if self.smooth: self.drawDataSeries(extra)
-        else:           self.drawDataSeries(extra, drawstyle='steps-pre')
-- 
GitLab