diff --git a/doc/source/colormap.rst b/doc/source/colormap.rst
index 86ffe4a2..d4f93118 100644
--- a/doc/source/colormap.rst
+++ b/doc/source/colormap.rst
@@ -1,8 +1,81 @@
-ColorMap
-========
+Color Maps
+==========
+
+A color map defines a relationship between scalar data values and a range of colors. Color maps are
+commonly used to generate false color images, color scatter-plot points, and illustrate the height
+of surface plots.
+
+PyQtGraph's :class:`~pyqtgraph.ColorMap` can conveniently be applied to images and interactively
+adjusted by using :class:`~pyqtgraph.ColorBarItem`.
+To provide interactively user-defined color mappings, see
+:class:`~pyqtgraph.GradientEditorItem` and :class:`~pyqtgraph.GradientWidget`, which wraps it.
+:class:`~pyqtgraph.GradientEditorItem` combines the editing with a histogram and controls for
+interactively adjusting image levels.
+
+ColorMap can also be used a convenient source of colors from a consistent palette or to generate
+QPen and QBrush objects used to draw lines and fills that are colored according to their values
+along the horizontal or vertical axis.
+
+
+Sources for color maps
+----------------------
+
+Color maps can be user defined by assigning a number of *stops* over the range of 0 to 1. A color
+is given for each stop, and the in-between values are generated by interpolation.
+
+When map colors directly represent values, an improperly designed map can obscure detail over
+certain ranges of values, while creating false detail in others. PyQtGraph includes the
+perceptually uniform color maps provided by the
+`Colorcet project `_. Color maps can also be imported from the
+``colorcet`` library or from ``matplotlib``, if either of these is installed.
+
+To see all available color maps, please run the `ColorMap` demonstration available in the suite of
+:ref:`examples`.
+
+Examples
+--------
+
+False color display of a 2D data set. Display levels are controlled by
+a :class:`ColorBarItem `:
+
+.. literalinclude:: images/gen_example_false_color_image.py
+ :lines: 18-28
+ :dedent: 8
+
+Using QtGui.QPen and QtGui.QBrush to color plots according to the plotted value:
+
+.. literalinclude:: images/gen_example_gradient_plot.py
+ :lines: 16-33
+ :dedent: 8
+
+.. image::
+ images/example_false_color_image.png
+ :width: 49%
+ :alt: Example of a false color image
+
+.. image::
+ images/example_gradient_plot.png
+ :width: 49%
+ :alt: Example of drawing and filling plots with gradients
+
+The use of color maps is also demonstrated in the `ImageView`, `Color Gradient Plots` and `ColorBarItem`
+:ref:`examples`.
+
+
+API Reference
+-------------
+
+.. autofunction:: pyqtgraph.colormap.listMaps
+
+.. autofunction:: pyqtgraph.colormap.get
+
+.. autofunction:: pyqtgraph.colormap.getFromMatplotlib
+
+.. autofunction:: pyqtgraph.colormap.getFromColorcet
+
+
.. autoclass:: pyqtgraph.ColorMap
:members:
.. automethod:: pyqtgraph.ColorMap.__init__
-
diff --git a/doc/source/exporting.rst b/doc/source/exporting.rst
index 0bb1c82a..502bd8e9 100644
--- a/doc/source/exporting.rst
+++ b/doc/source/exporting.rst
@@ -41,23 +41,33 @@ Export Formats
Exporting from the API
----------------------
-To export a file programatically, follow this example::
+To export a file programatically, follow this example:
- import pyqtgraph as pg
- import pyqtgraph.exporters
+.. code-block:: python
+
+ import pyqtgraph as pg
+ import pyqtgraph.exporters
- # generate something to export
- plt = pg.plot([1,5,2,4,3])
+ # generate something to export
+ plt = pg.plot([1,5,2,4,3])
- # create an exporter instance, as an argument give it
- # the item you wish to export
- exporter = pg.exporters.ImageExporter(plt.plotItem)
+ # create an exporter instance, as an argument give it
+ # the item you wish to export
+ exporter = pg.exporters.ImageExporter(plt.plotItem)
- # set export parameters if needed
- exporter.parameters()['width'] = 100 # (note this also affects height parameter)
+ # set export parameters if needed
+ exporter.parameters()['width'] = 100 # (note this also affects height parameter)
- # save to file
- exporter.export('fileName.png')
+ # save to file
+ exporter.export('fileName.png')
+
+To export the overall layout of a GraphicsLayoutWidget `grl`, the exporter initialization is
+
+.. code-block:: python
+
+ exporter = pg.exporters.ImageExporter( grl.scene() )
+
+instead.
Exporting 3D Graphics
@@ -69,5 +79,3 @@ generate an image from a GLViewWidget by using QGLWidget.grabFrameBuffer or QGLW
glview.grabFrameBuffer().save('fileName.png')
See the Qt documentation for more information.
-
-
diff --git a/doc/source/graphicsItems/colorbaritem.rst b/doc/source/graphicsItems/colorbaritem.rst
new file mode 100644
index 00000000..e4ec490b
--- /dev/null
+++ b/doc/source/graphicsItems/colorbaritem.rst
@@ -0,0 +1,7 @@
+ColorBarItem
+============
+
+.. autoclass:: pyqtgraph.ColorBarItem
+ :members:
+
+ .. automethod:: pyqtgraph.ColorBarItem.__init__
diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst
index 54bf12f2..7b497ac3 100644
--- a/doc/source/graphicsItems/index.rst
+++ b/doc/source/graphicsItems/index.rst
@@ -12,6 +12,7 @@ Contents:
plotdataitem
plotitem
imageitem
+ colorbaritem
pcolormeshitem
graphitem
viewbox
diff --git a/doc/source/images/example_false_color_image.png b/doc/source/images/example_false_color_image.png
new file mode 100644
index 00000000..971d58d3
Binary files /dev/null and b/doc/source/images/example_false_color_image.png differ
diff --git a/doc/source/images/example_gradient_plot.png b/doc/source/images/example_gradient_plot.png
new file mode 100644
index 00000000..f5c87350
Binary files /dev/null and b/doc/source/images/example_gradient_plot.png differ
diff --git a/doc/source/images/gen_example_false_color_image.py b/doc/source/images/gen_example_false_color_image.py
new file mode 100644
index 00000000..22e6e006
--- /dev/null
+++ b/doc/source/images/gen_example_false_color_image.py
@@ -0,0 +1,45 @@
+"""
+generates 'example_false_color_image.png'
+"""
+import numpy as np
+import pyqtgraph as pg
+import pyqtgraph.exporters as exp
+from pyqtgraph.Qt import QtCore, QtGui, QtWidgets, mkQApp
+
+class MainWindow(pg.GraphicsLayoutWidget):
+ """ example application main window """
+ def __init__(self):
+ super().__init__()
+ self.resize(420,400)
+ self.show()
+
+ plot = self.addPlot() # title="non-interactive")
+
+ # prepare demonstration data:
+ data = np.fromfunction(lambda i, j: (1+0.3*np.sin(i)) * (i)**2 + (j)**2, (100, 100))
+ noisy_data = data * (1 + 0.2 * np.random.random(data.shape) )
+
+ # Example: False color image with interactive level adjustment
+ img = pg.ImageItem(image=noisy_data) # create monochrome image from demonstration data
+ plot.addItem( img ) # add to PlotItem 'plot'
+ cm = pg.colormap.get('CET-L9') # prepare a linear color map
+ bar = pg.ColorBarItem( values= (0, 20_000), cmap=cm ) # prepare interactive color bar
+ # Have ColorBarItem control colors of img and appear in 'plot':
+ bar.setImageItem( img, insert_in=plot )
+
+ self.timer = pg.QtCore.QTimer( singleShot=True )
+ self.timer.timeout.connect(self.export)
+ self.timer.start(100)
+
+ def export(self):
+ print('exporting')
+ exporter = exp.ImageExporter(self.scene())
+ exporter.parameters()['width'] = 420
+ exporter.export('example_false_color_image.png')
+
+mkQApp("False color image example")
+main_window = MainWindow()
+
+## Start Qt event loop
+if __name__ == '__main__':
+ mkQApp().exec_()
diff --git a/doc/source/images/gen_example_gradient_plot.py b/doc/source/images/gen_example_gradient_plot.py
new file mode 100644
index 00000000..be5c8047
--- /dev/null
+++ b/doc/source/images/gen_example_gradient_plot.py
@@ -0,0 +1,55 @@
+"""
+generates 'example_gradient_plot.png'
+"""
+import numpy as np
+import pyqtgraph as pg
+import pyqtgraph.exporters as exp
+from pyqtgraph.Qt import QtCore, QtGui, QtWidgets, mkQApp
+
+class MainWindow(pg.GraphicsLayoutWidget):
+ """ example application main window """
+ def __init__(self):
+ super().__init__()
+ self.resize(420,400)
+ self.show()
+
+ # Prepare demonstration data
+ raw = np.linspace(0.0, 2.0, 400)
+ y_data1 = ( (raw+0.1)%1 ) ** 4
+ y_data2 = ( (raw+0.1)%1 ) ** 4 - ( (raw+0.6)%1 ) ** 4
+
+ # Example 1: Gradient pen
+ cm = pg.colormap.get('CET-L17') # prepare a linear color map
+ cm.reverse() # reverse it to put light colors at the top
+ pen = cm.getPen( span=(0.0,1.0), width=5 ) # gradient from blue (y=0) to white (y=1)
+ # plot a curve drawn with a pen colored according to y value:
+ curve1 = pg.PlotDataItem( y=y_data1, pen=pen )
+
+ # Example 2: Gradient brush
+ cm = pg.colormap.get('CET-D1') # prepare a diverging color map
+ cm.setMappingMode('diverging') # set mapping mode
+ brush = cm.getBrush( span=(-1., 1.) ) # gradient from blue at -1 to red at +1
+ # plot a curve that is filled to zero with the gradient brush:
+ curve2 = pg.PlotDataItem( y=y_data2, pen='w', brush=brush, fillLevel=0.0 )
+
+ for idx, curve in enumerate( (curve1, curve2) ):
+ plot = self.addPlot(row=idx, col=0)
+ plot.getAxis('left').setWidth(25)
+ plot.addItem( curve )
+
+ self.timer = pg.QtCore.QTimer( singleShot=True )
+ self.timer.timeout.connect(self.export)
+ self.timer.start(100)
+
+ def export(self):
+ print('exporting')
+ exporter = exp.ImageExporter(self.scene())
+ exporter.parameters()['width'] = 420
+ exporter.export('example_gradient_plot.png')
+
+mkQApp("Gradient plotting example")
+main_window = MainWindow()
+
+## Start Qt event loop
+if __name__ == '__main__':
+ mkQApp().exec_()
diff --git a/examples/ColorGradientPlots.py b/examples/ColorGradientPlots.py
new file mode 100644
index 00000000..2a46009d
--- /dev/null
+++ b/examples/ColorGradientPlots.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+"""
+This example demonstrates plotting with color gradients.
+It also shows multiple plots with timed rolling updates
+"""
+# Add path to library (just for examples; you do not need this)
+import initExample
+
+import numpy as np
+import time
+from pyqtgraph.Qt import QtCore, QtGui, QtWidgets, mkQApp
+import pyqtgraph as pg
+
+class DataSource(object):
+ """ source of buffered demonstration data """
+ def __init__(self, sample_rate=200., signal_period=0.55, negative_period=None, max_length=300):
+ """ prepare, but don't start yet """
+ self.rate = sample_rate
+ self.period = signal_period
+ self.neg_period = negative_period
+ self.start_time = 0.
+ self.sample_idx = 0 # number of next sample to be taken
+
+ def start(self, timestamp):
+ """ start acquiring simulated data """
+ self.start_time = timestamp
+ self.sample_idx = 0
+
+ def get_data(self, timestamp, max_length=6000):
+ """ return all data acquired since last get_data call """
+ next_idx = int( (timestamp - self.start_time) * self.rate )
+ if next_idx - self.sample_idx > max_length:
+ self.sample_idx = next_idx - max_length # catch up if needed
+ # create some mildly intersting data:
+ sample_phases = np.arange( self.sample_idx, next_idx, dtype=np.float64 )
+ self.sample_idx = next_idx
+
+ sample_phase_pos = sample_phases / (self.period*self.rate)
+ sample_phase_pos %= 1.0
+ if self.neg_period is None:
+ return sample_phase_pos**4
+ sample_phase_neg = sample_phases / (self.neg_period*self.rate)
+ sample_phase_neg %= 1.0
+ return sample_phase_pos**4 - sample_phase_neg**4
+
+class MainWindow(pg.GraphicsLayoutWidget):
+ """ example application main window """
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle('pyqtgraph example: gradient plots')
+ self.resize(800,800)
+ self.show()
+
+ layout = self # we are using a GraphicsLayoutWidget as main window for convenience
+ cm = pg.colormap.get('CET-L17')
+ cm.reverse()
+ pen0 = cm.getPen( span=(0.0,1.0), width=5 )
+ curve0 = pg.PlotDataItem(pen=pen0 )
+ comment0 = 'Clipped color map applied to vertical axis'
+
+ cm = pg.colormap.get('CET-D1')
+ cm.setMappingMode('diverging')
+ brush = cm.getBrush( span=(-1., 1.), orientation='vertical' )
+ curve1 = pg.PlotDataItem(pen='w', brush=brush, fillLevel=0.0 )
+ comment1 = 'Diverging vertical color map used as brush'
+
+ cm = pg.colormap.get('CET-L17')
+ cm.setMappingMode('mirror')
+ pen2 = cm.getPen( span=(400.0,600.0), width=5, orientation='horizontal' )
+ curve2 = pg.PlotDataItem(pen=pen2 )
+ comment2 = 'Mirrored color map applied to horizontal axis'
+
+ cm = pg.colormap.get('CET-C2')
+ cm.setMappingMode('repeat')
+ pen3 = cm.getPen( span=(100, 200), width=5, orientation='horizontal' )
+ curve3 = pg.PlotDataItem(pen=pen3 ) # vertical diverging fill
+ comment3 = 'Repeated color map applied to horizontal axis'
+
+ curves = (curve0, curve1, curve2, curve3)
+ comments = (comment0, comment1, comment2, comment3)
+
+ length = int( 3.0 * 200. ) # length of display in samples
+ self.top_plot = None
+ for idx, (curve, comment) in enumerate( zip(curves,comments) ):
+ plot = layout.addPlot(row=idx+1, col=0)
+ text = pg.TextItem( comment, anchor=(0,1) )
+ text.setPos(0.,1.)
+ if self.top_plot is None:
+ self.top_plot = plot
+ else:
+ plot.setXLink( self.top_plot )
+ plot.addItem( curve )
+ plot.addItem( text )
+ plot.setXRange( 0, length )
+ if idx != 1: plot.setYRange( 0. , 1.1 )
+ else : plot.setYRange( -1. , 1.2 ) # last plot include positive/negative data
+
+ self.traces = (
+ {'crv': curve0, 'buf': np.zeros( length ), 'ptr':0, 'ds': DataSource( signal_period=0.55 ) },
+ {'crv': curve1, 'buf': np.zeros( length ), 'ptr':0, 'ds': DataSource( signal_period=0.61, negative_period=0.55 ) },
+ {'crv': curve2, 'buf': np.zeros( length ), 'ptr':0, 'ds': DataSource( signal_period=0.65 ) },
+ {'crv': curve3, 'buf': np.zeros( length ), 'ptr':0, 'ds': DataSource( signal_period=0.52 ) },
+ )
+ self.timer = QtCore.QTimer(timerType=QtCore.Qt.PreciseTimer)
+ self.timer.timeout.connect(self.update)
+ timestamp = time.perf_counter()
+ for dic in self.traces:
+ dic['ds'].start( timestamp )
+ self.last_update = time.perf_counter()
+ self.mean_dt = None
+ self.timer.start(33)
+
+ def update(self):
+ """ called by timer at 30 Hz """
+ timestamp = time.perf_counter()
+ # measure actual update rate:
+ dt = timestamp - self.last_update
+ if self.mean_dt is None:
+ self.mean_dt = dt
+ else:
+ self.mean_dt = 0.95 * self.mean_dt + 0.05 * dt # average over fluctuating measurements
+ self.top_plot.setTitle(
+ 'refresh: {:0.1f}ms -> {:0.1f} fps'.format( 1000*self.mean_dt, 1/self.mean_dt )
+ )
+ # handle rolling buffer:
+ self.last_update = timestamp
+ for dic in self.traces:
+ new_data = dic['ds'].get_data( timestamp )
+ idx_a = dic['ptr']
+ idx_b = idx_a + len( new_data )
+ len_buffer = dic['buf'].shape[0]
+ if idx_b < len_buffer: # data does not cross buffer boundary
+ dic['buf'][idx_a:idx_b] = new_data
+ else: # part of the new data needs to roll over to beginning of buffer
+ len_1 = len_buffer - idx_a # this many elements still fit
+ dic['buf'][idx_a:idx_a+len_1] = new_data[:len_1] # first part of data at end
+ idx_b = len(new_data) - len_1
+ dic['buf'][0:idx_b] = new_data[len_1:] # second part of data at re-start
+ dic['ptr'] = idx_b
+ dic['crv'].setData( dic['buf'] )
+
+mkQApp("Gradient plotting example")
+main_window = MainWindow()
+
+## Start Qt event loop
+if __name__ == '__main__':
+ mkQApp().exec_()
diff --git a/examples/utils.py b/examples/utils.py
index 65f764d6..346c6bca 100644
--- a/examples/utils.py
+++ b/examples/utils.py
@@ -13,6 +13,7 @@ examples = OrderedDict([
('Timestamps on x axis', 'DateAxisItem.py'),
('Image Analysis', 'imageAnalysis.py'),
('Color Maps', 'colorMaps.py'),
+ ('Color Gradient Plots', 'ColorGradientPlots.py'),
('ViewBox Features', Namespace(filename='ViewBoxFeatures.py', recommended=True)),
('Dock widgets', 'dockarea.py'),
('Console', 'ConsoleWidget.py'),
diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py
index 3c6c0ada..d508a8a0 100644
--- a/pyqtgraph/colormap.py
+++ b/pyqtgraph/colormap.py
@@ -9,15 +9,22 @@ _mapCache = {}
def listMaps(source=None):
"""
- Warning, highly experimental, subject to change.
+ .. warning:: Experimental, subject to change.
- List available color maps
- =============== =================================================================
- **Arguments:**
- source 'matplotlib' lists maps that can be imported from MatPlotLib
- 'colorcet' lists maps that can be imported from ColorCET
- otherwise local maps are listed
- =============== =================================================================
+ List available color maps.
+
+ Parameters
+ ----------
+ source: str, optional
+ Color map source. If omitted, locally stored maps are listed. Otherwise
+
+ - 'matplotlib' lists maps that can be imported from Matplotlib
+ - 'colorcet' lists maps that can be imported from ColorCET
+
+ Returns
+ -------
+ list of str
+ Known color map names.
"""
if source is None:
pathname = path.join(path.dirname(__file__), 'colors','maps')
@@ -32,7 +39,7 @@ def listMaps(source=None):
import matplotlib.pyplot as mpl_plt
list_of_maps = mpl_plt.colormaps()
return list_of_maps
- except ModuleNotFoundError:
+ except ModuleNotFoundError:
return []
elif source.lower() == 'colorcet':
try:
@@ -40,23 +47,34 @@ def listMaps(source=None):
list_of_maps = list( colorcet.palette.keys() )
list_of_maps.sort()
return list_of_maps
- except ModuleNotFoundError:
+ except ModuleNotFoundError:
return []
- return []
+ return []
def get(name, source=None, skipCache=False):
"""
- Warning, highly experimental, subject to change.
+ .. warning:: Experimental, subject to change.
- Returns a ColorMap object from a local definition or imported from another library
- =============== =================================================================
- **Arguments:**
- name Name of color map. Can be a path to a defining file.
- source 'matplotlib' imports a map defined by Matplotlib
- 'colorcet' imports a maps defined by ColorCET
- otherwise local data is used
- =============== =================================================================
+ Returns a ColorMap object from a local definition or imported from another library.
+ The generated ColorMap objects are cached for fast repeated access.
+
+ Parameters
+ ----------
+ name: str
+ Name of color map. In addition to the included maps, this can also
+ be a path to a file in the local folder. See the files in the
+ ``pyqtgraph/colors/maps/`` folder for examples of the format.
+ source: str, optional
+ If omitted, a locally stored map is returned. Otherwise
+
+ - 'matplotlib' imports a map defined by Matplotlib.
+ - 'colorcet' imports a map defined by ColorCET.
+
+ skipCache: bool, optional
+ If `skipCache=True`, the internal cache is skipped and a new
+ ColorMap object is generated. This can load an unaltered copy
+ when the previous ColorMap object has been modified.
"""
if not skipCache and name in _mapCache:
return _mapCache[name]
@@ -95,7 +113,7 @@ def _getFromFile(name):
color_tuple = tuple( [ int(255*float(c)+0.5) for c in comp ] )
else:
hex_str = parts[0]
- if hex_str[0] == '#':
+ if hex_str[0] == '#':
hex_str = hex_str[1:] # strip leading #
if len(hex_str) < 3: continue # not enough information
if len(hex_str) == 3: # parse as abbreviated RGB
@@ -109,13 +127,16 @@ def _getFromFile(name):
# end of line reading loop
# end of open
cm = ColorMap(
- pos=np.linspace(0.0, 1.0, len(color_list)),
+ pos=np.linspace(0.0, 1.0, len(color_list)),
color=color_list) #, names=color_names)
_mapCache[name] = cm
return cm
def getFromMatplotlib(name):
- """ import colormap from matplotlib definition """
+ """
+ Generates a ColorMap object from a Matplotlib definition.
+ Same as ``colormap.get(name, source='matplotlib')``.
+ """
# inspired and informed by "mpl_cmaps_in_ImageItem.py", published by Sebastian Hoefer at
# https://github.com/honkomonk/pyqtgraph_sandbox/blob/master/mpl_cmaps_in_ImageItem.py
try:
@@ -147,7 +168,7 @@ def getFromMatplotlib(name):
col_data[:,-1] = np.linspace(0., 1., 64)
for idx, key in enumerate(['red','green','blue']):
col_data[:,idx] = np.clip( data[key](col_data[:,-1]), 0, 1)
- cm = ColorMap(pos=col_data[:,-1], color=255*col_data[:,:3]+0.5)
+ cm = ColorMap(pos=col_data[:,-1], color=255*col_data[:,:3]+0.5)
elif hasattr(col_map, 'colors'): # handle ListedColormap
col_data = np.array(col_map.colors)
cm = ColorMap(pos=np.linspace(0.0, 1.0, col_data.shape[0]), color=255*col_data[:,:3]+0.5 )
@@ -156,7 +177,7 @@ def getFromMatplotlib(name):
return cm
def getFromColorcet(name):
- """ import colormap from colorcet definition """
+ """ Generates a ColorMap object from a colorcet definition. Same as ``colormap.get(name, source='colorcet')``. """
try:
import colorcet
except ModuleNotFoundError:
@@ -165,14 +186,14 @@ def getFromColorcet(name):
color_list = []
for hex_str in color_strings:
if hex_str[0] != '#': continue
- if len(hex_str) != 7:
+ if len(hex_str) != 7:
raise ValueError('Invalid color string '+str(hex_str)+' in colorcet import.')
color_tuple = tuple( bytes.fromhex( hex_str[1:] ) )
color_list.append( color_tuple )
- if len(color_list) == 0:
+ if len(color_list) == 0:
return None
cm = ColorMap(
- pos=np.linspace(0.0, 1.0, len(color_list)),
+ pos=np.linspace(0.0, 1.0, len(color_list)),
color=color_list) #, names=color_names)
_mapCache[name] = cm
return cm
@@ -180,22 +201,20 @@ def getFromColorcet(name):
class ColorMap(object):
"""
- A ColorMap defines a relationship between a scalar value and a range of colors.
- ColorMaps are commonly used for false-coloring monochromatic images, coloring
- scatter-plot points, and coloring surface plots by height.
+ ColorMap(pos, color, mapping=ColorMap.CLIP)
- Each color map is defined by a set of colors, each corresponding to a
- particular scalar value. For example:
+ ColorMap stores a mapping of specific data values to colors, for example:
- | 0.0 -> black
- | 0.2 -> red
- | 0.6 -> yellow
- | 1.0 -> white
+ | 0.0 → black
+ | 0.2 → red
+ | 0.6 → yellow
+ | 1.0 → white
- The colors for intermediate values are determined by interpolating between
+ The colors for intermediate values are determined by interpolating between
the two nearest colors in RGB color space.
- To provide user-defined color mappings, see :class:`GradientWidget `.
+ A ColorMap object provides access to the interpolated colors by indexing with a float value:
+ ``cm[0.5]`` returns a QColor corresponding to the center of ColorMap `cm`.
"""
## mapping modes
@@ -221,29 +240,26 @@ class ColorMap(object):
def __init__(self, pos, color, mapping=CLIP, mode=None): #, names=None):
"""
- =============== =======================================================================
- **Arguments:**
- pos Array of positions where each color is defined
- color Array of colors.
- Values are interpreted via
- :func:`mkColor() `.
- mapping Mapping mode (ColorMap.CLIP, REPEAT, MIRROR or DIVERGING)
- controlling mapping of relative index to color. String representations
- 'clip', 'repeat', 'mirror' or 'diverging' are also accepted.
- CLIP maps colors to [0.0;1.0] and is the default.
- REPEAT maps colors to repeating intervals [0.0;1.0];[1.0-2.0],...
- MIRROR maps colors to [0.0;-1.0] and [0.0;+1.0] identically
- DIVERGING maps colors to [-1.0;+1.0]
- =============== =======================================================================
+ __init__(pos, color, mapping=ColorMap.CLIP)
+
+ Parameters
+ ----------
+ pos: array_like of float in range 0 to 1
+ Assigned positions of specified colors
+ color: array_like of colors
+ List of colors, interpreted via :func:`mkColor() `.
+ mapping: str or int, optional
+ Controls how values outside the 0 to 1 range are mapped to colors.
+ See :func:`setMappingMode() ` for details.
+
+ The default of `ColorMap.CLIP` continues to show
+ the colors assigned to 0 and 1 for all values below or above this range, respectively.
"""
if mode is not None:
warnings.warn(
"'mode' argument is deprecated and does nothing.",
DeprecationWarning, stacklevel=2
)
- if isinstance(mapping, str):
- mapping = self.enumMap[mapping.lower()]
-
self.pos = np.array(pos)
order = np.argsort(self.pos)
self.pos = self.pos[order]
@@ -252,39 +268,86 @@ class ColorMap(object):
axis = -1,
arr = color,
)[order]
+
+ self.mapping_mode = self.CLIP # default to CLIP mode
+ if mapping is not None:
+ self.setMappingMode( mapping )
+ self.stopsCache = {}
- self.mapping_mode = self.CLIP # default to CLIP mode
+ def setMappingMode(self, mapping):
+ """
+ Sets the way that values outside of the range 0 to 1 are mapped to colors.
+
+ Parameters
+ ----------
+ mapping: int or str
+ Sets mapping mode to
+
+ - `ColorMap.CLIP` or 'clip': Values are clipped to the range 0 to 1. ColorMap defaults to this.
+ - `ColorMap.REPEAT` or 'repeat': Colors repeat cyclically, i.e. range 1 to 2 repeats the colors for 0 to 1.
+ - `ColorMap.MIRROR` or 'mirror': The range 0 to -1 uses same colors (in reverse order) as 0 to 1.
+ - `ColorMap.DIVERGING` or 'diverging': Colors are mapped to -1 to 1 such that the central value appears at 0.
+ """
+ if isinstance(mapping, str):
+ mapping = self.enumMap[mapping.lower()]
if mapping in [self.CLIP, self.REPEAT, self.DIVERGING, self.MIRROR]:
self.mapping_mode = mapping # only allow defined values
- self.stopsCache = {}
+ else:
+ raise ValueError("Undefined mapping type '{:s}'".format(str(mapping)) )
def __getitem__(self, key):
""" Convenient shorthand access to palette colors """
- if isinstance(key, int): # access by color index
+ if isinstance(key, int): # access by color index
return self.getByIndex(key)
# otherwise access by map
try: # accept any numerical format that converts to float
- float_idx = float(key)
+ float_idx = float(key)
return self.mapToQColor(float_idx)
except ValueError: pass
return None
+ def reverse(self):
+ """
+ Reverses the color map, so that the color assigned to a value of 1 now appears at 0 and vice versa.
+ This is convenient to adjust imported color maps.
+ """
+ self.pos = 1.0 - np.flip( self.pos )
+ self.color = np.flip( self.color, axis=0 )
+ self.stopsCache = {}
+
def map(self, data, mode=BYTE):
"""
- Return an array of colors corresponding to the values in *data*.
- Data must be either a scalar position or an array (any shape) of positions.
-
- The *mode* argument determines the type of data returned:
+ map(data, mode=ColorMap.BYTE)
- =========== ===============================================================
- byte (default) Values are returned as 0-255 unsigned bytes.
- float Values are returned as 0.0-1.0 floats.
- qcolor Values are returned as an array of QColor objects.
- =========== ===============================================================
+ Returns an array of colors corresponding to a single value or an array of values.
+ Data must be either a scalar position or an array (any shape) of positions.
+
+ Parameters
+ ----------
+ data: float or array_like of float
+ Scalar value(s) to be mapped to colors
+
+ mode: str or int, optional
+ Determines return format:
+
+ - `ColorMap.BYTE` or 'byte': Colors are returned as 0-255 unsigned bytes. (default)
+ - `ColorMap.FLOAT` or 'float': Colors are returned as 0.0-1.0 floats.
+ - `ColorMap.QCOLOR` or 'qcolor': Colors are returned as QColor objects.
+
+ Returns
+ -------
+ array of color.dtype
+ for `ColorMap.BYTE` or `ColorMap.FLOAT`:
+
+ RGB values for each `data` value, arranged in the same shape as `data`.
+ list of QColor objects
+ for `ColorMap.QCOLOR`:
+
+ Colors for each `data` value as Qcolor objects.
"""
if isinstance(mode, str):
mode = self.enumMap[mode.lower()]
-
+
if mode == self.QCOLOR:
pos, color = self.getStops(self.BYTE)
else:
@@ -316,7 +379,7 @@ class ColorMap(object):
return [QtGui.QColor(*x) for x in interp]
else:
return interp
-
+
def mapToQColor(self, data):
"""Convenience function; see :func:`map() `."""
return self.map(data, mode=self.QCOLOR)
@@ -330,37 +393,115 @@ class ColorMap(object):
return self.map(data, mode=self.FLOAT)
def getByIndex(self, idx):
- """Retrieve palette QColor by index"""
+ """Retrieve a QColor by the index of the stop it is assigned to."""
return QtGui.QColor( *self.color[idx] )
def getGradient(self, p1=None, p2=None):
- """Return a QLinearGradient object spanning from QPoints p1 to p2."""
- if p1 == None:
+ """
+ Returns a QtGui.QLinearGradient corresponding to this ColorMap.
+ The span and orientiation is given by two points in plot coordinates.
+
+ When no parameters are given for `p1` and `p2`, the gradient is mapped to the
+ `y` coordinates 0 to 1, unless the color map is defined for a more limited range.
+
+ Parameters
+ ----------
+ p1: QtCore.QPointF, default (0,0)
+ Starting point (value 0) of the gradient.
+ p2: QtCore.QPointF, default (dy,0)
+ End point (value 1) of the gradient. Default parameter `dy` is the span of ``max(pos) - min(pos)``
+ over which the color map is defined, typically `dy=1`.
+ """
+ if p1 is None:
p1 = QtCore.QPointF(0,0)
- if p2 == None:
+ if p2 is None:
p2 = QtCore.QPointF(self.pos.max()-self.pos.min(),0)
- g = QtGui.QLinearGradient(p1, p2)
-
+ grad = QtGui.QLinearGradient(p1, p2)
+
pos, color = self.getStops(mode=self.BYTE)
color = [QtGui.QColor(*x) for x in color]
- g.setStops(list(zip(pos, color)))
- return g
+ if self.mapping_mode == self.MIRROR:
+ pos_n = (1. - np.flip(pos)) / 2
+ col_n = np.flip( color, axis=0 )
+ pos_p = (1. + pos) / 2
+ col_p = color
+ pos = np.concatenate( (pos_n, pos_p) )
+ color = np.concatenate( (col_n, col_p) )
+ grad.setStops(list(zip(pos, color)))
+ if self.mapping_mode == self.REPEAT:
+ grad.setSpread( QtGui.QGradient.RepeatSpread )
+ return grad
+
+ def getBrush(self, span=(0.,1.), orientation='vertical'):
+ """
+ Returns a QBrush painting with the color map applied over the selected span of plot values.
+ When the mapping mode is set to `ColorMap.MIRROR`, the selected span includes the color map twice,
+ first in reversed order and then normal.
+
+ Parameters
+ ----------
+ span : tuple (min, max), default (0.0, 1.0)
+ Span of data values covered by the gradient:
+
+ - Color map value 0.0 will appear at `min`,
+ - Color map value 1.0 will appear at `max`.
+
+ orientation : str, default 'vertical'
+ Orientiation of the gradient:
+
+ - 'vertical': `span` corresponds to the `y` coordinate.
+ - 'horizontal': `span` corresponds to the `x` coordinate.
+ """
+ if orientation == 'vertical':
+ grad = self.getGradient( p1=QtCore.QPointF(0.,span[0]), p2=QtCore.QPointF(0.,span[1]) )
+ elif orientation == 'horizontal':
+ grad = self.getGradient( p1=QtCore.QPointF(span[0],0.), p2=QtCore.QPointF(span[1],0.) )
+ else:
+ raise ValueError("Orientation must be 'vertical' or 'horizontal'")
+ return QtGui.QBrush(grad)
+
+ def getPen(self, span=(0.,1.), orientation='vertical', width=1.0):
+ """
+ Returns a QPen that draws according to the color map based on vertical or horizontal position.
+
+ Parameters
+ ----------
+ span : tuple (min, max), default (0.0, 1.0)
+ Span of the data values covered by the gradient:
+
+ - Color map value 0.0 will appear at `min`.
+ - Color map value 1.0 will appear at `max`.
+
+ orientation : str, default 'vertical'
+ Orientiation of the gradient:
+
+ - 'vertical' creates a vertical gradient, where `span` corresponds to the `y` coordinate.
+ - 'horizontal' creates a horizontal gradient, where `span` correspnds to the `x` coordinate.
+
+ width : int or float
+ Width of the pen in pixels on screen.
+ """
+ brush = self.getBrush( span=span, orientation=orientation )
+ pen = QtGui.QPen(brush, width)
+ pen.setCosmetic(True)
+ return pen
def getColors(self, mode=None):
- """Return list of all color stops converted to the specified mode.
- If mode is None, then no conversion is done."""
+ """Returns a list of all color stops, converted to the specified mode.
+ If `mode` is None, no conversion is performed.
+ """
if isinstance(mode, str):
mode = self.enumMap[mode.lower()]
-
+
color = self.color
if mode in [self.BYTE, self.QCOLOR] and color.dtype.kind == 'f':
color = (color * 255).astype(np.ubyte)
elif mode == self.FLOAT and color.dtype.kind != 'f':
color = color.astype(float) / 255.
-
+
if mode == self.QCOLOR:
color = [QtGui.QColor(*x) for x in color]
-
+
return color
def getStops(self, mode):
@@ -374,20 +515,39 @@ class ColorMap(object):
self.stopsCache[mode] = (self.pos, color)
return self.stopsCache[mode]
- def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode='byte'):
+ def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode=BYTE):
"""
- Return an RGB(A) lookup table (ndarray).
-
- =============== =============================================================================
- **Arguments:**
- start The starting value in the lookup table (default=0.0)
- stop The final value in the lookup table (default=1.0)
- nPts The number of points in the returned lookup table.
- alpha True, False, or None - Specifies whether or not alpha values are included
- in the table. If alpha is None, it will be automatically determined.
- mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'.
- See :func:`map() `.
- =============== =============================================================================
+ getLookupTable(start=0.0, stop=1.0, nPts=512, alpha=None, mode=ColorMap.BYTE)
+
+ Returns an equally-spaced lookup table of RGB(A) values created
+ by interpolating the specified color stops.
+
+ Parameters
+ ----------
+ start: float, default=0.0
+ The starting value in the lookup table
+ stop: float, default=1.0
+ The final value in the lookup table
+ nPts: int, default is 512
+ The number of points in the returned lookup table.
+ alpha: True, False, or None
+ Specifies whether or not alpha values are included in the table.
+ If alpha is None, it will be automatically determined.
+ mode: int or str, default is `ColorMap.BYTE`
+ Determines return type as described in :func:`map() `, can be
+ either `ColorMap.BYTE` (0 to 255), `ColorMap.FLOAT` (0.0 to 1.0) or `ColorMap.QColor`.
+
+ Returns
+ -------
+ array of color.dtype
+ for `ColorMap.BYTE` or `ColorMap.FLOAT`:
+
+ RGB values for each `data` value, arranged in the same shape as `data`.
+ If alpha values are included the array has shape (`nPts`, 4), otherwise (`nPts`, 3).
+ list of QColor objects
+ for `ColorMap.QCOLOR`:
+
+ Colors for each `data` value as QColor objects.
"""
if isinstance(mode, str):
mode = self.enumMap[mode.lower()]
@@ -404,13 +564,13 @@ class ColorMap(object):
return table
def usesAlpha(self):
- """Return True if any stops have an alpha < 255"""
+ """Returns `True` if any stops have assigned colors with alpha < 255."""
max = 1.0 if self.color.dtype.kind == 'f' else 255
return np.any(self.color[:,3] != max)
def isMapTrivial(self):
"""
- Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0.
+ Returns `True` if the gradient has exactly two stops in it: Black at 0.0 and white at 1.0.
"""
if len(self.pos) != 2:
return False