Index: src/sas/qtgui/ColorMap.py
===================================================================
--- src/sas/qtgui/ColorMap.py	(revision 03c372db88a55844b6aa666b125e708bd0c77b0a)
+++ src/sas/qtgui/ColorMap.py	(revision 5d89f43266c67cb96a7bf395800ce4cce896c9f8)
@@ -13,4 +13,5 @@
 from sas.sasgui.guiframe.dataFitting import Data2D
 from sas.qtgui.GuiUtils import formatNumber
+from rangeSlider import RangeSlider
 
 DEFAULT_MAP = 'jet'
@@ -20,4 +21,5 @@
 
 class ColorMap(QtGui.QDialog, Ui_ColorMapUI):
+    apply_signal = QtCore.pyqtSignal(tuple, str)
     def __init__(self, parent=None, cmap=None, vmin=0.0, vmax=100.0, data=None):
         super(ColorMap, self).__init__()
@@ -32,6 +34,6 @@
         self.rmaps = sorted(set(self.all_maps) - set(self.maps))
 
-        self.vmin = vmin
-        self.vmax = vmax
+        self.vmin = self.vmin_orig = vmin
+        self.vmax = self.vmax_orig = vmax
 
         # Initialize detector labels
@@ -40,4 +42,6 @@
         # Initialize the combo box
         self.initMapCombobox()
+
+        self.initRangeSlider()
 
         # Add the color map component
@@ -52,4 +56,8 @@
         self.txtMaxAmplitude.setValidator(validator_max)
 
+        # Set the initial amplitudes
+        self.txtMinAmplitude.setText(formatNumber(self.vmin))
+        self.txtMaxAmplitude.setText(formatNumber(self.vmax))
+
         # Enforce constant size on the widget
         self.setFixedSize(self.minimumSizeHint())
@@ -61,6 +69,9 @@
         self.chkReverse.stateChanged.connect(self.onColorMapReversed)
 
-        # Handle the reset button click
+        # Handle the Reset button click
         self.buttonBox.button(QtGui.QDialogButtonBox.Reset).clicked.connect(self.onReset)
+
+        # Handle the Apply button click
+        self.buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.onApply)
 
         # Handle the amplitude setup
@@ -86,11 +97,24 @@
         # Go back to original settings
         self._cmap = self._cmap_orig
+        self.vmin = self.vmin_orig
+        self.vmax = self.vmax_orig
         self._norm = mpl.colors.Normalize(vmin=self.vmin, vmax=self.vmax)
-        self.txtMaxAmplitude.clear()
-        self.txtMinAmplitude.clear()
+        self.txtMinAmplitude.setText(formatNumber(self.vmin))
+        self.txtMaxAmplitude.setText(formatNumber(self.vmax))
         self.initMapCombobox()
+        self.slider.setMinimum(self.vmin)
+        self.slider.setMaximum(self.vmax)
+        self.slider.setLowValue(self.vmin)
+        self.slider.setHighValue(self.vmax)
         # Redraw the widget
         self.redrawColorBar()
         self.canvas.draw()
+
+    def onApply(self):
+        """
+        Respond to the Apply button click.
+        Send a signal to the plotter with vmin/vmax/cmap for chart update
+        """
+        self.apply_signal.emit(self.norm(), self.cmap())
 
     def initDetectorData(self):
@@ -125,4 +149,35 @@
         self.cbColorMap.setCurrentIndex(self.cbColorMap.findText(self._cmap))
 
+    def initRangeSlider(self):
+        """
+        Create and display the double slider for data range mapping.
+        """
+        self.slider = RangeSlider()
+        self.slider.setMinimum(self.vmin)
+        self.slider.setMaximum(self.vmax)
+        self.slider.setLowValue(self.vmin)
+        self.slider.setHighValue(self.vmax)
+        self.slider.setOrientation(QtCore.Qt.Horizontal)
+
+        self.slider_label = QtGui.QLabel()
+        self.slider_label.setText("Drag the sliders to adjust color range.")
+
+        def set_vmin(value):
+            self.vmin = value
+            self.txtMinAmplitude.setText(str(value))
+            self.updateMap()
+        def set_vmax(value):
+            self.vmax = value
+            self.txtMaxAmplitude.setText(str(value))
+            self.updateMap()
+
+        self.slider.lowValueChanged.connect(set_vmin)
+        self.slider.highValueChanged.connect(set_vmin)
+
+    def updateMap(self):
+        self._norm = mpl.colors.Normalize(vmin=self.vmin, vmax=self.vmax)
+        self.redrawColorBar()
+        self.canvas.draw()
+
     def initColorMap(self):
         """
@@ -131,9 +186,14 @@
         self.fig = mpl.figure.Figure(figsize=(4, 1))
         self.ax1 = self.fig.add_axes([0.05, 0.65, 0.9, 0.15])
+
         self._norm = mpl.colors.Normalize(vmin=self.vmin, vmax=self.vmax)
         self.redrawColorBar()
         self.canvas = FigureCanvas(self.fig)
+
         layout = QtGui.QVBoxLayout()
+        layout.addWidget(self.slider_label)
+        layout.addWidget(self.slider)
         layout.addWidget(self.canvas)
+
         self.widget.setLayout(layout)
 
@@ -154,5 +214,5 @@
                                             norm=self._norm,
                                             orientation='horizontal')
-        self.cb.set_label('Detector Colors')
+        self.cb.set_label('Color map range')
 
     def onColorMapReversed(self, isChecked):
@@ -177,5 +237,5 @@
 
         self._cmap = new_map
-        # Clearning the content of the combobox.
+        # Clear the content of the combobox.
         # Needs signal blocking, or else onMapIndexChange() spoils it all
         self.cbColorMap.blockSignals(True)
Index: src/sas/qtgui/Plotter2D.py
===================================================================
--- src/sas/qtgui/Plotter2D.py	(revision 03c372db88a55844b6aa666b125e708bd0c77b0a)
+++ src/sas/qtgui/Plotter2D.py	(revision 5d89f43266c67cb96a7bf395800ce4cce896c9f8)
@@ -29,4 +29,6 @@
         # Default scale
         self.scale = 'log_{10}'
+        self.vmin = None
+        self.vmax = None
 
     @property
@@ -208,10 +210,17 @@
                                     data=self.data)
 
+        color_map_dialog.apply_signal.connect(self.onApplyMap)
+
         if color_map_dialog.exec_() == QtGui.QDialog.Accepted:
-            self.cmap = color_map_dialog.cmap()
-            self.vmin, self.vmax = color_map_dialog.norm()
-            # Redraw the chart with new cmap
-            self.plot()
-        pass
+            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
+
+    def onApplyMap(self, v_values, cmap):
+        """
+        Update the chart color map based on data passed from the widget
+        """
+        self.cmap = str(cmap)
+        self.vmin, self.vmax = v_values
+        # Redraw the chart with new cmap
+        self.plot()
 
     def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
@@ -261,7 +270,12 @@
             self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
 
+            zmax_temp = self.zmax
+            if self.vmin is not None:
+                zmin_temp = self.vmin
+                zmax_temp = self.vmax
+
             im = self.ax.imshow(output, interpolation='nearest',
                                 origin='lower',
-                                vmin=zmin_temp, vmax=self.zmax,
+                                vmin=zmin_temp, vmax=zmax_temp,
                                 cmap=self.cmap,
                                 extent=(self.xmin, self.xmax,
@@ -301,5 +315,4 @@
 
             ax = Axes3D(self.figure)
-            #cbax = self.figure.add_axes([0.84, 0.1, 0.02, 0.8])
 
             # Disable rotation for large sets.
Index: src/sas/qtgui/UI/ColorMapUI.ui
===================================================================
--- src/sas/qtgui/UI/ColorMapUI.ui	(revision 092a3d9e371190b241becedfa1c1b6039185593a)
+++ src/sas/qtgui/UI/ColorMapUI.ui	(revision 5d89f43266c67cb96a7bf395800ce4cce896c9f8)
@@ -56,5 +56,5 @@
        </property>
        <property name="standardButtons">
-        <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset</set>
+        <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset</set>
        </property>
       </widget>
@@ -147,4 +147,7 @@
         <item row="0" column="0">
          <widget class="QLabel" name="label_4">
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Detector width.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
           <property name="text">
            <string>Width in pixels</string>
@@ -161,4 +164,7 @@
         <item row="1" column="0">
          <widget class="QLabel" name="label_5">
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Detector height.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
           <property name="text">
            <string>Height in pixels</string>
@@ -175,6 +181,9 @@
         <item row="2" column="0">
          <widget class="QLabel" name="label_6">
-          <property name="text">
-           <string>Q max</string>
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Maximum absolute Q&lt;span style=&quot; vertical-align:sub;&quot;&gt;x,y&lt;/span&gt; value.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+          <property name="text">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Q&lt;span style=&quot; vertical-align:sub;&quot;&gt;max&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
           </property>
          </widget>
@@ -189,4 +198,7 @@
         <item row="3" column="0">
          <widget class="QLabel" name="label_7">
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Minimum value of Q&lt;span style=&quot; vertical-align:sub;&quot;&gt;x&lt;/span&gt; in units of Q.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
           <property name="text">
            <string>Beam stop radius</string>
Index: src/sas/qtgui/UnitTesting/ColorMapTest.py
===================================================================
--- src/sas/qtgui/UnitTesting/ColorMapTest.py	(revision 03c372db88a55844b6aa666b125e708bd0c77b0a)
+++ src/sas/qtgui/UnitTesting/ColorMapTest.py	(revision 5d89f43266c67cb96a7bf395800ce4cce896c9f8)
@@ -13,4 +13,5 @@
 import sas.qtgui.Plotter2D as Plotter2D
 from UnitTesting.TestUtils import WarningTestNotImplemented
+from UnitTesting.TestUtils import QtSignalSpy
 
 # Local
@@ -65,4 +66,9 @@
         self.assertIsInstance(self.widget.txtMaxAmplitude.validator(), QtGui.QDoubleValidator)
 
+        # Ranges
+        self.assertEqual(self.widget.txtMinAmplitude.text(), "0")
+        self.assertEqual(self.widget.txtMaxAmplitude.text(), "100")
+        self.assertIsInstance(self.widget.slider, QtGui.QSlider)
+
     def testOnReset(self):
         '''Check the dialog reset function'''
@@ -78,5 +84,22 @@
         self.assertEqual(self.widget.cbColorMap.currentIndex(), 20)
         self.assertFalse(self.widget.chkReverse.isChecked())
-        self.assertEqual(self.widget.txtMinAmplitude.text(), "")
+        self.assertEqual(self.widget.txtMinAmplitude.text(), "0")
+
+    def testOnApply(self):
+        '''Check the dialog apply function'''
+        # Set some controls to non-default state
+        self.widget.show()
+        self.widget.cbColorMap.setCurrentIndex(20) # PuRd_r
+        self.widget.chkReverse.setChecked(True)
+        self.widget.txtMinAmplitude.setText("20.0")
+
+        spy_apply = QtSignalSpy(self.widget, self.widget.apply_signal)
+        # Reset the widget state
+        self.widget.onApply()
+
+        # Assure the widget is still up and the signal was sent.
+        self.assertTrue(self.widget.isVisible())
+        self.assertEqual(spy_apply.count(), 1)
+        self.assertIn('PuRd_r', spy_apply.called()[0]['args'][1])
 
     def testInitMapCombobox(self):
@@ -97,4 +120,24 @@
         self.assertTrue(self.widget.chkReverse.isChecked())
 
+    def testInitRangeSlider(self):
+        '''Test the range slider initializer'''
+        # Set a color map from the direct list
+        self.widget._cmap = "gnuplot"
+        self.widget.initRangeSlider()
+
+        # Check the values
+        self.assertEqual(self.widget.slider.minimum(), 0)
+        self.assertEqual(self.widget.slider.maximum(), 100)
+        self.assertEqual(self.widget.slider.orientation(), 1)
+
+        # Emit new low value
+        self.widget.slider.lowValueChanged.emit(5)
+        # Assure the widget received changes
+        self.assertEqual(self.widget.txtMinAmplitude.text(), "5")
+
+        # Emit new high value
+        self.widget.slider.highValueChanged.emit(45)
+        # Assure the widget received changes
+        self.assertEqual(self.widget.txtMinAmplitude.text(), "45")
 
     def testOnMapIndexChange(self):
Index: src/sas/qtgui/rangeSlider.py
===================================================================
--- src/sas/qtgui/rangeSlider.py	(revision 5d89f43266c67cb96a7bf395800ce4cce896c9f8)
+++ src/sas/qtgui/rangeSlider.py	(revision 5d89f43266c67cb96a7bf395800ce4cce896c9f8)
@@ -0,0 +1,294 @@
+# -*- coding: utf-8 -*-
+
+"""
+/***************************************************************************
+Name                 : RangeSlider
+Description          : A slider for ranges
+Date                 : Jun 20, 2012 
+copyright            : (C) 2012 by Giuseppe Sucameli
+email                : brush.tyler@gmail.com
+
+the code is based on RangeSlider by phil
+(see https://svn.enthought.com/enthought/browser/TraitsBackendQt/trunk/enthought/traits/ui/qt4/extra/range_slider.py) licensed under GPLv2
+ ***************************************************************************/
+
+/***************************************************************************
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ *                                                                         *
+ ***************************************************************************/
+"""
+
+from PyQt4 import QtGui, QtCore
+
+class RangeSlider(QtGui.QSlider):
+    """ A slider for ranges.
+    
+        This class provides a dual-slider for ranges, where there is a defined
+        maximum and minimum, as is a normal slider, but instead of having a
+        single slider value, there are 2 slider values.
+        
+        This class emits the same signals as the QSlider base class, with the 
+        exception of valueChanged. 
+
+        In addition, two new signals are emitted to catch the movement of 
+        each handle, lowValueChanged(int) and highValueChanged(int).
+    """
+    highValueChanged = QtCore.pyqtSignal(int)
+    lowValueChanged = QtCore.pyqtSignal(int)
+    def __init__(self, *args):
+        super(RangeSlider, self).__init__(*args)
+
+        self._low = self.minimum()
+        self._high = self.maximum()
+        
+        self.pressed_control = QtGui.QStyle.SC_None
+        self.hover_control = QtGui.QStyle.SC_None
+        self.click_offset = 0
+        
+        # -1 for the low, 1 for the high, 0 for both
+        self.active_slider = -1
+
+    def lowValue(self):
+        return self._low
+
+    def setLowValue(self, low):
+        if low < self.minimum():
+            low = self.minimum()
+        if low == self._low:
+            return
+
+        self._low = low
+        self.update()
+
+        if self.hasTracking():
+            self.lowValueChanged.emit(self._low)
+
+    def highValue(self):
+        return self._high
+
+    def setHighValue(self, high):
+        if high > self.maximum():
+            high = self.maximum()
+        if high == self._high:
+            return
+
+        self._high = high
+        self.update()
+
+        if self.hasTracking():
+            self.lowValueChanged.emit(self._high)
+        
+    def paintEvent(self, event):
+        # based on http://qt.gitorious.org/qt/qt/blobs/master/src/gui/widgets/qslider.cpp
+
+        painter = QtGui.QPainter(self)
+        style = QtGui.QApplication.style() 
+
+        for i, value in enumerate([self._low, self._high]):
+            opt = QtGui.QStyleOptionSlider()
+            self.initStyleOption(opt)
+
+            gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self)
+            sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self)
+
+            if self.orientation() == QtCore.Qt.Horizontal:
+                handle_length = sr.width()
+                slider_length = gr.width()
+                slider_min = gr.x()
+                slider_max = gr.right() - handle_length + 1
+            else:
+                handle_length = sr.height()
+                slider_length = gr.height()
+                slider_min = gr.y()
+                slider_max = gr.bottom() - handle_length + 1
+
+            opt.subControls = QtGui.QStyle.SC_SliderGroove | QtGui.QStyle.SC_SliderHandle
+
+            # draw the first slider with inverted appearance, then the second 
+            # slider drawn on top of the existing ones
+            if i == 0:
+                if self.orientation() == QtCore.Qt.Horizontal:
+                    opt.upsideDown = not self.invertedAppearance()
+                else:
+                    opt.upsideDown = self.invertedAppearance()
+
+                if self.orientation() == QtCore.Qt.Horizontal:
+                    opt.sliderValue = value - self.minimum()
+                    opt.sliderPosition = self.minimum() + (self.maximum() - value)
+
+                    opt.rect.setX(slider_min)
+                    opt.rect.setWidth(slider_length)
+
+                else:
+                    opt.rect.setY(slider_min)
+                    opt.rect.setHeight(slider_length)
+
+            else:
+                # do not highlight the second part when has focus to avoid 
+                # drawing of partially overlapped semi-transparent backgrounds
+                opt.state &= ~QtGui.QStyle.State_HasFocus
+
+                opt.sliderValue = 0
+                opt.sliderPosition = self.minimum()
+
+                if self.orientation() == QtCore.Qt.Horizontal:
+                    opt.upsideDown = self.invertedAppearance()
+
+                    pos = style.sliderPositionFromValue(0, self.maximum()-self.minimum(),
+                                             value-self.minimum(), slider_max - slider_min - 4,
+                                             opt.upsideDown)
+
+                    opt.rect.setX(pos + 3)
+                    opt.rect.setWidth(slider_max - pos + handle_length - 3)
+
+                else:
+                    opt.upsideDown = not self.invertedAppearance()
+
+                    pos = style.sliderPositionFromValue(0, self.maximum()-self.minimum(),
+                                             self.maximum()-value, slider_max - slider_min - 4,
+                                             opt.upsideDown)
+
+                    opt.rect.setY(slider_min-1)
+                    opt.rect.setHeight(slider_min + slider_length - pos - 2)
+
+            if self.tickPosition() != self.NoTicks:
+                opt.subControls |= QtGui.QStyle.SC_SliderTickmarks
+
+            if self.pressed_control:
+                opt.activeSubControls = self.pressed_control
+                opt.state |= QtGui.QStyle.State_Sunken
+            else:
+                opt.activeSubControls = self.hover_control
+
+            style.drawComplexControl(QtGui.QStyle.CC_Slider, opt, painter, self)
+            
+        
+    def mousePressEvent(self, event):
+        event.accept()
+        
+        style = QtGui.QApplication.style()
+        button = event.button()
+        
+        # In a normal slider control, when the user clicks on a point in the 
+        # slider's total range, but not on the slider part of the control the
+        # control would jump the slider value to where the user clicked.
+        # For this control, clicks which are not direct hits will slide both
+        # slider parts
+                
+        if button:
+            opt = QtGui.QStyleOptionSlider()
+            self.initStyleOption(opt)
+
+            self.active_slider = 0
+            mid = (self.maximum() - self.minimum()) / 2.0
+            
+            for i, value in enumerate([self._low, self._high]):
+                opt.sliderPosition = value
+                hit = style.hitTestComplexControl(style.CC_Slider, opt, event.pos(), self)
+
+                if hit == style.SC_SliderHandle:
+                    self.pressed_control = hit
+
+                    # if both handles are close together, avoid locks 
+                    # choosing the one with more empty space near it
+                    if self._low + 2 >= self._high:
+                        self.active_slider = 1 if self._high < mid else -1
+                    else:
+                        self.active_slider = -1 if i == 0 else 1
+                    self.triggerAction(self.SliderMove)
+                    self.setRepeatAction(self.SliderNoAction)
+                    self.setSliderDown(True)
+                    break
+
+            if self.active_slider == 0:
+                self.pressed_control = QtGui.QStyle.SC_SliderHandle
+                self.click_offset = self.__pixelPosToRangeValue(self.__pick(event.pos()))
+                self.triggerAction(self.SliderMove)
+                self.setRepeatAction(self.SliderNoAction)
+                self.setSliderDown(True)
+        else:
+            event.ignore()
+
+    def mouseReleaseEvent(self, event):
+        if self.pressed_control != QtGui.QStyle.SC_SliderHandle:
+            event.ignore()
+            return
+
+        self.setSliderDown(False)
+        return QtGui.QSlider.mouseReleaseEvent(self, event)
+                                
+    def mouseMoveEvent(self, event):
+        if self.pressed_control != QtGui.QStyle.SC_SliderHandle:
+            event.ignore()
+            return
+        
+        event.accept()
+        new_pos = self.__pixelPosToRangeValue(self.__pick(event.pos()))
+        opt = QtGui.QStyleOptionSlider()
+        self.initStyleOption(opt)
+
+        old_click_offset = self.click_offset
+        self.click_offset = new_pos
+        
+        if self.active_slider == 0:
+            offset = new_pos - old_click_offset
+            new_low = self._low + offset
+            new_high = self._high + offset
+
+            if new_low < self.minimum():
+                diff = self.minimum() - new_low
+                new_low += diff
+                new_high += diff
+            if new_high > self.maximum():
+                diff = self.maximum() - new_high
+                new_low += diff
+                new_high += diff
+
+            self.setLowValue( new_low )
+            self.setHighValue( new_high )
+
+        elif self.active_slider < 0:
+            if new_pos > self._high:
+                new_pos = self._high
+            self.setLowValue( new_pos )
+
+        else:
+            if new_pos < self._low:
+                new_pos = self._low
+            self.setHighValue( new_pos )
+
+        if self.hasTracking():
+            self.emit(QtCore.SIGNAL('sliderMoved(int)'), new_pos)
+
+            
+    def __pick(self, pt):
+        if self.orientation() == QtCore.Qt.Horizontal:
+            return pt.x()
+        else:
+            return pt.y()
+           
+           
+    def __pixelPosToRangeValue(self, pos):
+        opt = QtGui.QStyleOptionSlider()
+        self.initStyleOption(opt)
+        style = QtGui.QApplication.style()
+        
+        gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self)
+        sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self)
+        
+        if self.orientation() == QtCore.Qt.Horizontal:
+            handle_length = sr.width()
+            slider_min = gr.x() + handle_length/2
+            slider_max = gr.right() - handle_length/2 + 1
+        else:
+            handle_length = sr.height()
+            slider_min = gr.y() + handle_length/2
+            slider_max = gr.bottom() - handle_length/2 + 1
+            
+        return self.minimum() + style.sliderValueFromPosition(0, self.maximum()-self.minimum(),
+                                            pos-slider_min, slider_max - slider_min,
+                                            opt.upsideDown)
