diff --git a/fsl/data/featresults.py b/fsl/data/featresults.py
index 719092a03771d78867d36c69ae7d241fa2231b59..138c0379e11563504f39f4aac6ece0cb44db0036 100644
--- a/fsl/data/featresults.py
+++ b/fsl/data/featresults.py
@@ -134,7 +134,7 @@ def loadDesign(featdir):
             if line.strip() == '/Matrix':
                 break
 
-        matrix = np.loadtxt(f)
+        matrix = np.loadtxt(f, ndmin=2)
 
     if matrix is None or matrix.size == 0:
         raise RuntimeError('{} does not appear to be a '
diff --git a/fsl/data/image.py b/fsl/data/image.py
index 31988dc6ed7cfa5d770a1cde2152b00804bf0d89..3dda62827dec53ca10ec928cf5cbaea68875a5f0 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -333,9 +333,14 @@ class Image(props.HasProperties):
         return int(code)
 
 
-    def getWorldOrientation(self, axis, code=None):
-        """Returns a code representing the orientation of the specified axis
-        in world space.
+    def getOrientation(self, axis, xform):
+        """Returns a code representing the orientation of the specified data
+        axis in the coordinate system defined by the given transformation
+        matrix.
+
+        :arg xform: A transformation matrix which is assumed to transform
+                    coordinates from the image world coordinate system to
+                    some other coordinate system.
 
         This method returns one of the following values, indicating the
         direction in which coordinates along the specified axis increase:
@@ -357,45 +362,16 @@ class Image(props.HasProperties):
         increases from inferior to superior).
         """
 
-        if self.getXFormCode(code) == constants.NIFTI_XFORM_UNKNOWN:
-            return constants.ORIENT_UNKNOWN
-
-        if   axis == 0: return constants.ORIENT_L2R
-        elif axis == 1: return constants.ORIENT_P2A
-        elif axis == 2: return constants.ORIENT_I2S
-
-        else: return constants.ORIENT_UNKNOWN
-
-
-    def getVoxelOrientation(self, axis, code=None):
-        """Returns a code representing the (estimated) orientation of the
-        specified data axis.
-
-        :arg code: May be either ``qform`` or ``sform``, specifying which
-                   transformation to use.
-
-        See the :meth:`getWorldOrientation` method for a description
-        of the return value.
-        """
-        
-        if self.getXFormCode(code) == constants.NIFTI_XFORM_UNKNOWN:
-            return constants.ORIENT_UNKNOWN
-
-        if   code is None:    xform = self.nibImage.get_affine()
-        elif code == 'sform': xform = self.nibImage.get_sform()
-        elif code == 'qform': xform = self.nibImage.get_qform()
-        else: raise ValueError('code must be None, qform, or sform')
+        if self.getXFormCode() == constants.NIFTI_XFORM_UNKNOWN:
+            return constants.ORIENT_UNKNOWN 
         
-        # the aff2axcodes returns one code for each 
-        # axis in the image array (i.e. in voxel space),
-        # which denotes the real world direction
         import nibabel as nib
         code = nib.orientations.aff2axcodes(
             xform,
             ((constants.ORIENT_R2L, constants.ORIENT_L2R),
              (constants.ORIENT_A2P, constants.ORIENT_P2A),
              (constants.ORIENT_S2I, constants.ORIENT_I2S)))[axis]
-        
+
         return code
 
 
diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py
index 6bdc254805eb25fd0fe3e967901f081e6ae5b08b..3a2087da9a028717e832374be773ff89d3d07dfc 100644
--- a/fsl/data/melodicimage.py
+++ b/fsl/data/melodicimage.py
@@ -8,30 +8,58 @@
 """This module provides the :class:`MelodicImage` class, an :class:`.Image`
 sub-class which encapsulates data from a MELODIC analysis.
 """
+
+
 import os.path as op
 
+import props
+
 import image          as fslimage
 import melodicresults as melresults
 
 
 class MelodicImage(fslimage.Image):
+    """The ``MelodicImage`` class is an :class:`.Image` which encapsulates
+    the results of a FSL MELODIC analysis. A ``MelodicImage`` corresponds to
+    the spatial component map file, generally called ``melodic_IC.nii.gz``.
+
+    The ``MelodicImage`` class provides a few MELODIC-specific attributes and
+    methods:
+
+    .. autosummary::
+
+       tr
+       getComponentTimeSeries
+       numComponents
+       getTopLevelAnalysisDir
+       getDataFile
     """
+
+
+    tr = props.Real(default=1.0)
+    """The TR time of the raw data from which this ``MelodicImage`` was
+    generated. If it is possible to do so, this is automatically initialised
+    from the data file (see the :meth:`getDataFile` method).
     """
+    
 
-    def __init__(self, image, *args, **kwargs):
-        """
-        """
+    def __init__(self, path, *args, **kwargs):
+        """Create a ``MelodicImage``.
 
+        :arg path: A path specifying the ``melodic_IC`` image file, or the
+                   ``.ica`` directory.
 
-        if op.isdir(image):
+        All other arguments are passed through to the :meth:`.Image.__init__`
+        method.
+        """
 
-            dirname  = image
+        if op.isdir(path):
+            dirname  = path
             filename = 'melodic_IC'
 
-
         else:
-            dirname  = op.dirname( image)
-            filename = op.basename(image)
+            dirname  = op.dirname( path)
+            filename = op.basename(path)
 
         dirname = dirname.rstrip(op.sep)
 
@@ -48,12 +76,41 @@ class MelodicImage(fslimage.Image):
                                 *args,
                                 **kwargs)
 
+        self.__meldir = dirname
         self.__melmix = melresults.getComponentTimeSeries(dirname)
 
+        # Automatically set the
+        # TR value if possible
+        dataFile = self.getDataFile()
+
+        if dataFile is not None: 
+            dataImage = fslimage.Image(dataFile, loadData=False)
+            if dataImage.is4DImage():
+                self.tr = dataImage.pixdim[3]
+
         
     def getComponentTimeSeries(self, component):
+        """Returns the time course for the specified (0-indexed) component. """
         return self.__melmix[:, component]
 
 
     def numComponents(self):
+        """Returns the number of components in this ``MelodicImage``. """
         return self.shape[3]
+
+
+    def getTopLevelAnalysisDir(self):
+        """Returns the top level analysis, if the melodic analysis for this
+        ``MelodicImage`` is contained within another analysis. Otherwise,
+        returnsa ``None``. See the
+        :func:`.melodicresults.getTopLevelAnalysisDir` function.
+        """
+        return melresults.getTopLevelAnalysisDir(self.__meldir)
+
+
+    def getDataFile(self):
+        """Returns the file name of the data image from which this
+        ``MelodicImage`` was generated, if possible. See the
+        :func:`.melodicresults.getDataFile` function.
+        """
+        return melresults.getDataFile(self.__meldir)
diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py
index d05a25ab309e369353cd1b637cb884e1194f67f2..c31811723bd78e2ce8036072d3c5d3f0e19734d5 100644
--- a/fsl/data/melodicresults.py
+++ b/fsl/data/melodicresults.py
@@ -15,49 +15,46 @@ following functions are provided:
 
    isMelodicDir
    getMelodicDir
+   getTopLevelAnalysisDir
+   getDataFile
    getICFile
+   getMixFile
    getNumComponents
    getComponentTimeSeries
 """
 
 
-import            os 
 import os.path as op
 import numpy   as np
 
-import fsl.data.image as fslimage
+import fsl.data.image       as fslimage
+import fsl.data.featresults as featresults
 
 
 def isMelodicDir(path):
-    """
+    """Returns ``True`` if the given path looks like it is contained within
+    a MELODIC directory, ``False`` otherwise. 
     """
 
     # Must be named *.ica or *.gica
-    meldir = getMelodicDir(path)
-
-    if meldir is None:
-        return False
-
-    # Must contain an image file called melodic_IC
-    try:
-        fslimage.addExt(op.join(meldir, 'melodic_IC'), mustExist=True)
-    except ValueError:
-        return False
-
-    # Must contain a file called melodic_mix
-    if not op.exists(op.join(meldir, 'melodic_mix')):
-        return False
-
-    return True
+    return getMelodicDir(path) is not None
 
     
 def getMelodicDir(path):
-    """
+    """Returns the MELODIC directory in which the given path is contained,
+    or ``None`` if it is not contained within a MELODIC directory. A melodic
+    directory:
+
+      - Must be named ``*.ica`` or ``*.gica``
+      - Must contain a file called ``melodic_IC.nii.gz``
+      - Must contain a file called ``melodic_mix``.
     """
 
     # TODO This code is identical to featresults.getFEATDir.
     # Can you generalise it and put it somewhere in fsl.utils?
 
+    path     = op.abspath(path)
+
     sufs     = ['.ica', '.gica']
     idxs     = [(path.rfind(s), s) for s in sufs]
     idx, suf = max(idxs, key=lambda (i, s): i)
@@ -66,28 +63,92 @@ def getMelodicDir(path):
         return None
 
     idx  += len(suf)
-    path  = path[:idx]
+    path  = path[:idx].rstrip(op.sep)
 
-    if path.endswith(suf) or path.endswith('{}{}'.format(suf, op.sep)):
-        return path
+    if not path.endswith(suf):
+        return None
+
+    # Must contain an image file called melodic_IC
+    try:
+        fslimage.addExt(op.join(path, 'melodic_IC'), mustExist=True)
+    except ValueError:
+        return None
+
+    # Must contain a file called melodic_mix
+    if not op.exists(op.join(path, 'melodic_mix')):
+        return None
                                            
-    return None 
+    return path
 
 
-def getICFile(meldir):
+def getTopLevelAnalysisDir(path):
+    """If the given path is a MELODIC directory, and it is contained within
+    a FEAT directory, or another MELODIC directory, the path to the latter
+    directory is returned. Otherwise, ``None`` is returned.
     """
+
+    meldir = getMelodicDir(path)
+    sufs   =  ['.feat', '.gfeat', '.ica', '.gica']
+    
+    if meldir is None:
+        return None
+
+    if featresults.isFEATDir(meldir):
+        return featresults.getFEATDir(meldir)
+
+    parentDir = op.dirname(meldir)
+    parentDir = parentDir.rstrip(op.sep)
+
+    if not any([parentDir.endswith(s) for s in sufs]):
+        return None
+
+    # Must contain a file called filtered_func_data.nii.gz
+    dataFile = op.join(parentDir, 'filtered_func_data')
+
+    try:
+        dataFile = fslimage.addExt(dataFile, mustExist=True)
+    except ValueError:
+        return None
+
+    return parentDir
+
+    
+def getDataFile(meldir):
+    """If the given melodic directory is contained within another analysis
+    directory, the path to the data file is returned. Otherwise ``None`` is
+    returned.
     """
+
+    topDir = getTopLevelAnalysisDir(meldir)
+
+    if topDir is None:
+        return None
+
+    dataFile = op.join(topDir, 'filtered_func_data')
+
+    try:
+        return fslimage.addExt(dataFile, mustExist=True)
+    except ValueError:
+        return None
+
+
+def getICFile(meldir):
+    """Returns the path to the melodic IC image. """
     return fslimage.addExt(op.join(meldir, 'melodic_IC'))
 
 
 def getMixFile(meldir):
-    """
-    """
+    """Returns the path to the melodic mix file. """
     return op.join(meldir, 'melodic_mix')
 
 
+def getReportFile(meldir):
+    pass
+
+
 def getNumComponents(meldir):
-    """
+    """Returns the number of components generated in the melodic analysis
+    contained in the given directrory.
     """
 
     icImg = fslimage.Image(getICFile(meldir), loadData=False)
@@ -95,7 +156,8 @@ def getNumComponents(meldir):
 
 
 def getComponentTimeSeries(meldir):
-    """
+    """Returns a ``numpy`` array containing the melodic mix for the given
+    directory.
     """
 
     mixfile = getMixFile(meldir)
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 27ec5ed61ebbe336a80ad5fae945d5651d4660c7..a3846b2c83f02d6e3ac6394ebd5567a5fcff6ce7 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -325,13 +325,17 @@ labels = TypeDict({
     'OverlayInfoPanel.Image.transform'    : 'Transform/space',
     'OverlayInfoPanel.Image.orient'       : 'Orientation',
     
-    'OverlayInfoPanel.Image'              : 'NIFTI1 image',
-    'OverlayInfoPanel.FEATImage'          : 'NIFTI1 image (FEAT analysis)',
-    'OverlayInfoPanel.FEATImage.featInfo' : 'FEAT information',
-    'OverlayInfoPanel.Model'              : 'VTK model',
-    'OverlayInfoPanel.Model.numVertices'  : 'Number of vertices',
-    'OverlayInfoPanel.Model.numIndices'   : 'Number of indices',
-    'OverlayInfoPanel.dataSource'         : 'Data source',
+    'OverlayInfoPanel.Image'                    : 'NIFTI1 image',
+    'OverlayInfoPanel.FEATImage'                : 'NIFTI1 image '
+                                                  '(FEAT analysis)',
+    'OverlayInfoPanel.FEATImage.featInfo'       : 'FEAT information',
+    'OverlayInfoPanel.MelodicImage'             : 'NIFTI1 image '
+                                                  '(MELODIC analysis)', 
+    'OverlayInfoPanel.MelodicImage.melodicInfo' : 'MELODIC information',
+    'OverlayInfoPanel.Model'                    : 'VTK model',
+    'OverlayInfoPanel.Model.numVertices'        : 'Number of vertices',
+    'OverlayInfoPanel.Model.numIndices'         : 'Number of indices',
+    'OverlayInfoPanel.dataSource'               : 'Data source',
 })
 
 
@@ -612,12 +616,9 @@ nifti = TypeDict({
     'voxOrient.0'   : 'X voxel orientation',
     'voxOrient.1'   : 'Y voxel orientation',
     'voxOrient.2'   : 'Z voxel orientation',
-    'sformOrient.0' : 'X sform orientation',
-    'sformOrient.1' : 'Y sform orientation',
-    'sformOrient.2' : 'Z sform orientation',
-    'qformOrient.0' : 'X qform orientation',
-    'qformOrient.1' : 'Y qform orientation',
-    'qformOrient.2' : 'Z qform orientation', 
+    'worldOrient.0' : 'X world orientation',
+    'worldOrient.1' : 'Y world orientation',
+    'worldOrient.2' : 'Z world orientation',
 
     'qform' : 'QForm matrix',
     'sform' : 'SForm matrix',
@@ -708,4 +709,14 @@ feat = TypeDict({
     'numPoints'    : 'Number of volumes',
     'numEVs'       : 'Number of EVs',
     'numContrasts' : 'Number of contrasts',
+    'report'       : 'Link to report',
+})
+
+
+melodic = TypeDict({
+    'dataFile'       : 'Data file',
+    'partOfAnalysis' : 'Part of analysis',
+    'numComponents'  : 'Number of ICs',
+    'tr'             : 'TR time',
+    'report'         : 'Link to report',
 })
diff --git a/fsl/fsleyes/colourmaps.py b/fsl/fsleyes/colourmaps.py
index 46e1a2f1b07ceb429367d8cfc18b728280ac1d5f..0589151de45b3242394a6b55c768d8af5991bd9c 100644
--- a/fsl/fsleyes/colourmaps.py
+++ b/fsl/fsleyes/colourmaps.py
@@ -626,7 +626,7 @@ def randomBrightColour():
 def randomDarkColour():
     """Generates a random saturated and darkened RGB colour."""
 
-    return applyBricon(randomBrightColour(), 0.25, 0.5)
+    return applyBricon(randomBrightColour(), 0.35, 0.5)
 
 
 def complementaryColour(rgb):
diff --git a/fsl/fsleyes/controls/overlayinfopanel.py b/fsl/fsleyes/controls/overlayinfopanel.py
index 8f15f85342208b794a0aca449b20e9df794764a2..7d340e1718408977a36cd0bf239d9da1fa3df3ea 100644
--- a/fsl/fsleyes/controls/overlayinfopanel.py
+++ b/fsl/fsleyes/controls/overlayinfopanel.py
@@ -14,9 +14,11 @@ import collections
 import wx
 import wx.html as wxhtml
 
-import fsl.data.strings   as strings
-import fsl.data.constants as constants
-import fsl.fsleyes.panel  as fslpanel
+import numpy as np
+
+import fsl.data.strings    as strings
+import fsl.data.constants  as constants
+import fsl.fsleyes.panel   as fslpanel
 
 
 class OverlayInfoPanel(fslpanel.FSLEyesPanel):
@@ -30,14 +32,15 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
        :scale: 50%
        :align: center
 
-    Slightly different informtion is shown depending on the overlay type,
+    Slightly different information is shown depending on the overlay type,
     and is generated by the following methods:
 
-    =================== ==========================
-    :class:`.Image`     :meth:`__getImageInfo`
-    :class:`.FEATImage` :meth:`__getFEATImageInfo`
-    :class:`.Model`     :meth:`__getModelInfo`
-    =================== ==========================
+    ====================== =============================
+    :class:`.Image`        :meth:`__getImageInfo`
+    :class:`.FEATImage`    :meth:`__getFEATImageInfo`
+    :class:`.MelodicImage` :meth:`__getMelodicImageInfo`
+    :class:`.Model`        :meth:`__getModelInfo`
+    ====================== =============================
     """
 
 
@@ -177,8 +180,10 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
         
         info = OverlayInfo('{} - {}'.format(
             display.name, strings.labels[self, overlay]))
+        
         img  = overlay.nibImage
         hdr  = img.get_header()
+        opts = display.getDisplayOpts()
 
         voxUnits, timeUnits = hdr.get_xyzt_units()
         qformCode           = int(hdr['qform_code'])
@@ -239,7 +244,8 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
                          section=xformSect) 
 
         for i in range(3):
-            orient = overlay.getVoxelOrientation(i)
+            xform  = opts.getTransform('world', 'id')
+            orient = overlay.getOrientation(i, xform)
             orient = '{} - {}'.format(
                 strings.anatomy['Image', 'lowlong',  orient],
                 strings.anatomy['Image', 'highlong', orient])
@@ -248,23 +254,15 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
                          section=orientSect)
 
         for i in range(3):
-            orient = overlay.getWorldOrientation(i, code='sform')
+            xform  = np.eye(4)
+            orient = overlay.getOrientation(i, xform)
             orient = '{} - {}'.format(
                 strings.anatomy['Image', 'lowlong',  orient],
                 strings.anatomy['Image', 'highlong', orient])
-            info.addInfo(strings.nifti['sformOrient.{}'.format(i)],
+            info.addInfo(strings.nifti['worldOrient.{}'.format(i)],
                          orient,
                          section=orientSect)
 
-        for i in range(3):
-            orient = overlay.getWorldOrientation(i, code='qform')
-            orient = '{} - {}'.format(
-                strings.anatomy['Image', 'lowlong',  orient],
-                strings.anatomy['Image', 'highlong', orient])
-            info.addInfo(strings.nifti['qformOrient.{}'.format(i)],
-                         orient,
-                         section=orientSect) 
-
         return info
 
 
@@ -293,6 +291,33 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
 
         return info
 
+
+    def __getMelodicImageInfo(self, overlay, display):
+        """Creates and returns an :class:`OverlayInfo` object containing
+        information about the given :class:`.MelodicImage` overlay.
+
+        :arg overlay: A :class:`.MelodicImage` instance.
+        :arg display: The :class:`.Display` instance assocated with the
+                      ``MelodicImage``.
+        """
+
+        info = self.__getImageInfo(overlay, display)
+
+        melInfo = collections.OrderedDict([
+            ('tr',             overlay.tr),
+            ('dataFile',       overlay.getDataFile()),
+            ('partOfAnalysis', overlay.getTopLevelAnalysisDir()),
+            ('numComponents',  overlay.numComponents()),
+        ])
+
+        secName = strings.labels[self, overlay, 'melodicInfo']
+        info.addSection(secName)
+
+        for k, v in melInfo.items():
+            info.addInfo(strings.melodic[k], v, section=secName)
+
+        return info
+
     
     def __getModelInfo(self, overlay, display):
         """Creates and returns an :class:`OverlayInfo` object containing
diff --git a/fsl/fsleyes/controls/timeserieslistpanel.py b/fsl/fsleyes/controls/timeserieslistpanel.py
index c3d500fb48be3bc98aa70c8d1b0947d43b7ea45d..c211d35b9c7f7d5c0d98ebf94ddf4c53ac4b15c4 100644
--- a/fsl/fsleyes/controls/timeserieslistpanel.py
+++ b/fsl/fsleyes/controls/timeserieslistpanel.py
@@ -183,7 +183,7 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel):
             copy.setData(*ts.getData())
 
             # This is hacky, and is here in order to
-            # make the __onLIstSelect method work.
+            # make the __onListSelect method work.
             if isinstance(ts, timeseries.MelodicTimeSeries):
                 copy.tsLoc = 'volume'
                 copy.coord = ts.getComponent()
diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py
index 62d6c1890340963d47df8ff687d053a4a68d9d83..053801d5f8db5d2e1667920a47045449a2a696af 100644
--- a/fsl/fsleyes/overlay.py
+++ b/fsl/fsleyes/overlay.py
@@ -182,12 +182,12 @@ class OverlayList(props.HasProperties):
         return self.overlays.insertAll(index, items) 
 
 
-def guessDataSourceType(filename):
+def guessDataSourceType(path):
     """A convenience function which, given the name of a file or directory,
     figures out a suitable overlay type.
 
     Returns a tuple containing two values - a type which should be able to
-    load the filename, and the filename, possibly adjusted. If the file type
+    load the path, and the path itself, possibly adjusted. If the type
     is unrecognised, the first tuple value will be ``None``.
     """
 
@@ -198,30 +198,53 @@ def guessDataSourceType(filename):
     import fsl.data.melodicresults as melresults
     import fsl.data.featresults    as featresults
 
-    filename = op.abspath(filename)
-
-    if filename.endswith('.vtk'):
-        return fslmodel.Model, filename
-
-    else:
-        if op.isdir(filename):
-            if featresults.isFEATDir(filename):
-                return fslfeatimage.FEATImage, filename
-            elif melresults.isMelodicDir(filename):
-                return fslmelimage.MelodicImage, filename 
-        else:
-            
-            try:               filename = fslimage.addExt(filename, True)
-            except ValueError: return None, filename
+    path = op.abspath(path)
+
+    # VTK files are easy
+    if path.endswith('.vtk'):
+        return fslmodel.Model, path
+
+    # Now, we check to see if the given
+    # path is part of a FEAT or MELODIC
+    # analysis. The way we go about this is
+    # a bit silly, but is necessary due to
+    # the fact thet a melodic analysis can
+    # be contained  within a feat analysis
+    # (or another melodic analysis). So we
+    # check for all analysis types and, if
+    # more than one analysis type matches,
+    # we return the one with the longest
+    # path name.
+    analyses = [
+        (fslfeatimage.FEATImage,    featresults.getFEATDir(   path)),
+        (fslmelimage .MelodicImage, melresults .getMelodicDir(path))]
+
+    # Remove the analysis types that didn't match
+    # (the get*Dir function returned None)
+    analyses = [(t, d) for (t, d) in analyses if d is not None]
+
+    # If we have one or more matches for
+    # an analysis directory, we return
+    # the one with the longest path
+    if len(analyses) > 0:
+
+        dirlens = map(len, [d for (t, d) in analyses])
+        maxidx  = dirlens.index(max(dirlens))
+        
+        return analyses[maxidx]
 
-            if featresults.isFEATDir(filename):
-                return fslfeatimage.FEATImage, filename
-            elif melresults.isMelodicDir(filename):
-                return fslmelimage.MelodicImage, filename
-            else:
-                return fslimage.Image, filename
+    # If the path is not an analysis directory,
+    # see if it is a regular nifti image
+    try:
+        path = fslimage.addExt(path, mustExist=True)
+        return fslimage.Image, path
+    
+    except ValueError:
+        pass
 
-    return None, filename
+    # Otherwise, I don't
+    # know what to do
+    return None, path
 
 
 def makeWildcard():
diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py
index 8d484aa16aed370f6a7cdb12149774f16aeff847..64c1631098279f21448f5f4de1bc096b6ba16545 100644
--- a/fsl/fsleyes/plotting/timeseries.py
+++ b/fsl/fsleyes/plotting/timeseries.py
@@ -71,8 +71,7 @@ class TimeSeries(dataseries.DataSeries):
         
     def getData(self, xdata=None, ydata=None):
         """Overrides :meth:`.DataSeries.getData`. Returns the data associated
-        with this ``TimeSeries`` instance, pre-processed according to the
-        current :class:`.TimeSeriesPanel` settings.
+        with this ``TimeSeries`` instance.
 
         The ``xdata`` and ``ydata`` arguments may be used by sub-classes to
         override the x/y data in the event that they have already performed
@@ -88,22 +87,7 @@ class TimeSeries(dataseries.DataSeries):
 
         xdata = np.array(xdata, dtype=np.float32)
         ydata = np.array(ydata, dtype=np.float32)
-
-        if self.tsPanel.usePixdim:
-            xdata *= self.overlay.pixdim[3]
         
-        if self.tsPanel.plotMode == 'demean':
-            ydata = ydata - ydata.mean()
-
-        elif self.tsPanel.plotMode == 'normalise':
-            ymin  = ydata.min()
-            ymax  = ydata.max()
-            ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1
-            
-        elif self.tsPanel.plotMode == 'percentChange':
-            mean  = ydata.mean()
-            ydata =  100 * (ydata / mean) - 100
-            
         return xdata, ydata
 
 
@@ -499,7 +483,7 @@ class FEATTimeSeries(VoxelTimeSeries):
                     copenum)
 
 
-    def __plotPEFitChanged(self, evnum):
+    def __plotPEFitChanged(self, *a):
         """Called when the :attr:`plotPEFits` setting changes.
 
         If necessary, creates and caches one or more
diff --git a/fsl/fsleyes/profiles/orthoviewprofile.py b/fsl/fsleyes/profiles/orthoviewprofile.py
index aa90f209924d48c1d611facc11e10abbf9f67e58..8554f225965a2f1a92cab803f5d9f42fa22834d7 100644
--- a/fsl/fsleyes/profiles/orthoviewprofile.py
+++ b/fsl/fsleyes/profiles/orthoviewprofile.py
@@ -24,24 +24,27 @@ class OrthoViewProfile(profiles.Profile):
     allow the user to navigate through the ``OrthoPanel`` display of the
     overlays in the :class:`.OverlayList`.
     
-    ``OrthoViewProfile`` defines three *modes* (see the :class:`.Profile`
-    class documentation):
+    ``OrthoViewProfile`` defines the following *modes* (see the
+    :class:`.Profile` class documentation):
 
-    ======== ==============================================================
-    ``nav``  The user can change the currently displayed location. This is
-             accomplished by updating the :attr:`.DisplayContext.location`
-             property on left mouse drags.
+    ========== ==============================================================
+    ``nav``    The user can change the currently displayed location. This is
+               accomplished by updating the :attr:`.DisplayContext.location`
+               property on left mouse drags.
     
-    ``zoom`` The user can zoom in/out of a canvas with the mouse wheel, and
-             draw a rectangle on a canvas in which to zoom. This is
-             accomplished by updating the :attr:`.SliceCanvasOpts.zoom`
-             property on mouse wheel changes, and displaying a
-             :class:`~.annotations.Rect` annotation on left mouse drags.
+    ``zoom``   The user can zoom in/out of a canvas with the mouse wheel, and
+               draw a rectangle on a canvas in which to zoom. This is
+               accomplished by updating the :attr:`.SliceCanvasOpts.zoom`
+               property on mouse wheel changes, and displaying a
+               :class:`~.annotations.Rect` annotation on left mouse drags.
     
-    ``pan``  The user can pan around a canvas (if the canvas is zoomed in).
-             This is accomplished by calling the
-             :meth:`.SliceCanvas.panDisplayBy` on left mouse drags.
-    ======== ==============================================================
+    ``pan``    The user can pan around a canvas (if the canvas is zoomed in).
+               This is accomplished by calling the
+               :meth:`.SliceCanvas.panDisplayBy` on left mouse drags.
+
+    ``bricon`` The user can drag the mouse along a canvas to change the
+               brightness/contrast of the currently selected overlay.
+    ========== ==============================================================
 
 
     The ``OrthoViewProfile`` class also defines a few actions:
@@ -86,7 +89,7 @@ class OrthoViewProfile(profiles.Profile):
         if extraModes   is None: extraModes   = []
         if extraActions is None: extraActions = {}
 
-        modes   = ['nav', 'pan', 'zoom']
+        modes   = ['nav', 'pan', 'zoom', 'bricon']
         actionz = {
             'resetZoom'    : self.resetZoom,
             'centreCursor' : self.centreCursor,
@@ -309,7 +312,7 @@ class OrthoViewProfile(profiles.Profile):
         if zoom == 0:
             return
 
-        self._zoomModeMouseWheel(canvas, zoom)
+        self._zoomModeMouseWheel(None, canvas, zoom)
 
         
     def _zoomModeLeftMouseDrag(self, ev, canvas, mousePos, canvasPos):
@@ -424,3 +427,33 @@ class OrthoViewProfile(profiles.Profile):
         else:                     return
 
         canvas.panDisplayBy(xoff, yoff)
+
+
+    def _briconModeLeftMouseDrag(self, ev, canvas, mousePos, canvasPos):
+        """Handles left mouse drags in ``bricon`` mode.
+
+        The brightness and contrast of the currently selected overlay are
+        adjusted according to the location of the mouse, relative to the
+        canvas.
+        """
+
+        overlay = self._displayCtx.getSelectedOverlay()
+
+        if overlay is None:
+            return 
+
+        display = self._displayCtx.getDisplay(overlay)
+        w, h    = canvas.GetSize().Get()
+        x, y    = mousePos
+
+        brightness = float(x) / w
+        contrast   = float(y) / h
+
+        log.debug('Adjusting bricon for {} '
+                  '(brightness: {}, contrast: {})'.format(
+                      overlay.name,
+                      brightness,
+                      contrast))
+
+        display.brightness = 100 * brightness
+        display.contrast   = 100 * contrast
diff --git a/fsl/fsleyes/profiles/profilemap.py b/fsl/fsleyes/profiles/profilemap.py
index 36554a6af266991408f34656dcfcbb50e4ee59c5..747841b8e67671b4928d1c8da420249bf104faab 100644
--- a/fsl/fsleyes/profiles/profilemap.py
+++ b/fsl/fsleyes/profiles/profilemap.py
@@ -68,7 +68,8 @@ tempModeMap = {
         (('nav',  wx.WXK_CONTROL), 'zoom'),
         (('pan',  wx.WXK_CONTROL), 'zoom'),
         (('nav',  wx.WXK_ALT),     'pan'),
-        (('zoom', wx.WXK_ALT),     'pan'))),
+        (('zoom', wx.WXK_ALT),     'pan'),
+        (('nav',  wx.WXK_SHIFT),   'bricon'))),
 
     # OrthoEditProfile inherits all of the
     # settings for OrthoViewProfile above,
diff --git a/fsl/fsleyes/splash.py b/fsl/fsleyes/splash.py
index 3a4faef6ccd4600f327e5912381ae9da1ef64dcf..a3b8b8a6bc4492f64a85aaf442bcbb615827559e 100644
--- a/fsl/fsleyes/splash.py
+++ b/fsl/fsleyes/splash.py
@@ -50,7 +50,7 @@ class FSLEyesSplash(wx.Frame):
         splashimg  = splashbmp.ConvertToImage()
     
         self.__splashPanel = imagepanel.ImagePanel(self, splashimg)
-        self.__statusBar   = wx.StaticText(self, style=wx.ELLIPSIZE_MIDDLE)
+        self.__statusBar   = wx.StaticText(self, style=wx.ST_ELLIPSIZE_MIDDLE)
         
         self.__statusBar.SetLabel(strings.messages[self, 'default'])
 
diff --git a/fsl/fsleyes/views/canvaspanel.py b/fsl/fsleyes/views/canvaspanel.py
index 6e95555ef665d4d8015325f7b61c49493826ae47..4ca4ad4068d0bdd2d7ecd5ff81c84f746edc768e 100644
--- a/fsl/fsleyes/views/canvaspanel.py
+++ b/fsl/fsleyes/views/canvaspanel.py
@@ -36,8 +36,8 @@ import fsl.fsleyes.controls.clusterpanel          as clusterpanel
 import fsl.fsleyes.controls.lookuptablepanel      as lookuptablepanel
 import fsl.fsleyes.controls.shellpanel            as shellpanel
 
-import                                  colourbarpanel
-import                                  viewpanel
+import                                               colourbarpanel
+import                                               viewpanel
 
 
 log = logging.getLogger(__name__)
@@ -64,10 +64,16 @@ class CanvasPanel(viewpanel.ViewPanel):
     Sub-classes of the ``CanvasPanel`` must do the following:
     
       1. Add their content to the panel that is accessible via the
-         :meth:`getCanvasPanel` method (see the note on
+         :meth:`getContentPanel` method (see the note on
          :ref:`adding content <canvaspanel-adding-content>`).
     
       2. Override the :meth:`getGLCanvases` method.
+
+      3. Call the :meth:`centrePanelLayout` method in their ``__init__``
+         method.
+
+      4. Override the :meth:`centrePanelLayout` method if any custom layout is
+         necessary.
     
     
     **Actions**
@@ -104,14 +110,14 @@ class CanvasPanel(viewpanel.ViewPanel):
 
 
     .. _canvaspanel-adding-content:
+
     
     **Adding content**
 
     
-    To support colour bar functionality, the ``CanvasPanel`` uses a hierarchy
-    of ``wx.Panel`` instances, depicted in the following containment
-    hierarchy diagram:
-
+    To support colour bar and screenshot functionality, the ``CanvasPanel``
+    uses a hierarchy of ``wx.Panel`` instances, depicted in the following
+    containment hierarchy diagram:
     
     .. graphviz::
 
@@ -127,23 +133,49 @@ class CanvasPanel(viewpanel.ViewPanel):
          rankdir="BT";
     
          1 [label="CanvasPanel"];
-         2 [label="Canvas container"];
-         3 [label="ColourBarPanel"];
-         4 [label="Centre panel"];
-         5 [label="Content added by sub-classes"];
+         2 [label="Centre panel"];
+         3 [label="Custom content (for complex layouts)"];
+         4 [label="Container panel"];
+         5 [label="ColourBarPanel"];
+         6 [label="Content panel"];
+         7 [label="Content added by sub-classes"];
 
          2 -> 1;
          3 -> 2;
          4 -> 2;
          5 -> 4;
+         6 -> 4;
+         7 -> 6;
        }
 
     
     As depicted in the diagram, sub-classes need to add their content to the
-    *centre panel*. This panel is accessible via the :meth:`getCanvasPanel`
-    method. The *container panel* is what gets passed to the
+    *content panel*. This panel is accessible via the :meth:`getContentPanel`
+    method. 
+
+    
+    The *centre panel* is what gets passed to the
     :meth:`.ViewPanel.setCentrePanel` method, and is accessible via the
-    :meth:`getCanvasContainer` method, if necessary.
+    :meth:`getCentrePanel` method, if necessary. The *container panel* is
+    also available, via the :meth:`getContainerPanel`. Everything in the
+    container panel will appear in screenshots (see the :meth:`screenshot`
+    method).
+
+    
+    The :meth:`centrePanelLayout` method lays out the centre panel, using the
+    :meth:`layoutContainerPanel` method to lay out the colour bar and the
+    content panel. The ``centrePanelLayout`` method simply adds the canvas
+    container directly to the centre panel. Sub-classes which have more
+    advanced layout requirements (e.g.  the :class:`.LightBoxPanel` needs a
+    scrollbar) may override the :meth:`centrePanelLayout` method to implement
+    their own layout.  These sub-class implementations must:
+
+      1. Call the :meth:`layoutContainerPanel` method.
+
+      2. Add the container panel (accessed via :meth:`getContainerPanel`)
+         to the centre panel (accessed via :meth:`getCentrePanel`).
+
+      3. Add any other custom content to the centre panel.
     """
 
     
@@ -274,10 +306,11 @@ class CanvasPanel(viewpanel.ViewPanel):
             self.disableProperty('syncOverlayOrder')
             self.disableProperty('syncOverlayDisplay')
 
-        self.__canvasContainer = wx.Panel(self)
-        self.__canvasPanel     = wx.Panel(self.__canvasContainer)
+        self.__centrePanel    = wx.Panel(self)
+        self.__containerPanel = wx.Panel(self.__centrePanel)
+        self.__contentPanel   = wx.Panel(self.__containerPanel)
 
-        self.setCentrePanel(self.__canvasContainer)
+        self.setCentrePanel(self.__centrePanel)
 
         # Stores a reference to a wx.Timer
         # when movie mode is enabled
@@ -291,17 +324,18 @@ class CanvasPanel(viewpanel.ViewPanel):
                          self.__movieRateChanged)
 
         # Canvas/colour bar layout is managed in
-        # the _layout/_toggleColourBar methods
-        self.__canvasSizer   = None
-        self.__colourBar     = None
+        # the layoutColourBarAndCanvas method
+        self.__colourBar = None
 
         # Use a different listener name so that subclasses
         # can register on the same properties with self._name
         lName = 'CanvasPanel_{}'.format(self._name)
-        self.__opts.addListener('colourBarLocation', lName, self.__layout)
-        self.__opts.addListener('showColourBar',     lName, self.__layout)
-        
-        self.__layout()
+        self.__opts.addListener('colourBarLocation',
+                                lName,
+                                self.__colourBarPropsChanged)
+        self.__opts.addListener('showColourBar',
+                                lName,
+                                self.__colourBarPropsChanged)
 
 
     def destroy(self):
@@ -337,20 +371,28 @@ class CanvasPanel(viewpanel.ViewPanel):
         return self.__opts
                 
         
-    def getCanvasPanel(self):
+    def getCentrePanel(self):
+        """Returns the ``wx.Panel`` which is passed to
+        :meth:`.ViewPanel.setCentrePanel`. See the note on
+        :ref:`adding content <canvaspanel-adding-content>`.
+        """
+        return self.__centrePanel
+
+    
+    def getContentPanel(self):
         """Returns the ``wx.Panel`` to which sub-classes must add their content.
         See the note on :ref:`adding content <canvaspanel-adding-content>`.
         """
-        return self.__canvasPanel
+        return self.__contentPanel 
 
 
-    def getCanvasContainer(self):
+    def getContainerPanel(self):
         """Returns the ``wx.Panel`` which contains the
-        :class:`.ColourBarPanel` if it is being displayed, and the canvas
+        :class:`.ColourBarPanel` if it is being displayed, and the content
         panel. See the note on
         :ref:`adding content <canvaspanel-adding-content>`.
         """
-        return self.__canvasContainer
+        return self.__containerPanel
 
 
     def getGLCanvases(self):
@@ -375,11 +417,27 @@ class CanvasPanel(viewpanel.ViewPanel):
         return None
 
 
-    def __layout(self, *a):
-        """Called when any colour bar display properties are changed (see
-        :class:`.SceneOpts`). Lays out the container panel, which contains
-        the :class:`.ColourBarPanel` and all content added by the
-        ``CanvasPanel`` sub-class implementation.
+    def centrePanelLayout(self):
+        """Lays out the centre panel. This method may be overridden by
+        sub-classes which need more advanced layout logic. See the note on
+        :ref:`adding content <canvaspanel-adding-content>`
+        """
+
+        self.layoutContainerPanel()
+
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(self.__containerPanel, flag=wx.EXPAND, proportion=1)
+        self.__centrePanel.SetSizer(sizer)
+        
+        self.PostSizeEvent()
+        
+    
+    def layoutContainerPanel(self):
+        """Creates a ``wx.Sizer``, and uses it to lay out the colour bar panel
+        and canvas panel. The sizer object is returned.
+
+        This method is used by the default :meth:`centrePanelLayout` method,
+        and is available for custom sub-class implementations to use.
         """
 
         if not self.__opts.showColourBar:
@@ -391,19 +449,15 @@ class CanvasPanel(viewpanel.ViewPanel):
                 self.__colourBar.destroy()
                 self.__colourBar.Destroy()
                 self.__colourBar = None
-                
-            self.__canvasSizer = wx.BoxSizer(wx.HORIZONTAL)
-            self.__canvasSizer.Add(self.__canvasPanel,
-                                   flag=wx.EXPAND,
-                                   proportion=1)
 
-            self.__canvasContainer.SetSizer(self.__canvasSizer)
-            self.PostSizeEvent()
+            sizer = wx.BoxSizer(wx.HORIZONTAL)
+            sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
+            self.__containerPanel.SetSizer(sizer)
             return
 
         if self.__colourBar is None:
             self.__colourBar = colourbarpanel.ColourBarPanel(
-                self.__canvasContainer, self._overlayList, self._displayCtx)
+                self.__containerPanel, self._overlayList, self._displayCtx)
 
         self.__opts.bindProps('colourBarLabelSide',
                               self.__colourBar,
@@ -415,23 +469,18 @@ class CanvasPanel(viewpanel.ViewPanel):
             self.__colourBar.orientation = 'vertical'
         
         if self.__opts.colourBarLocation in ('top', 'bottom'):
-            self.__canvasSizer = wx.BoxSizer(wx.VERTICAL)
+            sizer = wx.BoxSizer(wx.VERTICAL)
         else:
-            self.__canvasSizer = wx.BoxSizer(wx.HORIZONTAL)
-
-        self.__canvasContainer.SetSizer(self.__canvasSizer)
+            sizer = wx.BoxSizer(wx.HORIZONTAL)
 
         if self.__opts.colourBarLocation in ('top', 'left'):
-            self.__canvasSizer.Add(self.__colourBar,   flag=wx.EXPAND)
-            self.__canvasSizer.Add(self.__canvasPanel, flag=wx.EXPAND,
-                                   proportion=1)
+            sizer.Add(self.__colourBar,    flag=wx.EXPAND)
+            sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
         else:
-            self.__canvasSizer.Add(self.__canvasPanel, flag=wx.EXPAND,
-                                   proportion=1)
-            self.__canvasSizer.Add(self.__colourBar,   flag=wx.EXPAND)
+            sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
+            sizer.Add(self.__colourBar,    flag=wx.EXPAND)
 
-        # Force the canvas panel to resize itself
-        self.PostSizeEvent()
+        self.__containerPanel.SetSizer(sizer)
 
 
     def __movieModeChanged(self, *a):
@@ -455,6 +504,13 @@ class CanvasPanel(viewpanel.ViewPanel):
         self.__movieTimer = wx.Timer(self)
         self.Bind(wx.EVT_TIMER, self.__movieUpdate)
         self.__movieTimer.Start(rate)
+
+
+    def __colourBarPropsChanged(self, *a):
+        """Called when any colour bar display properties are changed (see
+        :class:`.SceneOpts`). Calls :meth:`canvasPanelLayout`.
+        """
+        self.centrePanelLayout()
         
 
     def __movieRateChanged(self, *a):
@@ -653,7 +709,7 @@ def _screenshot(overlayList, displayCtx, canvasPanel):
         # direct parent of the colour bar
         # canvas, and an ancestor of the
         # other GL canvases
-        parent        = canvasPanel.getCanvasContainer()
+        parent        = canvasPanel.getContainerPanel()
         width, height = parent.GetClientSize().Get()
         windowDC      = wx.WindowDC(parent)
         memoryDC      = wx.MemoryDC()
@@ -679,6 +735,9 @@ def _screenshot(overlayList, displayCtx, canvasPanel):
         rgb  = bmp.ConvertToImage().GetData()
         rgb  = np.fromstring(rgb, dtype=np.uint8)
 
+        log.debug('Creating bitmap {} * {} for {} screenshot'.format(
+            width, height, type(canvasPanel).__name__))
+
         data[:, :, :3] = rgb.reshape(height, width, 3)
 
         # Patch in bitmaps for every GL canvas
@@ -689,6 +748,11 @@ def _screenshot(overlayList, displayCtx, canvasPanel):
             if glCanvas is None:
                 continue
 
+            # Hidden wx objects will
+            # still return a size
+            if not glCanvas.IsShown():
+                continue
+
             pos   = relativePosition(glCanvas, parent)
             size  = glCanvas.GetClientSize().Get()
 
@@ -710,6 +774,9 @@ def _screenshot(overlayList, displayCtx, canvasPanel):
                 w    = xend - xstart
                 h    = yend - ystart
                 bmp  = bmp[:h, :w, :]
+
+            log.debug('Patching {} in at [{} - {}], [{} - {}]'.format(
+                type(glCanvas).__name__, xstart, xend, ystart, yend))
             
             data[ystart:yend, xstart:xend] = bmp
 
diff --git a/fsl/fsleyes/views/lightboxpanel.py b/fsl/fsleyes/views/lightboxpanel.py
index 0d3de04f3e24cb7b70e36179650bff1d5b8d9e2e..7ac448133b5a65dfbbb221ac93bab5c4ddb434fb 100644
--- a/fsl/fsleyes/views/lightboxpanel.py
+++ b/fsl/fsleyes/views/lightboxpanel.py
@@ -86,11 +86,11 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
                                          actionz)
 
         self.__scrollbar = wx.ScrollBar(
-            self.getCanvasPanel(),
+            self.getCentrePanel(),
             style=wx.SB_VERTICAL)
         
         self.__lbCanvas  = lightboxcanvas.WXGLLightBoxCanvas(
-            self.getCanvasPanel(),
+            self.getContentPanel(),
             overlayList,
             displayCtx)
 
@@ -117,10 +117,9 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
         sceneOpts.bindProps('zrange',       self.__lbCanvas)
 
         self.__canvasSizer = wx.BoxSizer(wx.HORIZONTAL)
-        self.getCanvasPanel().SetSizer(self.__canvasSizer)
+        self.getContentPanel().SetSizer(self.__canvasSizer)
 
-        self.__canvasSizer.Add(self.__lbCanvas,  flag=wx.EXPAND, proportion=1)
-        self.__canvasSizer.Add(self.__scrollbar, flag=wx.EXPAND)
+        self.__canvasSizer.Add(self.__lbCanvas, flag=wx.EXPAND, proportion=1)
 
         # When the display context location changes,
         # make sure the location is shown on the canvas
@@ -162,8 +161,9 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
 
         self.__onLightBoxChange()
         self.__onZoom()
+        
         self.__selectedOverlayChanged()
-        self.Layout()
+        self.centrePanelLayout()
         self.initProfile()
 
         # The ViewPanel AuiManager seems to
@@ -204,6 +204,25 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
         """Returns a reference to the :class:`.LightBoxCanvas` instance. """
         return self.__lbCanvas
 
+    
+    def centrePanelLayout(self):
+        """Overrides :meth:`.CanvasPanel.centrePanelLayout`. Adds the
+        scrollbar to the centre panel.
+        """
+
+        self.layoutContainerPanel()
+        
+        centrePanel    = self.getCentrePanel()
+        containerPanel = self.getContainerPanel()
+        sizer          = wx.BoxSizer(wx.HORIZONTAL)
+
+        centrePanel.SetSizer(sizer)
+
+        sizer.Add(containerPanel,   flag=wx.EXPAND, proportion=1)
+        sizer.Add(self.__scrollbar, flag=wx.EXPAND)
+
+        self.PostSizeEvent()
+
         
     def __selectedOverlayChanged(self, *a):
         """Called when the :attr:`.DisplayContext.selectedOverlay` changes.
diff --git a/fsl/fsleyes/views/orthopanel.py b/fsl/fsleyes/views/orthopanel.py
index 5ae778dac51596a86598ae20ea6aa8ac5f54b167..25ca142f550b4effa1afd5c67b9266b4161f80eb 100644
--- a/fsl/fsleyes/views/orthopanel.py
+++ b/fsl/fsleyes/views/orthopanel.py
@@ -148,19 +148,19 @@ class OrthoPanel(canvaspanel.CanvasPanel):
                                          sceneOpts,
                                          actionz)
 
-        canvasPanel = self.getCanvasPanel()
+        contentPanel = self.getContentPanel()
 
         # The canvases themselves - each one displays a
         # slice along each of the three world axes
-        self.__xcanvas = slicecanvas.WXGLSliceCanvas(canvasPanel,
+        self.__xcanvas = slicecanvas.WXGLSliceCanvas(contentPanel,
                                                      overlayList,
                                                      displayCtx,
                                                      zax=0)
-        self.__ycanvas = slicecanvas.WXGLSliceCanvas(canvasPanel,
+        self.__ycanvas = slicecanvas.WXGLSliceCanvas(contentPanel,
                                                      overlayList,
                                                      displayCtx,
                                                      zax=1)
-        self.__zcanvas = slicecanvas.WXGLSliceCanvas(canvasPanel,
+        self.__zcanvas = slicecanvas.WXGLSliceCanvas(contentPanel,
                                                      overlayList,
                                                      displayCtx,
                                                      zax=2)
@@ -172,9 +172,9 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         self.__zLabels = {}
         
         for side in ('left', 'right', 'top', 'bottom'):
-            self.__xLabels[side] = wx.StaticText(canvasPanel)
-            self.__yLabels[side] = wx.StaticText(canvasPanel)
-            self.__zLabels[side] = wx.StaticText(canvasPanel)
+            self.__xLabels[side] = wx.StaticText(contentPanel)
+            self.__yLabels[side] = wx.StaticText(contentPanel)
+            self.__zLabels[side] = wx.StaticText(contentPanel)
 
         self.__xcanvas.bindProps('showCursor',   sceneOpts)
         self.__ycanvas.bindProps('showCursor',   sceneOpts)
@@ -236,13 +236,14 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         # the slice canvases when the canvas
         # panel is resized, so aspect ratio
         # is maintained
-        canvasPanel.Bind(wx.EVT_SIZE, self.__onResize)
+        contentPanel.Bind(wx.EVT_SIZE, self.__onResize)
 
         # Initialise the panel
         self.__refreshLayout()
         self.__bgColourChanged()
         self.__overlayListChanged()
         self.__locationChanged()
+        self.centrePanelLayout()
         self.initProfile()
 
         # The ViewPanel AuiManager seems to
@@ -328,8 +329,8 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         bg = [int(round(c * 255)) for c in bg]
         fg = [int(round(c * 255)) for c in fg]
 
-        self.getCanvasPanel().SetBackgroundColour(bg)
-        self.getCanvasPanel().SetForegroundColour(fg)
+        self.getContentPanel().SetBackgroundColour(bg)
+        self.getContentPanel().SetForegroundColour(fg)
 
         self.__xcanvas.SetBackgroundColour(bg)
         self.__ycanvas.SetBackgroundColour(bg)
@@ -358,16 +359,12 @@ class OrthoPanel(canvaspanel.CanvasPanel):
                     self.__zLabels.values() 
 
         if overlay is not None:
-            opts = self._displayCtx.getOpts(overlay)
+            opts  = self._displayCtx.getOpts(overlay)
+            xform = opts.getTransform('world', 'display')
             
-            if opts.transform in ('pixdim', 'id'):
-                xorient = overlay.getVoxelOrientation(0)
-                yorient = overlay.getVoxelOrientation(1)
-                zorient = overlay.getVoxelOrientation(2)
-            else:
-                xorient = overlay.getWorldOrientation(0)
-                yorient = overlay.getWorldOrientation(1)
-                zorient = overlay.getWorldOrientation(2)
+            xorient = overlay.getOrientation(0, xform)
+            yorient = overlay.getOrientation(1, xform)
+            zorient = overlay.getOrientation(2, xform)
 
             if constants.ORIENT_UNKNOWN in (xorient, yorient, zorient):
 
@@ -428,13 +425,10 @@ class OrthoPanel(canvaspanel.CanvasPanel):
 
             # Update anatomy labels when 
             # overlay bounds change
-            if i == self._displayCtx.selectedOverlay:
-                opts.addListener('bounds',
-                                 self._name,
-                                 self.__refreshLabels,
-                                 overwrite=True)
-            else:
-                opts.removeListener('bounds', self._name)
+            opts.addListener('bounds',
+                             self._name,
+                             self.__refreshLabels,
+                             overwrite=True)
                 
         # anatomical orientation may have changed with an image change
         self.__refreshLabels()
@@ -479,23 +473,17 @@ class OrthoPanel(canvaspanel.CanvasPanel):
             self.PostSizeEvent()
             return
 
-        opts = self._displayCtx.getOpts(overlay)
 
-        # The image is being displayed as it is stored on
-        # disk - the image.getOrientation method calculates
-        # and returns labels for each voxelwise axis.
-        if opts.transform in ('pixdim', 'id'):
-            xorient = overlay.getVoxelOrientation(0)
-            yorient = overlay.getVoxelOrientation(1)
-            zorient = overlay.getVoxelOrientation(2)
+        log.debug('Refreshing orientation labels '
+                  'according to {}'.format(overlay.name))
 
-        # The overlay is being displayed in 'real world' space -
-        # the definition of this space may be present in the
-        # overlay meta data
-        else:
-            xorient = overlay.getWorldOrientation(0)
-            yorient = overlay.getWorldOrientation(1)
-            zorient = overlay.getWorldOrientation(2)
+        # Figure out the orientation of the
+        # image in the display coordinate system
+        opts    = self._displayCtx.getOpts(overlay)
+        xform   = opts.getTransform('world', 'display')
+        xorient = overlay.getOrientation(0, xform)
+        yorient = overlay.getOrientation(1, xform)
+        zorient = overlay.getOrientation(2, xform)
 
         xlo = strings.anatomy['Image', 'lowshort',  xorient]
         ylo = strings.anatomy['Image', 'lowshort',  yorient]
@@ -504,6 +492,10 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         yhi = strings.anatomy['Image', 'highshort', yorient]
         zhi = strings.anatomy['Image', 'highshort', zorient]
 
+        log.debug('X orientation: {} - {}'.format(xlo, xhi))
+        log.debug('Y orientation: {} - {}'.format(ylo, yhi))
+        log.debug('Z orientation: {} - {}'.format(zlo, zhi))
+
         bg = sceneOpts.bgColour
         fg = colourmaps.complementaryColour(bg)
         bg = [int(round(c * 255)) for c in bg]
@@ -513,16 +505,16 @@ class OrthoPanel(canvaspanel.CanvasPanel):
 
         self.__xLabels['left']  .SetLabel(ylo)
         self.__xLabels['right'] .SetLabel(yhi)
-        self.__xLabels['top']   .SetLabel(zlo)
-        self.__xLabels['bottom'].SetLabel(zhi)
+        self.__xLabels['bottom'].SetLabel(zlo)
+        self.__xLabels['top']   .SetLabel(zhi)
         self.__yLabels['left']  .SetLabel(xlo)
         self.__yLabels['right'] .SetLabel(xhi)
-        self.__yLabels['top']   .SetLabel(zlo)
-        self.__yLabels['bottom'].SetLabel(zhi)
+        self.__yLabels['bottom'].SetLabel(zlo)
+        self.__yLabels['top']   .SetLabel(zhi)
         self.__zLabels['left']  .SetLabel(xlo)
         self.__zLabels['right'] .SetLabel(xhi)
-        self.__zLabels['top']   .SetLabel(ylo)
-        self.__zLabels['bottom'].SetLabel(yhi)
+        self.__zLabels['bottom'].SetLabel(ylo)
+        self.__zLabels['top']   .SetLabel(yhi)
 
         self.PostSizeEvent()
 
@@ -539,7 +531,7 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         opts   = self.getSceneOptions()
         layout = opts.layout
 
-        width, height = self.getCanvasPanel().GetClientSize().Get()
+        width, height = self.getContentPanel().GetClientSize().Get()
 
         show     = [opts.showXCanvas,  opts.showYCanvas,  opts.showZCanvas]
         canvases = [self.__xcanvas,    self.__ycanvas,    self.__zcanvas]
@@ -765,7 +757,7 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         for w in widgets:
             self.__canvasSizer.Add(w, flag=flag)
                                           
-        self.getCanvasPanel().SetSizer(self.__canvasSizer)
+        self.getContentPanel().SetSizer(self.__canvasSizer)
 
         # Calculate/ adjust the appropriate sizes
         # for each canvas, such that they are scaled
@@ -775,7 +767,7 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         self.__calcCanvasSizes()
 
         self.Layout()
-        self.getCanvasPanel().Layout()
+        self.getContentPanel().Layout()
         self.Refresh()
 
 
diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py
index 0a64d0e59dd5dcafbdf053cb7f0783f3806eb696..e80aa4b782ebd24f5dc04a026b658effb3eac796 100644
--- a/fsl/fsleyes/views/plotpanel.py
+++ b/fsl/fsleyes/views/plotpanel.py
@@ -355,13 +355,16 @@ class PlotPanel(viewpanel.ViewPanel):
         self.Refresh()
 
 
-    def drawDataSeries(self, extraSeries=None, **plotArgs):
+    def drawDataSeries(self, extraSeries=None, preproc=None, **plotArgs):
         """Plots all of the :class:`.DataSeries` instances in the
         :attr:`dataSeries` list
 
         :arg extraSeries: A sequence of additional ``DataSeries`` to be
                           plotted.
 
+        :arg preproc:     An optional preprocessing function - passed to the
+                          :meth:`__drawOneDataSeries` method.
+
         :arg plotArgs:    Passed through to the :meth:`__drawOneDataSeries`
                           method.
         """
@@ -397,7 +400,7 @@ class PlotPanel(viewpanel.ViewPanel):
         ylims = []
 
         for ds in toPlot:
-            xlim, ylim = self.__drawOneDataSeries(ds, **plotArgs)
+            xlim, ylim = self.__drawOneDataSeries(ds, preproc, **plotArgs)
             xlims.append(xlim)
             ylims.append(ylim)
 
@@ -474,16 +477,26 @@ class PlotPanel(viewpanel.ViewPanel):
         self.Refresh()
 
         
-    def __drawOneDataSeries(self, ds, **plotArgs):
+    def __drawOneDataSeries(self, ds, preproc=None, **plotArgs):
         """Plots a single :class:`.DataSeries` instance. This method is called
         by the :meth:`drawDataSeries` method.
 
         :arg ds:       The ``DataSeries`` instance.
 
+        :arg preproc:  An optional preprocessing function which must accept
+                       the ``DataSeries`` instance as its sole argument, and
+                       must return the ``(xdata, ydata)`` with any required
+                       processing applied.  The default preprocessing function
+                       returns the result of a call to
+                       :meth:`.DataSeries.getData`.
+
         :arg plotArgs: May be used to customise the plot - these
                        arguments are all passed through to the
                        ``Axis.plot`` function.
         """
+        
+        if preproc is None:
+            preproc = lambda s: s.getData()
 
         if ds.alpha == 0:
             return (0, 0), (0, 0)
@@ -491,7 +504,7 @@ class PlotPanel(viewpanel.ViewPanel):
         log.debug('Drawing {} for {}'.format(type(ds).__name__,
                                              ds.overlay))
 
-        xdata, ydata = ds.getData()
+        xdata, ydata = preproc(ds)
 
         if len(xdata) != len(ydata) or len(xdata) == 0:
             return (0, 0), (0, 0)
diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py
index 5a879e526528b542ece4f59efe961faa81221d1b..d83221c9a1eee94064fc5a21b1cc3f4fa1f6351a 100644
--- a/fsl/fsleyes/views/timeseriespanel.py
+++ b/fsl/fsleyes/views/timeseriespanel.py
@@ -20,7 +20,7 @@ import fsl.data.featimage                          as fslfeatimage
 import fsl.data.melodicimage                       as fslmelimage
 import fsl.data.image                              as fslimage
 import fsl.fsleyes.colourmaps                      as fslcmaps
-import fsl.fsleyes.plotting.timeseries             as timeseries
+import fsl.fsleyes.plotting                        as plotting
 import fsl.fsleyes.controls.timeseriescontrolpanel as timeseriescontrolpanel
 import fsl.fsleyes.controls.timeserieslistpanel    as timeserieslistpanel
 
@@ -281,14 +281,15 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         tss = [ts for ts in tss if ts is not None]
 
         for i, ts in enumerate(list(tss)):
-            if isinstance(ts, timeseries.FEATTimeSeries):
+            if isinstance(ts, plotting.FEATTimeSeries):
                 tss.pop(i)
                 tss = tss[:i] + ts.getModelTimeSeries() + tss[i:]
 
         for ts in tss:
             ts.label = ts.makeLabel()
 
-        self.drawDataSeries(extraSeries=tss)
+        self.drawDataSeries(extraSeries=tss,
+                            preproc=self.__prepareTimeSeriesData)
 
 
     def getTimeSeries(self, overlay):
@@ -298,6 +299,39 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         return self.__currentTss.get(overlay)
 
 
+    def __prepareTimeSeriesData(self, ts):
+        """Given a :class:`.TimeSeries` instance, scales and normalises
+        the x and y data according to the current values of the
+        :attr:`usePixdim` and :attr:`plotMode` properties.
+
+        This method is used as a preprocessing function for all
+        :class:`.TimeSeries` instances that are plotted - see the
+        :meth:`.PlotPanel.drawDataSeries` method.
+        """
+
+        xdata, ydata = ts.getData()
+
+        if self.usePixdim:
+            if isinstance(ts.overlay, fslmelimage.MelodicImage):
+                xdata *= ts.overlay.tr
+            else:
+                xdata *= ts.overlay.pixdim[3]
+        
+        if self.plotMode == 'demean':
+            ydata = ydata - ydata.mean()
+
+        elif self.plotMode == 'normalise':
+            ymin  = ydata.min()
+            ymax  = ydata.max()
+            ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1
+            
+        elif self.plotMode == 'percentChange':
+            mean  = ydata.mean()
+            ydata =  100 * (ydata / mean) - 100
+            
+        return xdata, ydata 
+
+
     def __overlayListChanged(self, *a):
         """Called when the :class:`.OverlayList` changes. Makes sure that
         there are no :class:`.TimeSeries` instances in the
@@ -402,18 +436,18 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             return None, None, None
 
         if isinstance(overlay, fslfeatimage.FEATImage):
-            ts = timeseries.FEATTimeSeries(self, overlay, self._displayCtx)
+            ts = plotting.FEATTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx]
             propNames = ['location']
             
         elif isinstance(overlay, fslmelimage.MelodicImage) and \
              self.plotMelodicICs:
-            ts = timeseries.MelodicTimeSeries(self, overlay, self._displayCtx)
+            ts = plotting.MelodicTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx.getOpts(overlay)]
             propNames = ['volume'] 
             
         else:
-            ts = timeseries.VoxelTimeSeries(self, overlay, self._displayCtx)
+            ts = plotting.VoxelTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx]
             propNames = ['location'] 
 
diff --git a/fsl/tools/render.py b/fsl/tools/render.py
index e346114e1992714d70b9885373b980a8a68c20fb..f586153e2bc5b3bd8d5f948a815e5afdcb51fdd8 100644
--- a/fsl/tools/render.py
+++ b/fsl/tools/render.py
@@ -77,22 +77,10 @@ def buildLabelBitmaps(overlayList,
 
         display = displayCtx.getDisplay(overlay)
         opts    = display.getDisplayOpts()
-
-        # The overlay is being displayed as it is stored on
-        # disk - the image.getOrientation method calculates
-        # and returns labels for each voxelwise axis.
-        if opts.transform in ('pixdim', 'id'):
-            xorient = overlay.getVoxelOrientation(0)
-            yorient = overlay.getVoxelOrientation(1)
-            zorient = overlay.getVoxelOrientation(2)
-
-        # The overlay is being displayed in 'real world' space -
-        # the definition of this space may be present in the
-        # overlay meta data
-        else:
-            xorient = overlay.getWorldOrientation(0)
-            yorient = overlay.getWorldOrientation(1)
-            zorient = overlay.getWorldOrientation(2)
+        xform   = opts.getTransform('world', 'display')
+        xorient = overlay.getOrientation(0, xform)
+        yorient = overlay.getOrientation(1, xform)
+        zorient = overlay.getOrientation(2, xform)
 
     if constants.ORIENT_UNKNOWN in [xorient, yorient, zorient]:
         fgColour = 'red'
diff --git a/fsl/utils/dialog.py b/fsl/utils/dialog.py
index ec53c566a417f0247b094f7cdd24e0e30867d8bd..8cc91ac0b27cf11adc5f5a99277eb58e7940214f 100644
--- a/fsl/utils/dialog.py
+++ b/fsl/utils/dialog.py
@@ -132,12 +132,19 @@ class SimpleMessageDialog(wx.Dialog):
         self.SetClientSize((   width, height))
 
         self.Layout()
+        self.__message.Layout()
 
         if self.__style & SMD_KEEP_CENTERED:
             self.CentreOnParent()
 
+        # This ridiculousness seems to be
+        # necessary to force a repaint on
+        # all platforms (OSX, GTK, GTK/SSH)
+        wx.Yield()
         self.Refresh()
         self.Update()
+        self.__message.Refresh()
+        self.__message.Update() 
         wx.Yield()