From 244182d2bf25e3f07442e9b483f250755cd45925 Mon Sep 17 00:00:00 2001 From: Nils Nemitz Date: Fri, 19 Mar 2021 16:44:25 +0900 Subject: [PATCH] added colormap sampling functions --- examples/PaletteApplicationExample.py | 13 +- examples/PaletteTestAndEdit.py | 393 +++++++++++++++++ examples/colorMaps.py | 2 +- examples/utils.py | 1 + pyqtgraph/colormap.py | 46 +- pyqtgraph/namedColorManager.py | 2 +- pyqtgraph/palette.py | 606 +++++++++++++++++--------- 7 files changed, 824 insertions(+), 239 deletions(-) create mode 100644 examples/PaletteTestAndEdit.py diff --git a/examples/PaletteApplicationExample.py b/examples/PaletteApplicationExample.py index a6ec99be..7a16b33f 100644 --- a/examples/PaletteApplicationExample.py +++ b/examples/PaletteApplicationExample.py @@ -24,6 +24,8 @@ class MainWindow(QtWidgets.QMainWindow): self.setWindowTitle('pyqtgraph example: Palette application test') self.resize(600,600) + test_palette = pg.palette.get('system') + pg.palette.get('relaxed-dark').apply() main_layout = QtWidgets.QGridLayout( main_wid ) @@ -63,10 +65,17 @@ class MainWindow(QtWidgets.QMainWindow): self.data1 = +3 + np.random.normal(size=(15)) #500)) self.data2 = -3 + np.random.normal(size=(15)) #500)) - self.curve1 = pg.PlotDataItem(pen='r', symbol='o', symbolSize=10, symbolPen='gr_fg', symbolBrush=('y',127)) + # self.curve1 = pg.PlotDataItem( + # pen='r', + # symbol='o', symbolSize=10, symbolPen='gr_fg', symbolBrush=('y',127), + # hoverable=True, hoverPen='w', hoverBrush='w') + self.curve1 = pg.ScatterPlotItem( + symbol='o', symbolSize=12, symbolPen='gr_fg', symbolBrush=('y',127), + hoverable=True, hoverPen='gr_acc', hoverBrush='gr_reg') + # self.curve1.setHoverable(True) self.plt.addItem(self.curve1) - self.curve2 = pg.PlotCurveItem(pen='w', brush='d') + self.curve2 = pg.PlotCurveItem(pen='l', brush='d') self.curve2.setFillLevel(0) self.plt.addItem(self.curve2) self.show() diff --git a/examples/PaletteTestAndEdit.py b/examples/PaletteTestAndEdit.py new file mode 100644 index 00000000..9f225955 --- /dev/null +++ b/examples/PaletteTestAndEdit.py @@ -0,0 +1,393 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +Update a simple plot as rapidly as possible to measure speed. +""" + +## Add path to library (just for examples; you do not need this) +import initExample + +import qdarkstyle +import numpy as np + +from pyqtgraph.Qt import mkQApp, QtCore, QtGui, QtWidgets +from pyqtgraph.ptime import time +import pyqtgraph as pg + + +class MainWindow(QtWidgets.QMainWindow): + """ example application main window """ + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + + force_dark = True # start in dark mode on Windows + + self.setWindowTitle('pyqtgraph example: Palette application test') + self.resize(600,600) + + self.palette_options = ( + ('system', 'system', []), + ('legacy', 'legacy', []), + ('relaxed (dark)', 'relaxed_dark', []), + ('mono green', 'monochrome', ['green']), + ('mono amber', 'monochrome', ['amber']), + ('mono blue' , 'monochrome', ['blue' ]), + ('synthwave' , 'synthwave', []), + ) + + self.colormap_options = ( + 'CET-C1', 'CET-C2','CET-C6','CET-C7', 'CET-R2', 'CET-R4', + 'CET-L8', 'CET-L16', 'none' + # , 'none', 'CET-C1', 'CET-C2', 'CET-C3', 'CET-C4', 'CET-C5', 'CET-C6', 'CET-C7', 'CET-CBC1', 'CET-CBC2' + ) + + app = QtWidgets.QApplication.instance() + self.q_palette = { + 'system' : app.palette(), + 'dark' : self.make_dark_QPalette() + } + app.setStyle("Fusion") + + self.ui = self.prepare_ui() # relocate long-winded window layout + # dictionary self.ui contains references to: + # 'sample_start' QLineEdit for start of colormap sampling + # 'sample_step' QLineEdit for step of colormap sampling + # 'dark' QPushButton for toggling dark / standard GUI + + if force_dark: + self.ui['dark'].setChecked(True) + self.handle_dark_button(True) + + self.open_palette = pg.palette.get('system') + self.open_palette.apply() + self.update_color_fields( self.open_palette ) + + self.num_points = 30 + + # configure overview plot with four colors: + plt = self.ui['plot1'] + plt.enableAutoRange(False) + plt.setYRange( 0, 4, padding=0 ) + plt.setXRange( 0, self.num_points, padding=0 ) + for key in ('left','right','top','bottom'): + ax = plt.getAxis(key) + ax.show() + ax.setZValue(0.1) + + self.curves = [] + curve = pg.PlotCurveItem(pen='p0', brush='p0') # ('p0',127)) + curve.setFillLevel(0) + self.curves.append( (1, 1, curve) ) # dataset 1, vertical offset 3 + plt.addItem(curve) + curve = pg.ScatterPlotItem( + symbol='o', size=5, pen='p0', brush='p0', # ('p0',127), + hoverable=True, hoverPen='gr_hlt', hoverBrush='gr_fg') + self.curves.append( (1, 1, curve) ) # dataset 1, vertical offset 2 + plt.addItem(curve) + + pen_list = ['p2', 'p4', 'p6'] # add three more plots + for idx, pen in enumerate( pen_list ): + curve = pg.PlotCurveItem() + curve.setPen(pen, width=5) + self.curves.append( (3+2*idx, 1.5+0.8*idx, curve) ) # datasets 2+, vertical offset 3+ + plt.addItem(curve) + + # configure tall plot with eight colors and region overlay: + plt = self.ui['plot2'] + plt.enableAutoRange(False) + plt.setYRange( -0.6, 7.6, padding=0 ) + plt.setXRange( 0, self.num_points, padding=0 ) + plt.getAxis('bottom').hide() + plt.getAxis('left').setLabel('plot color') + plt.getAxis('left').setGrid(0.5) # 63) + + pen_list = [('p0',255),'p1','p2','p3','p4','p5','p6','p7'] # add right-side plots for each main color + for idx, pen in enumerate( pen_list ): + curve = pg.PlotCurveItem(pen=pen) + self.curves.append( (1+idx, idx, curve) ) # datasets 2+, vertical offset by index + plt.addItem(curve) + item = pg.LinearRegionItem( values=(4, 8), orientation='vertical' ) + plt.addItem(item) + + self.show() + + # prepare for continuous updates and frame rate measurement + self.last_time = time() + self.fps = None + self.timer = QtCore.QTimer(singleShot=False) + self.timer.timeout.connect( self.timed_update ) + + # prepare initial data and display in plots + self.data = np.zeros((9, self.num_points )) + self.data[0,:] = np.arange( self.data.shape[1] ) # used as x data + self.phases = np.zeros(9) + self.timed_update() + + + ### handle GUI interaction ############################################### + def update_color_fields(self, pal): + """ update line edit fields for selected palette """ + if pal is None: + print('palette is None!') + return + for key in self.ui['widget_from_color_key']: + wid = self.ui['widget_from_color_key'][key] + qcol = pal[key] + if wid is not None: + wid.setText( qcol.name() ) + + def handle_palette_select(self, idx): + """ user selected a palette in dropdown menu """ + text, identifier, args = self.palette_options[idx] + del text # not needed here + self.open_palette = pg.palette.get(identifier, *args) + print('loaded palette:', identifier, args) + + if identifier in pg.palette.PALETTE_DEFINITIONS: + info = pg.palette.PALETTE_DEFINITIONS[identifier] + colormap_sampling = info['colormap_sampling'] + if colormap_sampling is None: + identifier, start, step = 'none', 0.000, 0.125 + else: + identifier, start, step = colormap_sampling + self.ui['sample_start'].setText('{:+.3f}'.format(start)) + self.ui['sample_step' ].setText('{:+.3f}'.format(step) ) + for idx, map_id in enumerate( self.colormap_options ): + if map_id == identifier: + # print('found colormap at idx',idx) + self.ui['colormaps'].setCurrentIndex(idx) + self.update_color_fields(self.open_palette) + self.open_palette.apply() + + def handle_colormap_select(self, param=None): + """ user selected a colormap in dropdown menu or changed start / step vales """ + del param # drop index sent by QComboBox + identifier = self.ui['colormaps'].currentText() + if identifier == 'none': + return + start = self.ui['sample_start'].text() + step = self.ui['sample_step' ].text() + try: + start = float(start) + except ValueError: + start = 0.0 + self.ui['sample_start'].setText('{:+.3f}'.format(start) ) + try: + step = float(step) + except ValueError: + step = 0.125 + self.ui['sample_step'].setText('{:+.3f}'.format(step) ) + self.open_palette.sampleColorMap( cmap=identifier, start=start, step=step ) + # print('applied color map {:s} starting at {:.3f} with step {:3f}'.format(identifier, start, step) ) + + self.update_color_fields(self.open_palette) + self.open_palette.apply() + + def handle_color_update(self): + """ figure out what color field was updated """ + source = self.sender() + print('color update requested by field',source) + + def handle_update_button(self, active): + """ start/stop timer """ + if active: + self.timer.start(1) + else: + self.timer.stop() + + def handle_dark_button(self, active): + """ manually switch to dark palette to test on windows """ + app = QtWidgets.QApplication.instance() + if active: + app.setPalette( self.q_palette['dark'] ) # apply dark QPalette + else: + app.setPalette( self.q_palette['system'] ) # reapply QPalette stored at start-up + + def timed_update(self): + """ update loop, called by timer """ + self.speed = np.linspace(0.01, 0.06, 9) + self.phases += self.speed * np.random.normal(1, 1, size=9) + for idx in range(1, self.data.shape[0]): + self.data[idx, :-1] = self.data[idx, 1:] # roll + # self.data[idx, -1] = np.random.normal() + self.data[1:, -1] = 0.5 * np.sin( self.phases[1:] ) + xdata = self.data[0,:] + for idx, offset, curve in self.curves: + curve.setData( x=xdata, y=( offset + self.data[idx,:] ) ) + + now = time() + dt = now - self.last_time + self.last_time = now + if self.fps is None: + self.fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + self.fps = self.fps * (1-s) + (1.0/dt) * s + self.ui['plot2'].setTitle('%0.2f fps' % self.fps) + QtWidgets.QApplication.processEvents() ## force complete redraw for every plot + + + ### Qt color definitions for dark palette on Windows ##################### + def make_dark_QPalette(self): + # color definitions match QDarkstyle + BLACK = QtGui.QColor('#000000') + BG_LIGHT = QtGui.QColor('#505F69') + BG_NORMAL = QtGui.QColor('#32414B') + BG_DARK = QtGui.QColor('#19232D') + FG_LIGHT = QtGui.QColor('#F0F0F0') + FG_NORMAL = QtGui.QColor('#AAAAAA') + FG_DARK = QtGui.QColor('#787878') + SEL_LIGHT = QtGui.QColor('#148CD2') + SEL_NORMAL = QtGui.QColor('#1464A0') + SEL_DARK = QtGui.QColor('#14506E') + qpal = QtGui.QPalette( QtGui.QColor(BG_DARK) ) + for ptype in ( QtGui.QPalette.Active, QtGui.QPalette.Inactive ): + qpal.setColor( ptype, QtGui.QPalette.Window, BG_NORMAL ) + qpal.setColor( ptype, QtGui.QPalette.WindowText, FG_LIGHT ) # or white? + qpal.setColor( ptype, QtGui.QPalette.Base, BG_DARK ) + qpal.setColor( ptype, QtGui.QPalette.Text, FG_LIGHT ) + qpal.setColor( ptype, QtGui.QPalette.AlternateBase, BG_DARK ) + qpal.setColor( ptype, QtGui.QPalette.ToolTipBase, BG_LIGHT ) + qpal.setColor( ptype, QtGui.QPalette.ToolTipText, FG_LIGHT ) + qpal.setColor( ptype, QtGui.QPalette.Button, BG_DARK ) + qpal.setColor( ptype, QtGui.QPalette.ButtonText, FG_LIGHT ) + qpal.setColor( ptype, QtGui.QPalette.Link, SEL_NORMAL ) + qpal.setColor( ptype, QtGui.QPalette.LinkVisited, FG_NORMAL ) + qpal.setColor( ptype, QtGui.QPalette.Highlight, SEL_LIGHT ) + qpal.setColor( ptype, QtGui.QPalette.HighlightedText, BLACK ) + qpal.setColor( QtGui.QPalette.Disabled, QtGui.QPalette.Button, BG_NORMAL ) + qpal.setColor( QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, FG_DARK ) + qpal.setColor( QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, FG_DARK ) + return qpal + + ########################################################################## + def prepare_ui(self): + """ Boring Qt window layout code is implemented here """ + ui = {} + main_wid = QtWidgets.QWidget() + self.setCentralWidget(main_wid) + + color_fields = ( + # key, description, reference to line edit field) + ['gr_bg' , (0,0), 'graph background' ], + ['gr_fg' , (1,0), 'graph foreground' ], + ['gr_txt', (2,0), 'graph text' ], + ['gr_reg', (3,0), 'graph region' ], + ['gr_acc', (4,0), 'graphical accent' ], + ['gr_hlt', (5,0), 'graphical highlight'], + ['p0', (0,1), ' plot 0'], ['p1', (1,1), ' plot 1'], + ['p2', (2,1), ' plot 2'], ['p3', (3,1), ' plot 3'], + ['p4', (4,1), ' plot 4'], ['p5', (5,1), ' plot 5'], + ['p6', (6,1), ' plot 6'], ['p7', (7,1), ' plot 7'] + ) + + gr_wid1 = pg.GraphicsLayoutWidget(show=True) + ui['plot1'] = gr_wid1.addPlot() + + gr_wid2 = pg.GraphicsLayoutWidget(show=True) + ui['plot2'] = gr_wid2.addPlot() + + main_layout = QtWidgets.QHBoxLayout( main_wid ) + l_wid = QtWidgets.QWidget() + main_layout.addWidget(l_wid) + main_layout.addWidget(gr_wid2) + + l_layout = QtWidgets.QGridLayout( l_wid ) + l_layout.setContentsMargins(0,0,0,0) + l_layout.setSpacing(1) + row_idx = 0 + + label = QtWidgets.QLabel('Select a palette:') + l_layout.addWidget( label, row_idx,0, 1,2 ) + row_idx += 1 + + box = QtWidgets.QComboBox() + for text, identifier, args in self.palette_options: + del identifier, args # not needed here + box.addItem(text) + box.activated.connect(self.handle_palette_select) + btn = QtWidgets.QPushButton('dark GUI') + btn.setCheckable(True) + btn.setChecked(False) + btn.clicked.connect(self.handle_dark_button) + l_layout.addWidget( box, row_idx,0, 1,2 ) + l_layout.addWidget( btn, row_idx,3, 1,1 ) + ui['dark'] = btn + row_idx += 1 + + label = QtWidgets.QLabel('Sampled color map:') + l_layout.addWidget( label, row_idx,0, 1,2 ) + label = QtWidgets.QLabel('start') + l_layout.addWidget( label, row_idx,2, 1,1 ) + label = QtWidgets.QLabel('step') + l_layout.addWidget( label, row_idx,3, 1,1 ) + row_idx += 1 + + box = QtWidgets.QComboBox() + for identifier in self.colormap_options: + box.addItem(identifier) + ui['colormaps'] = box + ui['colormaps'].activated.connect(self.handle_colormap_select) + ui['sample_start'] = QtWidgets.QLineEdit(' 0.000') + ui['sample_start'].editingFinished.connect(self.handle_colormap_select) + ui['sample_step' ] = QtWidgets.QLineEdit('+0.125') + ui['sample_step' ].editingFinished.connect(self.handle_colormap_select) + l_layout.addWidget( box, row_idx,0, 1,2 ) + l_layout.addWidget( ui['sample_start'], row_idx,2, 1,1 ) + l_layout.addWidget( ui['sample_step' ], row_idx,3, 1,1 ) + row_idx += 1 + + spacer = QtWidgets.QWidget() + spacer.setFixedHeight(10) + l_layout.addWidget( spacer, row_idx,0, 1,2 ) + row_idx += 1 + + label = QtWidgets.QLabel('Functional colors:') + l_layout.addWidget( label, row_idx,0, 1,2 ) + row_idx += 1 + + row = 0 + ui['widget_from_color_key'] = {} # look-up for color editing fields + ui['color_key_from_widget'] = {} # reverse look-up for color editing fields + for field_list in color_fields: + key, pos, text = field_list + lab = QtWidgets.QLabel(text) + lab.setAlignment(QtCore.Qt.AlignCenter) + edt = QtWidgets.QLineEdit() + row = row_idx + pos[0] + col = 2 * pos[1] # 0 or 2 + l_layout.addWidget( lab, row,col+0, 1,1 ) + l_layout.addWidget( edt, row,col+1, 1,1 ) + ui['color_key_from_widget'][edt] = key + ui['widget_from_color_key'][key] = edt + row_idx = row + + btn = QtWidgets.QPushButton('generate continuous data') + btn.setCheckable(True) + btn.setChecked(False) + btn.clicked.connect(self.handle_update_button) + l_layout.addWidget( btn, row_idx,0, 1,2 ) + row_idx += 1 + + spacer = QtWidgets.QWidget() + spacer.setFixedHeight(10) + l_layout.addWidget( spacer, row_idx,0, 1,2 ) + row_idx += 1 + + label = QtWidgets.QLabel('Overview:') + l_layout.addWidget( label, row_idx,0, 1,4 ) + row_idx += 1 + + l_layout.addWidget( gr_wid1, row_idx,0, 1,4 ) + row_idx += 1 + + return ui + + + +mkQApp("Palette test application") +main_window = MainWindow() + +## Start Qt event loop +if __name__ == '__main__': + QtWidgets.QApplication.instance().exec_() diff --git a/examples/colorMaps.py b/examples/colorMaps.py index 17136d30..4099f2fc 100644 --- a/examples/colorMaps.py +++ b/examples/colorMaps.py @@ -54,7 +54,7 @@ monochrome_colors = ('blue', 'green', 'amber', 'red', 'pink', 'lavender', (0.5, for mono_val in monochrome_colors: num_bars += 1 lw.addLabel(str(mono_val)) - cmap = pg.colormap.make_monochrome(mono_val) + cmap = pg.colormap.makeMonochrome(mono_val) imi = pg.ImageItem() imi.setImage(img) imi.setLookupTable( cmap.getLookupTable(alpha=True) ) diff --git a/examples/utils.py b/examples/utils.py index 94f91771..40b363b0 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -21,6 +21,7 @@ examples = OrderedDict([ ('Remote Plotting', 'RemoteSpeedTest.py'), ('Scrolling plots', 'scrollingPlots.py'), ('Color Maps', 'colorMaps.py'), + ('Palette tester','PaletteTestAndEdit.py'), ('Palette adjustment','PaletteApplicationExample.py'), ('HDF5 big data', 'hdf5.py'), ('Demos', OrderedDict([ diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 69ae6305..f5c3583a 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -86,7 +86,6 @@ def _get_from_file(name): else: csv_mode = False for line in fh: - name = None line = line.strip() if len(line) == 0: continue # empty line if line[0] == ';': continue # comment @@ -110,11 +109,11 @@ def _get_from_file(name): idx += 1 # end of line reading loop # end of open - cm = ColorMap( + cmap = ColorMap( name=name, pos=np.linspace(0.0, 1.0, len(color_list)), color=color_list) #, names=color_names) - _mapCache[name] = cm - return cm + _mapCache[name] = cmap + return cmap def _get_from_matplotlib(name): """ import colormap from matplotlib definition """ @@ -124,7 +123,7 @@ def _get_from_matplotlib(name): import matplotlib.pyplot as mpl_plt except ModuleNotFoundError: return None - cm = None + cmap = None col_map = mpl_plt.get_cmap(name) if hasattr(col_map, '_segmentdata'): # handle LinearSegmentedColormap data = col_map._segmentdata @@ -142,20 +141,21 @@ def _get_from_matplotlib(name): positions[idx2] = tup[0] comp_vals[idx2] = tup[1] # these are sorted in the raw data col_data[:,idx] = np.interp(col_data[:,3], positions, comp_vals) - cm = ColorMap(pos=col_data[:,-1], color=255*col_data[:,:3]+0.5) + cmap = ColorMap(pos=col_data[:,-1], color=255*col_data[:,:3]+0.5) # some color maps (gnuplot in particular) are defined by RGB component functions: elif ('red' in data) and isinstance(data['red'], collections.Callable): col_data = np.zeros((64, 4)) 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) + cmap = 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 ) - if cm is not None: - _mapCache[name] = cm - return cm + cmap = ColorMap( name=name, + pos = np.linspace(0.0, 1.0, col_data.shape[0]), color=255*col_data[:,:3]+0.5 ) + if cmap is not None: + _mapCache[name] = cmap + return cmap def _get_from_colorcet(name): """ import colormap from colorcet definition """ @@ -173,22 +173,23 @@ def _get_from_colorcet(name): color_list.append( color_tuple ) if len(color_list) == 0: return None - cm = ColorMap( + cmap = ColorMap( name=name, pos=np.linspace(0.0, 1.0, len(color_list)), color=color_list) #, names=color_names) - _mapCache[name] = cm - return cm + _mapCache[name] = cmap + return cmap -def make_monochrome(color='green'): +def makeMonochrome(color='green'): """ Returns a ColorMap object imitating a monochrome computer screen =============== ================================================================= **Arguments:** color Primary color description. Can be one of predefined identifiers - 'green' or 'amber' + 'green', 'amber', 'blue', 'red', 'lavender', 'pink' or a tuple of relative R,G,B contributions in range 0.0 to 1.0 =============== ================================================================= """ + name = 'monochrome-'+str(color) stops = np.array([0.000, 0.167, 0.247, 0.320, 0.411, 0.539, 0.747, 1.000]) active = np.array([ 16, 72, 113, 147, 177, 205, 231, 255]) leakage = np.array([ 0, 1, 7, 21, 45, 80, 127, 191]) @@ -210,8 +211,8 @@ def make_monochrome(color='green'): g * delta + leak, b * delta + leak ) color_list.append(color_tuple) - cm = ColorMap(pos=stops, color=color_list ) - return cm + cmap = ColorMap(name=name, pos=stops, color=color_list ) + return cmap class ColorMap(object): """ @@ -260,7 +261,7 @@ class ColorMap(object): 'qcolor': QCOLOR, } - def __init__(self, pos, color, mode=None, mapping=None): #, names=None): + def __init__(self, pos, color, name=None, mode=None, mapping=None): #, names=None): """ =============== ================================================================= **Arguments:** @@ -280,6 +281,7 @@ class ColorMap(object): DIVERGING maps colors to [-1.0;+1.0] =============== ================================================================= """ + self.name = name # storing a name helps identify ColorMaps sampled by Palette self.pos = np.array(pos) order = np.argsort(self.pos) self.pos = self.pos[order] @@ -304,6 +306,12 @@ class ColorMap(object): self.mapping_mode = self.CLIP self.stopsCache = {} + + def __str__(self): + """ provide human-readable identifier """ + if self.name is None: + return 'unnamed ColorMap({:d})'.format(len(self.pos)) + return "ColorMap({:d}):'{:s}'".format(len(self.pos),self.name) def __getitem__(self, key): """ Convenient shorthand access to palette colors """ diff --git a/pyqtgraph/namedColorManager.py b/pyqtgraph/namedColorManager.py index 3fc654ad..7aabbd37 100644 --- a/pyqtgraph/namedColorManager.py +++ b/pyqtgraph/namedColorManager.py @@ -25,7 +25,7 @@ for key, col in [ # add functional colors ('gr_fg','d'), # graphical foreground ('gr_bg','k'), # graphical background ('gr_txt','d'), # graphical text color - ('gr_hov','r') # graphical hover color + ('gr_hlt','r') # graphical hover color ]: DEFAULT_COLORS[key] = DEFAULT_COLORS[col] diff --git a/pyqtgraph/palette.py b/pyqtgraph/palette.py index da037163..e5f611b2 100644 --- a/pyqtgraph/palette.py +++ b/pyqtgraph/palette.py @@ -1,254 +1,428 @@ -from .Qt import QtGui +from . import Qt +from .Qt import QtCore, QtGui, QtWidgets from . import functions as fn # namedColorManager from . import colormap __all__ = ['Palette'] -LEGACY_RAW = { # legacy raw colors: - 'b': ( 0, 0,255,255), - 'g': ( 0,255, 0,255), - 'r': (255, 0, 0,255), - 'c': ( 0,255,255,255), - 'm': (255, 0,255,255), - 'y': (255,255, 0,255), - 'k': ( 0, 0, 0,255), - 'w': (255,255,255,255), - 'd': (150,150,150,255), - 'l': (200,200,200,255), - 's': (100,100,150,255) -} -LEGACY_FUNC = { # functional colors: - 'gr_fg' : 'd', - 'gr_bg' : 'k', - 'gr_txt' : 'd', - 'gr_acc' : (200,200,100,255), - 'gr_hov' : 'r', - 'gr_reg' : ( 0, 0,255, 50) -} -LEGACY_PLOT = [ # plot / accent colors: - 'l','y','r','m','b','c','g','d' -] +#### todo list #### +# define legacy colors for relaxed-dark +# find color definitions for relaxed-light +# define color names for relaxed palettes +# enable color adjustment in PaletteTestandEdit.py! -RELAXED_RAW = { # "fresh" raw colors: - 'col_orange':'#A64D21', 'col_l_orange':'#D98A62', 'col_d_orange':'#732E0B', - 'col_red' :'#B32424', 'col_l_red' :'#E66767', 'col_d_red' :'#800D0D', - 'col_purple':'#991F66', 'col_l_purple':'#D956A3', 'col_d_purple':'#660A31', - 'col_violet':'#7922A6', 'col_l_violet':'#BC67E6', 'col_d_violet':'#5A0C80', - 'col_indigo':'#5F29CC', 'col_l_indigo':'#9673FF', 'col_d_indigo':'#380E8C', - 'col_blue' :'#2447B3', 'col_l_blue' :'#6787E6', 'col_d_blue' :'#0D2980', - 'col_sky' :'#216AA6', 'col_l_sky' :'#77ADD9', 'col_d_sky' :'#0B4473', - 'col_cyan' :'#1C8C8C', 'col_l_cyan' :'#73BFBF', 'col_d_cyan' :'#095959', - 'col_green' :'#1F9952', 'col_l_green' :'#7ACC9C', 'col_d_green' :'#0A6630', - 'col_grass' :'#7AA621', 'col_l_grass' :'#BCD982', 'col_d_grass' :'#50730B', - 'col_yellow':'#BFB226', 'col_l_yellow':'#F2E985', 'col_d_yellow':'#80760D', - 'col_gold' :'#A67A21', 'col_l_gold' :'#D9B46C', 'col_d_gold' :'#73500B', - # 'col_black' :'#000000', 'col_gr1' :'#242429', 'col_gr2' :'#44444D', - 'col_black' :'#000000', 'col_gr1' :'#161619', 'col_gr2' :'#43434D', - 'col_gr3' :'#70707F', 'col_gr4' :'#9D9DB2', 'col_gr5' :'#C9C9E5', - 'col_white' :'#FFFFFF' -} -RELAXED_DARK_FUNC= { # functional colors: - 'gr_fg' : 'col_gr5', - 'gr_bg' : 'col_gr1', - 'gr_txt' : 'col_gr5', - 'gr_acc' : 'col_cyan', - 'gr_hov' : 'col_white', - 'gr_reg' : ('col_cyan', 30), - # legacy colors: - 'b': 'col_l_blue' , 'c': 'col_l_cyan', 'g': 'col_l_green', - 'y': 'col_l_yellow', 'r': 'col_l_red' , 'm': 'col_l_violet', - 'k': 'col_black' , 'w': 'col_white', - 'd': 'col_gr2' , 'l': 'col_gr4' , 's': 'col_l_sky' -} -RELAXED_DARK_PLOT = [ # plot / accent colors: - 'col_l_sky' , - 'col_l_indigo', - 'col_l_purple', - 'col_l_red' , - 'col_l_gold' , - 'col_l_grass' , - 'col_l_cyan' , - 'col_l_blue' , - 'col_l_violet', - 'col_l_orange', - 'col_l_yellow', - 'col_l_green' -] +PALETTE_DEFINITIONS = { + 'legacy': { + 'colormap_sampling' : None, + 'b': ( 0, 0,255,255), 'g': ( 0,255, 0,255), 'r': (255, 0, 0,255), + 'c': ( 0,255,255,255), 'm': (255, 0,255,255), 'y': (255,255, 0,255), + 'k': ( 0, 0, 0,255), 'w': (255,255,255,255), + 'd': (150,150,150,255), 'l': (200,200,200,255), 's': (100,100,150,255), + # --- functional colors --- + 'gr_fg' : 'd', 'gr_bg' : 'k', + 'gr_txt': 'd', 'gr_acc': (200,200,100,255), + 'gr_hlt': 'r', 'gr_reg': ( 0, 0,255,100), + # --- manually assigned plot colors --- + 'p0':'l', 'p1':'y', 'p2':'r', 'p3':'m', + 'p4':'b', 'p5':'c', 'p6':'g', 'p7':'d' + }, + 'relaxed_dark':{ + 'colormap_sampling': ('CET-C6', 0.430, -0.125), + 'col_black' :'#000000', 'col_white' :'#FFFFFF', + 'col_gr1':'#19232D', 'col_gr2':'#32414B', 'col_gr3':'#505F69', # match QDarkStyle background colors + 'col_gr4':'#787878', 'col_gr5':'#AAAAAA', 'col_gr6':'#F0F0F0', # match QDarkstyle foreground colors + # --- functional colors --- + 'gr_fg' : 'col_gr4', 'gr_bg' : 'col_gr1', + 'gr_txt': 'col_gr5', 'gr_acc': '#1464A0', #col_cyan', + 'gr_hlt': 'col_white', 'gr_reg': ('#1464A0',100) + # legacy colors: + # 'b': 'col_l_blue' , 'c': 'col_l_cyan', 'g': 'col_l_green', + # 'y': 'col_l_yellow', 'r': 'col_l_red' , 'm': 'col_l_violet', + # 'k': 'col_black' , 'w': 'col_white', + # 'd': 'col_gr2' , 'l': 'col_gr4' , 's': 'col_l_sky' + }, + 'synthwave':{ + 'colormap_sampling': ('CET-L8', 0.275, 0.100), + 'col_black' :'#000000', 'col_white' :'#FFFFFF', + 'col_gr1':'#19232D', 'col_gr2':'#32414B', 'col_gr3':'#505F69', # match QDarkStyle background colors + 'col_gr4':'#787878', 'col_gr5':'#AAAAAA', 'col_gr6':'#F0F0F0', # match QDarkstyle foreground colors + # --- functional colors --- + 'gr_fg' : 'col_gr4', 'gr_bg' : 'col_gr1', + 'gr_txt': 'col_gr5', 'gr_acc': '#1464A0', #col_cyan', + 'gr_hlt': 'col_white', 'gr_reg': ('#1464A0',100) + # legacy colors: + # 'b': 'col_l_blue' , 'c': 'col_l_cyan', 'g': 'col_l_green', + # 'y': 'col_l_yellow', 'r': 'col_l_red' , 'm': 'col_l_violet', + # 'k': 'col_black' , 'w': 'col_white', + # 'd': 'col_gr2' , 'l': 'col_gr4' , 's': 'col_l_sky' + } -RELAXED_LIGHT_FUNC= { # functional colors: - 'gr_fg' : 'col_gr1', - 'gr_bg' : 'col_gr5', - 'gr_txt' : 'col_black', - 'gr_acc' : 'col_orange', - 'gr_hov' : 'col_black', - 'gr_reg' : ('col_blue', 30), - # legacy colors: - 'b': 'col_blue' , 'c': 'col_cyan', 'g': 'col_green', - 'y': 'col_yellow', 'r': 'col_red' , 'm': 'col_violet', - 'k': 'col_black' , 'w': 'col_white', - 'd': 'col_gr2' , 'l': 'col_gr4' , 's': 'col_sky' } -RELAXED_LIGHT_PLOT = [ # plot / accent colors: - 'col_sky' , - 'col_indigo', - 'col_purple', - 'col_red' , - 'col_gold' , - 'col_grass' , - 'col_cyan' , - 'col_blue' , - 'col_violet', - 'col_orange', - 'col_yellow', - 'col_green' -] + +# RELAXED_RAW = { # "fresh" raw colors: +# 'col_orange':'#A64D21', 'col_l_orange':'#D98A62', 'col_d_orange':'#732E0B', +# 'col_red' :'#B32424', 'col_l_red' :'#E66767', 'col_d_red' :'#800D0D', +# 'col_purple':'#991F66', 'col_l_purple':'#D956A3', 'col_d_purple':'#660A31', +# 'col_violet':'#7922A6', 'col_l_violet':'#BC67E6', 'col_d_violet':'#5A0C80', +# 'col_indigo':'#5F29CC', 'col_l_indigo':'#9673FF', 'col_d_indigo':'#380E8C', +# 'col_blue' :'#2447B3', 'col_l_blue' :'#6787E6', 'col_d_blue' :'#0D2980', +# 'col_sky' :'#216AA6', 'col_l_sky' :'#77ADD9', 'col_d_sky' :'#0B4473', +# 'col_cyan' :'#1C8C8C', 'col_l_cyan' :'#73BFBF', 'col_d_cyan' :'#095959', +# 'col_green' :'#1F9952', 'col_l_green' :'#7ACC9C', 'col_d_green' :'#0A6630', +# 'col_grass' :'#7AA621', 'col_l_grass' :'#BCD982', 'col_d_grass' :'#50730B', +# 'col_yellow':'#BFB226', 'col_l_yellow':'#F2E985', 'col_d_yellow':'#80760D', +# 'col_gold' :'#A67A21', 'col_l_gold' :'#D9B46C', 'col_d_gold' :'#73500B', +# # 'col_black' :'#000000', 'col_gr1' :'#242429', 'col_gr2' :'#44444D', +# 'col_black' :'#000000', 'col_gr1' :'#161619', 'col_gr2' :'#43434D', +# 'col_gr3' :'#70707F', 'col_gr4' :'#9D9DB2', 'col_gr5' :'#C9C9E5', +# 'col_white' :'#FFFFFF' +# } +# RELAXED_DARK_FUNC= { # functional colors: +# 'gr_fg' : 'col_gr5', +# 'gr_bg' : 'col_gr1', +# 'gr_txt' : 'col_gr5', +# 'gr_acc' : 'col_cyan', +# 'gr_hlt' : 'col_white', +# 'gr_reg' : ('col_cyan',100), +# # legacy colors: +# 'b': 'col_l_blue' , 'c': 'col_l_cyan', 'g': 'col_l_green', +# 'y': 'col_l_yellow', 'r': 'col_l_red' , 'm': 'col_l_violet', +# 'k': 'col_black' , 'w': 'col_white', +# 'd': 'col_gr2' , 'l': 'col_gr4' , 's': 'col_l_sky' +# } +# RELAXED_DARK_PLOT = [ # plot / accent colors: +# 'col_l_sky' , +# 'col_l_indigo', +# 'col_l_purple', +# 'col_l_red' , +# 'col_l_gold' , +# 'col_l_grass' , +# 'col_l_cyan' , +# 'col_l_blue' , +# 'col_l_violet', +# 'col_l_orange', +# 'col_l_yellow', +# 'col_l_green' +# ] + +# RELAXED_LIGHT_FUNC= { # functional colors: +# 'gr_fg' : 'col_gr1', +# 'gr_bg' : 'col_gr5', +# 'gr_txt' : 'col_black', +# 'gr_acc' : 'col_orange', +# 'gr_reg' : ('col_blue',100), +# # legacy colors: +# 'b': 'col_blue' , 'c': 'col_cyan', 'g': 'col_green', +# 'y': 'col_yellow', 'r': 'col_red' , 'm': 'col_violet', +# 'k': 'col_black' , 'w': 'col_white', +# 'd': 'col_gr2' , 'l': 'col_gr4' , 's': 'col_sky' +# } +# RELAXED_LIGHT_PLOT = [ # plot / accent colors: +# 'col_sky' , +# 'col_indigo', +# 'col_purple', +# 'col_red' , +# 'col_gold' , +# 'col_grass' , +# 'col_cyan' , +# 'col_blue' , +# 'col_violet', +# 'col_orange', +# 'col_yellow', +# 'col_green' +# ] -def block_to_QColor( block, dic=None ): - """ convert color information to a QColor """ - # allowed formats: - # 'name' - # ('name', alpha) - # (R,G,B) / (R,G,B,alpha) - if isinstance(block, QtGui.QColor): - return block # this is already a QColor +def identifier_to_QColor( identifier, color_dict=None ): + """ + Convert color information to a QColor + =================== ============================================================= + **allowed formats** + 'name' name must be a hex value or a key in 'color_dict' + ('name', alpha) new copy will be assigned the specified alpha value + QColor will be copied to avoid interaction if changing alpha values + Qt.GlobalColor will result in a matching QColor + (R,G,B) a new Qcolor will be created + (R,G,B, alpha) a new Qcolor with specified opacity will be created + =================== ============================================================= + """ + if isinstance(identifier, (QtGui.QColor, QtCore.Qt.GlobalColor)): + return QtGui.QColor(identifier) alpha = None - if isinstance(block, str): # return known QColor - name = block - if dic is None or name not in dic: + if isinstance(identifier, str): # return known QColor + name = identifier + if color_dict is None or name not in color_dict: if name[0] != '#': - raise ValueError('Undefined color name '+str(block)) + raise ValueError('Undefined color name '+str(identifier)) return QtGui.QColor( name ) else: - return dic[name] - if not hasattr(block, '__len__'): - raise ValueError('Invalid color definition '+str(block)) + return color_dict[name] + if not hasattr(identifier, '__len__'): + raise ValueError('Invalid color definition '+str(identifier)) qcol = None - if len(block) == 2: - name, alpha = block - if dic is None or name not in dic: + if len(identifier) == 2: + name, alpha = identifier + if color_dict is None or name not in color_dict: if name[0] != '#': - raise ValueError('Undefined color name '+str(block)) + raise ValueError('Undefined color identifier '+str(identifier)) qcol = QtGui.QColor( name ) else: - qcol = dic[ name ] - elif len(block) in (3,4): - qcol = QtGui.QColor( *block ) + qcol = color_dict[ name ] + elif len(identifier) in (3,4): + qcol = QtGui.QColor( *identifier ) if alpha is not None and qcol is not None: - qcol = QtGui.QColor(qcol) # make a copy before changing alpha + # distinct QColors are now created for each color + # qcol = QtGui.QColor(qcol) # make a copy before changing alpha qcol.setAlpha( alpha ) return qcol -def assemble_palette( raw_col, func_col, plot_col=None ): +def get(identifier, *args): """ - assemble palette color dictionary from parts: - raw_col should contain color information in (R,G,B,(A)) or hex format - func_col typically contains keys of colors defined before - plot_col is a list of plotting colors to be included as 'c0' to 'cX' (in hex) - """ - pal = {} - for part in [raw_col, func_col]: - for key in part: - col = part[key] - pal[key] = block_to_QColor( col, pal ) - if plot_col is not None: - for idx, col in enumerate( plot_col ): - key = 'p{:X}'.format(idx) # plot color 'pX' does not overlap hexadecimal codes. - pal[key] = block_to_QColor( col, pal ) - return pal - -DEFAULT_PALETTE = assemble_palette( LEGACY_RAW, LEGACY_FUNC, LEGACY_PLOT ) + Returns a Palette object that can be applied to update the PyQtGraph color scheme + =============== ==================================================================== + **Arguments:** + identifier 'system' (default): Colors are based on current Qt QPalette + 'legacy': The color scheme of previous versions of PyQtGraph + 'monochrome' ['color identifier']: Creates a palette that imitates + a monochrome computer monitor. + 'color identifier' can be one of + 'green', 'amber', 'blue', 'red', 'lavender', 'pink' + or a tuple of relative R,G,B contributions in range 0.0 to 1.0 -def get(name): - if name == 'relaxed_dark': - pal = assemble_palette( RELAXED_RAW, RELAXED_DARK_FUNC, RELAXED_DARK_PLOT ) - elif name == 'relaxed_light': - pal = assemble_palette( RELAXED_RAW, RELAXED_LIGHT_FUNC, RELAXED_LIGHT_PLOT ) - else: - pal = DEFAULT_PALETTE - return Palette( colors=pal ) - - -def make_monochrome(color='green', n_colors=8): - """ - Returns a Palette object imitating a monochrome computer screen - =============== ================================================================= - **Arguments:** - color Primary color description. Can be one of predefined identifiers - 'green' or 'amber' - or a tuple of relative R,G,B contributions in range 0.0 to 1.0 - =============== ================================================================= + {dictionary}: full palette specification, see palette.py for details + =============== ==================================================================== """ - cm = colormap.make_monochrome(color) - if cm is None: return None - raw = {} - for idx in range(8): - key = 'col_m{:d}'.format(idx) - raw[key] = cm[ idx/9 ] - # print('added color',key,'as',raw[key].name(),raw[key].getRgb()) - func = { - 'gr_bg' : 'col_m0', - 'gr_fg' : 'col_m4', - 'gr_txt': 'col_m6', - 'gr_acc': 'col_m5', - 'gr_hov': 'col_m7', - 'gr_reg': ('col_m1', 30), - 'k': 'col_m0', 'd': 'col_m1', 's': 'col_m3', 'l': 'col_m6', 'w': 'col_m7' - } - avail = raw.copy() # generate a disctionary of available colors - del avail['col_m0'] # already taken by black - del avail['col_m7'] # already taken by white - needed = { - 'b': ( 0, 0,255), 'c': ( 0,255,255), 'g': ( 0,255, 0), - 'y': (255,255, 0), 'r': (255, 0, 0), 'm': (255, 0,255) - } - for nd_key in needed: - nd_tup = needed[nd_key] # this is the int RGB tuple we are looking to represent - best_dist = 1e10 - best_key = None - for av_key in avail: - av_tup = avail[av_key].getRgb() # returns (R,G,B,A) tuple - dist = (nd_tup[0]-av_tup[0])**2 + (nd_tup[1]-av_tup[1])**2 + (nd_tup[2]-av_tup[2])**2 - if dist < best_dist: - best_dist = dist - best_key = av_key - # print('assigning',nd_key,'as',best_key,':',avail[best_key].getRgb() ) - func[nd_key] = avail[best_key] - del avail[best_key] # remove from available list - pal = assemble_palette( raw, func ) - return Palette( colors=pal, cmap=cm, n_colors=8 ) + if identifier == 'system': + pal = Palette() + return pal # default QPalette based settings + if identifier == 'monochrome': + pal = Palette() + pal.setMonochrome( *args ) + return pal + if identifier in PALETTE_DEFINITIONS: + info = PALETTE_DEFINITIONS[identifier].copy() + sampling_info = info.pop('colormap_sampling', None) + pal = Palette( cmap=sampling_info, colors=info ) + return pal + raise KeyError("Unknown palette identifier '"+str(identifier)+"'") class Palette(object): - # minimum colors to be defined: - def __init__(self, colors=None, cmap=None, n_colors=8): + """ + A Palette object provides a set of colors that can conveniently applied + to the PyQtGraph color scheme. + It specifies at least the following colors, but additional one can be added: + Primary colors: + 'b', 'c', 'g', 'y', 'r', 'm' + Gray scale: + 'k', 'd', 'l', 'w' ranging from black to white + 's' slate gray + System colors: + 'gr_bg', 'gr_fg', 'gr_txt' graph background, foreground and text colors + 'gr_wdw' window background color + 'gr_reg' partially transparent region shading color + 'gr_acc' accent for UI elements + 'gr_hlt' highlight for selected elements + Plot colors: + 'p0' to 'p7' typically sampled from a ColorMap + """ + def __init__(self, cmap=None, colors=None ): super().__init__() - self.palette = colors - self.cmap = cmap - self.n_colors = int(n_colors) - if self.n_colors < 8: self.n_colors = 8 # enforce minimum number of plot colors - if self.cmap is not None: - sep = 1/n_colors - for idx in range(self.n_colors): - key = 'p{:x}'.format( idx ) - if key in self.palette: - continue # do not overwrite user-provided plot colors - val = sep * (0.5 + idx) - col = self.cmap[val] - self.palette[key] = col - # print('assigning',key,'as',col,':',col.name()) + self.palette = { # populate dictionary of QColors with legacy defaults + 'b': QtGui.QColor( 0, 0,255,255), 'g': QtGui.QColor( 0,255, 0,255), + 'r': QtGui.QColor(255, 0, 0,255), 'c': QtGui.QColor( 0,255,255,255), + 'm': QtGui.QColor(255, 0,255,255), 'y': QtGui.QColor(255,255, 0,255), + 'k': QtGui.QColor( 0, 0, 0,255), 'w': QtGui.QColor(255,255,255,255), + 'd': QtGui.QColor(150,150,150,255), 'l': QtGui.QColor(200,200,200,255), + 's': QtGui.QColor(100,100,150,255) + } + self.dark = None # is initially set when assigning system palette + self.emulateSystem() + self.cmap = None + if cmap is not None: # prepare plot colors from provided colormap + if isinstance(cmap, (str, colormap.ColorMap) ): # sampleColorMap will convert if needed + self.sampleColorMap( cmap=cmap) + if isinstance(cmap, (tuple,list)): # ('identifier', start, step) + cmap, start, step = cmap + self.sampleColorMap(cmap=cmap, start=start, step=step) + if colors is not None: + # print('color dictionary:', colors) + self.add(colors) # override specified colors + + def __getitem__(self, key): + """ Convenient shorthand access to palette colors """ + if isinstance(key, str): # access by color name + return self.palette.get(key,None) + if isinstance(key, int): # access by plot color index + idx = key % 8 # map to 0 to 8 + key = 'p'+str(idx) + return self.palette.get(key,None) + return None + + def colorMap(self): + """ + Return the ColorMap object used to create plot colors or 'None' if not assigned. + """ + return self.cmap - # needs: addColors - # needs to be aware of number of plot colors - # needs to be indexable by key and numerical plot color - # indexed plot colors need to wrap around to work for any index. - # needs: clearColors + def sampleColorMap(self, cmap=None, n_colors=8, prefix='p', start=0., step=None ): + """ + Sample a ColorMap to update defined plot colors + ============= =============================================================================== + **Arguments** + cmap a ColorMap object to be sampled. If not given, a default color map is used + n_colors default '8': Number of assigned colors. + The default set needs to include 'p0' to 'p7' + prefix default 'p' assigns colors as 'p0' to 'pXX', at least 'p7' is required. + Additional sets can be defined with a different prefix, e.g. 'col_0' to 'col_7' + All prefixes need to start with 'p' or 'col' to avoid namespace overlap with + functional colors and hexadecimal numbers + start first sampled value (default is 0.000) + step step between samples. Default 'None' equally samples n colors from a + linear colormap, including values 0.0 and 1.0. + Color values > 1. and < 0. wrap around! + ============= =============================================================================== + """ + valid = prefix[0]=='p' or ( len(prefix)>=3 and prefix[:3]=='col') + if not valid: + raise ValueError("'prefix' of plot color needs to start with 'p'.") + if cmap is None: + cmap = self.cmap + if isinstance(cmap, str): + cmap = colormap.get(cmap) # obtain ColorMap if identifier is given + if cmap is None: + raise ValueError("Please specify 'cmap' parameter when no default colormap is available.") + if not isinstance( cmap, colormap.ColorMap ): + raise ValueError("Failed to obtain ColorMap object for 'cmap' = '+str(cmap).") + + if prefix == 'p': + self.cmap = cmap # replace default color map + n_colors = 8 # always define 8 primary plot colors + if step is None: + step = 1 / (n_colors - 1) # sample 0. to 1. (inclusive) by default + for cnt in range(n_colors): + val = start + cnt * step + # print( val ) + if val > 1.0 or val < 0.0: # don't touch 1.0 value + val = val % 1. # but otherwise map to 0 to 1 range + qcol = cmap[val] + key = prefix + str(cnt) + self.palette[key] = qcol + + def add(self, colors): + """ + Add colors given in dictionary 'colors' to the palette + All colors will be converted to QColor. + Setting 'gr_bg' with a mean color value < 127 will set the palette's 'dark' property + """ + for key in colors: + col = identifier_to_QColor( colors[key], color_dict=self.palette ) + if key == 'gr_bg': + bg_tuple = col.getRgb() + self.dark = bool( sum( bg_tuple[:3] ) < 3 * 127 ) # dark mode? + self.palette[key] = col + + def emulateSystem(self): + """ + Retrieves the current Qt 'active' palette and extracts the following colors: + ===================================== ============================================================================ + 'gr_fg','gr_txt' from QPalette.Text (foreground color used with Base) + 'gr_bg' from QPalette.Base (background color for e.g. text entry widgets) + 'gr_wdw' from QPalette.Window (a general background color) + 'gr_reg' from QPalette.AlternateBase (alternating row background color) + 'gr_acc' from QPalette.Link (color used for unvisited hyperlinks) + 'gr_hlt' from QPalette.Highlight (color to indicate a selected item) + ===================================== ============================================================================ + """ + app = QtWidgets.QApplication.instance() + if app is None: return None + qPalette = app.palette() + col_grp = QtGui.QPalette.Active + colors = {} + for key, alpha, col_role in ( + ('gr_bg' , None, QtGui.QPalette.Base), # background color for e.g. text entry + ('gr_fg' , None, QtGui.QPalette.WindowText), # overall foreground text color + ('gr_txt', None, QtGui.QPalette.Text), # foreground color used with Base + ('gr_reg', 100, QtGui.QPalette.AlternateBase), # alternating row background color + ('gr_acc', None, QtGui.QPalette.Link), # color of unvisited hyperlink + ('gr_hlt', None, QtGui.QPalette.Highlight), # color to indicate a selected item + ('ui_wind', None, QtGui.QPalette.Window), # a general background color + ('ui_text', None, QtGui.QPalette.WindowText) # overall foreground text color + ): + qcol = qPalette.color(col_grp, col_role) + if alpha is not None: qcol.setAlpha(alpha) + colors[key] = qcol + self.add(colors) + colors = { + 'b': QtCore.Qt.blue , 'c': QtCore.Qt.cyan, 'g': QtCore.Qt.green , + 'y': QtCore.Qt.yellow, 'r': QtCore.Qt.red , 'm': QtCore.Qt.magenta, + 'w': QtCore.Qt.white , 's': QtCore.Qt.gray, 'k': QtCore.Qt.black , + 'l': QtCore.Qt.lightGray, 'd': QtCore.Qt.darkGray + } + if not self.dark: # darken some colors for light mode + colors['c'] = QtCore.Qt.darkCyan + colors['g'] = QtCore.Qt.darkGreen + colors['y'] = QtCore.Qt.darkYellow + colors['m'] = QtCore.Qt.darkMagenta + self.add(colors) + colors = { + 'p'+str(idx) : name + for idx, name in enumerate( ('gr_fg', 'y','r','m','b','c','g','s') ) + } + self.add(colors) + + def setMonochrome(self, color='green'): + """ + Updates graph colors with a set based on 'monochrome' color map, + imitating a monochrome computer screen + ============== ================================================================= + **Arguments:** + color Primary color description. Can be one of predefined identifiers + 'green', 'amber', 'blue' + or a tuple of relative R,G,B contributions in range 0.0 to 1.0 + ============== ================================================================= + """ + cmap = colormap.makeMonochrome(color) + if cmap is None: + raise ValueError("Failed to generate color for '"+str(color)+"'") + self.sampleColorMap( cmap=cmap, start=1.0, step=-1/8 ) # assign bright to dark, don't go all the way to background. + # define colors 'm0' (near-black) to 'm8' (near-white): + self.sampleColorMap( n_colors=9, cmap=cmap, step=1/8, prefix='col_m' ) + colors = { + 'gr_bg' : 'col_m0', 'gr_fg' : 'col_m4', + 'gr_txt': 'col_m5', 'gr_acc': 'col_m6', + 'gr_hlt': 'col_m7', 'gr_reg': ('col_m1', 30), + 'k': 'col_m0', 'd': 'col_m1', 's': 'col_m3', 'l': 'col_m6', 'w': 'col_m7' + } + self.add( colors ) + # make a dictionary of plot colors (except darkest and lightest) to emulate primary colors: + avail = { key: self.palette[key] for key in ('p0','p1','p2','p3','p4','p5','p6') } + needed = { # int RGB colors that we are looking to emulate: + 'b': ( 0, 0,255), 'c': ( 0,255,255), 'g': ( 0,255, 0), + 'y': (255,255, 0), 'r': (255, 0, 0), 'm': (255, 0,255) + } + colors = {} + for nd_key in needed: + nd_tup = needed[nd_key] # int RGB tuple to be represented + best_dist = 1e10 + best_key = None + for av_key in avail: + av_tup = avail[av_key].getRgb() # returns (R,G,B,A) tuple + sq_dist = (nd_tup[0]-av_tup[0])**2 + (nd_tup[1]-av_tup[1])**2 + (nd_tup[2]-av_tup[2])**2 + if sq_dist < best_dist: + best_dist = sq_dist + best_key = av_key + # print('assigning',nd_key,'as',best_key,':',avail[best_key].getRgb() ) + colors[nd_key] = avail[best_key] + del avail[best_key] # remove from available list + self.add( colors ) def apply(self): """ - provides palette to NamedColorManager, which triggers a global refresh of named colors + Applies this palette to the overall PyQtGraph color scheme. + This provides the palette to NamedColorManager, which triggers a global refresh of named colors """ fn.NAMED_COLOR_MANAGER.redefinePalette( colors=self.palette )