Make plotting with gradients more fun (#1742)

* added convenience functions to better handle plotting with gradients

* docstring correction, example name correction

* retrying to get  documentation format right

* another attempt at cleaning up docs

* Don't hardcode timer type (and docs fixing attempt)

* more docstring re-formatting

* linebreaks in docstrings

* more documentation adjustments

* documentation pass

* some corrections to documentation

* more adjustments to documentation

* again?

* removed some whitespace and redundant blank lines, changed some checks '== None' to 'is None'

* fixed mis-spelling QColor as Qcolor
This commit is contained in:
Nils Nemitz 2021-05-07 15:20:21 +09:00 committed by GitHub
parent 2fb7cdafbd
commit bfc63bb730
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 613 additions and 116 deletions

View File

@ -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 <https://colorcet.holoviz.org/>`_. 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 <pyqtgraph.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__

View File

@ -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.

View File

@ -0,0 +1,7 @@
ColorBarItem
============
.. autoclass:: pyqtgraph.ColorBarItem
:members:
.. automethod:: pyqtgraph.ColorBarItem.__init__

View File

@ -12,6 +12,7 @@ Contents:
plotdataitem
plotitem
imageitem
colorbaritem
pcolormeshitem
graphitem
viewbox

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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_()

View File

@ -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_()

View File

@ -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_()

View File

@ -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'),

View File

@ -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 <pyqtgraph.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() <pyqtgraph.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() <pyqtgraph.mkColor>`.
mapping: str or int, optional
Controls how values outside the 0 to 1 range are mapped to colors.
See :func:`setMappingMode() <ColorMap.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() <pyqtgraph.ColorMap.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() <pyqtgraph.ColorMap.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() <pyqtgraph.ColorMap.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