mostly functional example

This commit is contained in:
Nils Nemitz 2021-03-08 02:14:37 +09:00
parent eb6f724767
commit 148a35d430
16 changed files with 536 additions and 174 deletions

View File

@ -0,0 +1,153 @@
#!/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 numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import mkQApp, QtGui, QtCore, QtWidgets
from pyqtgraph.ptime import time
from pyqtgraph import functions as fn
class MainWindow(QtWidgets.QMainWindow):
""" example application main window """
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
main_wid = QtWidgets.QWidget()
self.setCentralWidget(main_wid)
self.setWindowTitle('pyqtgraph example: Palette application test')
self.resize(600,600)
# pg.functions.SIGNAL_SOURCE.paletteChangedSignal.connect(self.testSignal) # test link
# pg.palette.get('monogreen').apply()
main_layout = QtWidgets.QGridLayout( main_wid )
gr_wid = pg.GraphicsLayoutWidget(show=True)
main_layout.addWidget( gr_wid, 0,0, 1,4 )
btn = QtWidgets.QPushButton('continuous')
btn.clicked.connect(self.handle_button_timer_on)
main_layout.addWidget(btn, 1,0, 1,1 )
btn = QtWidgets.QPushButton('stop updates')
btn.clicked.connect(self.handle_button_timer_off)
main_layout.addWidget(btn, 2,0, 1,1 )
btn = QtWidgets.QPushButton('apply <legacy>')
btn.clicked.connect(self.handle_button_pal1)
main_layout.addWidget(btn, 1,2, 1,1 )
btn = QtWidgets.QPushButton('apply <mono green>')
btn.clicked.connect(self.handle_button_pal2)
main_layout.addWidget(btn, 1,3, 1,1 )
btn = QtWidgets.QPushButton('apply <relaxed - dark>')
btn.clicked.connect(self.handle_button_pal3)
main_layout.addWidget(btn, 2,2, 1,1 )
btn = QtWidgets.QPushButton('apply <relaxed - light>')
btn.clicked.connect(self.handle_button_pal4)
main_layout.addWidget(btn, 2,3, 1,1 )
# self.manager = pg.fn.NAMED_COLOR_MANAGER
# self.pen1 = pg.NamedPen('p1') #,width=0.5)
self.plt = gr_wid.addPlot()
self.plt.enableAutoRange(False)
self.plt.setYRange( -7,7 )
self.plt.setXRange( 0, 15 ) #500 )
self.plt.setLabel('bottom', 'Index', units='B')
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.plt.addItem(self.curve1)
self.curve2 = pg.PlotCurveItem(pen='p3', brush='p4')
self.curve2.setFillLevel(0)
self.plt.addItem(self.curve2)
self.show()
self.pal_1 = pg.palette.get('legacy')
self.pal_2 = pg.palette.get('monogreen')
self.pal_3 = pg.palette.get('relaxed_dark')
self.pal_4 = pg.palette.get('relaxed_light')
self.lastTime = time()
self.fps = None
self.timer = QtCore.QTimer(singleShot=False)
self.timer.timeout.connect( self.timed_update )
self.timed_update()
def testSignal(self, val):
""" demonstrate use of PaletteChanged signal """
print('"Palette changed" signal was received with value', val)
def handle_button_timer_on(self):
""" (re-)activate timer """
self.timer.start(1)
def handle_button_timer_off(self):
""" de-activate timer """
self.timer.stop()
def handle_button_pal1(self):
""" apply palette 1 on request """
print('--> legacy')
self.pal_1.apply()
def handle_button_pal2(self):
""" apply palette 2 on request """
print('--> mono green')
self.pal_2.apply()
def handle_button_pal3(self):
""" apply palette 1 on request """
print('--> relax(light)')
self.pal_3.apply()
def handle_button_pal4(self):
""" apply palette 1 on request """
print('--> relax(light)')
self.pal_4.apply()
def timed_update(self):
""" update loop, called by timer """
self.data1[:-1] = self.data1[1:]
self.data1[-1] = +3 + np.random.normal()
xdata = np.arange( len(self.data1) )
self.curve1.setData( x=xdata, y=self.data1 )
self.data2[:-1] = self.data2[1:]
self.data2[-1] = -3 + np.random.normal()
self.curve2.setData( y=self.data2 )
now = time()
dt = now - self.lastTime
self.lastTime = 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.plt.setTitle('%0.2f fps' % self.fps)
QtWidgets.QApplication.processEvents() ## force complete redraw for every plot
mkQApp("Palette test application")
main_window = MainWindow()
## Start Qt event loop
if __name__ == '__main__':
QtWidgets.QApplication.instance().exec_()

View File

@ -1,7 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
from argparse import Namespace from argparse import Namespace
examples = OrderedDict([ examples = OrderedDict([
('Command-line usage', 'CLIexample.py'), ('Command-line usage', 'CLIexample.py'),
('Basic Plotting', Namespace(filename='Plotting.py', recommended=True)), ('Basic Plotting', Namespace(filename='Plotting.py', recommended=True)),
@ -21,6 +20,7 @@ examples = OrderedDict([
('Auto-range', 'PlotAutoRange.py'), ('Auto-range', 'PlotAutoRange.py'),
('Remote Plotting', 'RemoteSpeedTest.py'), ('Remote Plotting', 'RemoteSpeedTest.py'),
('Scrolling plots', 'scrollingPlots.py'), ('Scrolling plots', 'scrollingPlots.py'),
('Palette adjustment','PaletteApplicationExample.py'),
('HDF5 big data', 'hdf5.py'), ('HDF5 big data', 'hdf5.py'),
('Demos', OrderedDict([ ('Demos', OrderedDict([
('Optics', 'optics_demos.py'), ('Optics', 'optics_demos.py'),

View File

@ -62,12 +62,17 @@ CONFIG_OPTIONS = {
'useCupy': False, # When True, attempt to use cupy ( currently only with ImageItem and related functions ) 'useCupy': False, # When True, attempt to use cupy ( currently only with ImageItem and related functions )
} }
def setConfigOption(opt, value): def setConfigOption(opt, value):
if opt not in CONFIG_OPTIONS: if opt not in CONFIG_OPTIONS:
raise KeyError('Unknown configuration option "%s"' % opt) raise KeyError('Unknown configuration option "%s"' % opt)
if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'): if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'):
raise ValueError('imageAxisOrder must be either "row-major" or "col-major"') raise ValueError('imageAxisOrder must be either "row-major" or "col-major"')
# setConfigOption should be relocated to have access to functions.py
# Then background / foreground updates can be intercepted and applied to the palette
# if opt == 'background':
# functions.Colors['gr_bg'] = functions.Colors[value]
# if opt == 'foreground':
# functions.Colors['gr_fg'] = functions.Colors[value]
CONFIG_OPTIONS[opt] = value CONFIG_OPTIONS[opt] = value
def setConfigOptions(**opts): def setConfigOptions(**opts):
@ -280,6 +285,7 @@ from .ptime import time
from .Qt import isQObjectAlive from .Qt import isQObjectAlive
from .ThreadsafeTimer import * from .ThreadsafeTimer import *
from .namedPen import * from .namedPen import *
from .namedBrush import *
from .palette import * from .palette import *

View File

@ -22,6 +22,7 @@ from .Qt import QtGui, QtCore, QT_LIB, QtVersion
from . import Qt from . import Qt
from .namedColorManager import NamedColorManager from .namedColorManager import NamedColorManager
from .namedPen import NamedPen from .namedPen import NamedPen
from .namedBrush import NamedBrush
from .metaarray import MetaArray from .metaarray import MetaArray
from collections import OrderedDict from collections import OrderedDict
@ -219,25 +220,66 @@ class Color(QtGui.QColor):
return (self.red, self.green, self.blue, self.alpha)[ind]() return (self.red, self.green, self.blue, self.alpha)[ind]()
def parseNamedColorSpecification(*args):
"""
check if args specify a NamedColor, looking for
'name' or ('name', alpha) information.
Returns:
None if invalid
('name', alpha) if a valid name and alpha value is given
('name', None) if no alpha value is available
('', None) if an empty name is given, indicating a blank color
None if the specification does not match a NamedColor
"""
while len(args) <= 1:
if len(args) == 0:
return None
if len(args) == 1:
arg = args[0]
if isinstance(arg, str):
if len(arg) == 0:
return ('', None) # valid, but blank
if arg[0] == '#':
return None # hexadecimal string not handled as NamedColor
if arg in Colors:
return (arg, None) # valid name, no alpha given
if isinstance(arg, tuple) or isinstance(arg, list):
args = arg # promote to top level
else:
return None #numerical values not handled as NamedColor
if len(args) == 2:
if isinstance(arg[0], str):
return (arg[0], arg[1]) # return ('name', alpha) tuple
return None # all other cases not handled as NamedColor
def mkColor(*args): def mkColor(*args):
""" """
Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: Convenience function for constructing QColor from a variety of argument types. Accepted arguments are:
================ ================================================ ================ ================================================
'c' one of: r, g, b, c, m, y, k, w 'name' any color name specifed in palette
R, G, B, [A] integers 0-255 R, G, B, [A] integers 0-255
(R, G, B, [A]) tuple of integers 0-255 (R, G, B, [A]) tuple of integers 0-255
float greyscale, 0.0-1.0 float greyscale, 0.0-1.0
int see :func:`intColor() <pyqtgraph.intColor>` int see :func:`intColor() <pyqtgraph.intColor>`
(int, hues) see :func:`intColor() <pyqtgraph.intColor>` (int, hues) see :func:`intColor() <pyqtgraph.intColor>`
"RGB" hexadecimal strings; may begin with '#' "#RGB" hexadecimal strings; should begin with '#'
"RGBA" "#RGBA"
"RRGGBB" "#RRGGBB"
"RRGGBBAA" "#RRGGBBAA"
QColor QColor instance; makes a copy. QColor QColor instance; makes a copy.
================ ================================================ ================ ================================================
""" """
err = 'Not sure how to make a color from "%s"' % str(args) err = 'Not sure how to make a color from "%s"' % str(args)
result = parseNamedColorSpecification(args) # check if this is a named palette color
if result is not None: # make a return palette color
name, alpha = result
if name == '':
return QtGui.QColor(0,0,0,0) # empty string means "no color"
qcol = Colors[name]
if alpha is not None: qcol.setAlpha( alpha )
return qcol
if len(args) == 1: if len(args) == 1:
if isinstance(args[0], basestring): if isinstance(args[0], basestring):
c = args[0] c = args[0]
@ -301,116 +343,115 @@ def mkColor(*args):
return QtGui.QColor(*args) return QtGui.QColor(*args)
def mkBrush(*args, **kwds): def mkBrush(*args, **kargs):
""" """
| Convenience function for constructing Brush. | Convenience function for constructing Brush.
| This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>` | This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>`
| Calling mkBrush(None) returns an invisible brush. | Calling mkBrush(None) returns an invisible brush.
""" """
print('mkBrush called with',args,kwds) while ( # unravel single element sublists
if 'color' in kwds: ( isinstance(args, tuple) or isinstance(args,list) )
color = kwds['color'] and len(args) == 1
elif len(args) == 1: ):
arg = args[0] args = args[0]
if arg is None: # now args is either a non-list entity, or a multi-element tuple
return QtGui.QBrush(QtCore.Qt.NoBrush) # short-circuits:
elif isinstance(arg, QtGui.QBrush): if isinstance(args, NamedBrush):
return QtGui.QBrush(arg) return args # pass through predefined NamedPen directly
elif isinstance(args, QtGui.QBrush):
return QtGui.QBrush(args) ## return a copy of this brush
elif isinstance(args, dict):
return mkBrush(**args) # retry with kwargs assigned from dictionary
elif args is None:
return QtGui.QBrush( QtCore.Qt.NoBrush ) # explicit None means "no brush"
# no short-circuit, continue parsing to construct QPen or NamedPen
if 'hsv' in kargs: # hsv argument takes precedence
qcol = hsvColor( *kargs['hsv'] )
qbrush = QtGui.QBrush(qcol)
else:
if 'color' in kargs:
args = kargs['color'] # 'color' KW-argument overrides unnamed arguments
if args is None:
return QtGui.QBrush( QtCore.Qt.NoBrush ) # explicit None means "no brush"
if args == () or args == []:
print(' functions: returning default color NamedBrush')
qpen = NamedBrush( 'gr_fg' ) # default foreground color
else: else:
color = arg result = parseNamedColorSpecification(args)
elif len(args) > 1: if result is not None: # make a NamedBrush
color = args name, alpha = result
return QtGui.QBrush(mkColor(color)) if name == '':
return QtGui.QBrush( QtCore.Qt.NoBrush ) # empty string means "no brush"
def mkNamedPen(name, **kargs): qbrush = NamedBrush(name, alpha=alpha)
""" else: # make a QBrush
Try to create a named pen. qcol = mkColor(args)
Currently, this quietly returns None if 'name' does not specify a color. qbrush = QtGui.QBrush(qcol)
This causes mkPen to keep trying to parse color information. # here we would apply additional style based on KW-arguments
return qbrush
In addition to arguments 'style', 'width', 'dash' and 'cosmetic',
'alpha' = 0-255 sets the opacity of the named pen
"""
alpha = kargs.get('alpha', None)
try:
pen = NamedPen( name, alpha=alpha )
except ValueError:
print(' failed to make NamedPen',name,kargs)
return None
if kargs == {}:
print(' prepared NamedPen',name,'(no kargs)')
return pen # default pen of width zero is cosmetic by default
style = kargs.get('style', None) # in many cases, a predefined pen is just passed through
width = kargs.get('width', 1) # collect remaining kargs to define properties
dash = kargs.get('dash', None)
cosmetic = kargs.get('cosmetic', True)
pen.setCosmetic(cosmetic)
if style is not None: pen.setStyle(style)
if dash is not None: pen.setDashPattern(dash)
print(' prepared NamedPen',name,kargs)
return pen
def mkPen(*args, **kargs): def mkPen(*args, **kargs):
""" """
Convenience function for constructing QPen. Convenience function for constructing QPen.
Examples:: Examples::
mkPen(color) mkPen(color)
mkPen(color, width=2) mkPen(color, width=2)
mkPen(cosmetic=False, width=4.5, color='r') mkPen(cosmetic=False, width=4.5, color='r')
mkPen({'color': "FF0", width: 2}) mkPen({'color': "FF0", width: 2})
mkPen(None) # (no pen) mkPen(None) # (no pen)
mkPen() # default color
In these examples, *color* may be replaced with any arguments accepted by :func:`mkColor() <pyqtgraph.mkColor>` """ In these examples, *color* may be replaced with any arguments accepted by :func:`mkColor() <pyqtgraph.mkColor>` """
# print('mkPen called:',args,kargs) # print('mkPen called:',args,kargs)
color = kargs.get('color', None) # collect only immediately required properties while ( # unravel single element sublists
style = kargs.get('style', None) # in many cases, a predefined pen is just passed through ( isinstance(args, tuple) or isinstance(args,list) )
and len(args) == 1
pen = None ):
if len(args) == 1: args = args[0]
arg = args[0] # now args is either a non-list entity, or a multi-element tuple
if isinstance(arg, str): # short-circuits:
if len(arg) == 0: # empty string sets "no pen" if isinstance(args, NamedPen):
style = QtCore.Qt.NoPen return args # pass through predefined NamedPen directly
elif arg[0] != '#': elif isinstance(args, QtGui.QPen):
pen = mkNamedPen(arg) return QtGui.QPen(args) ## return a copy of this pen
elif isinstance(arg, NamedPen): elif isinstance(args, dict):
return arg # pass through predefined NamedPen directly return mkPen(**args) # retry with kwargs assigned from dictionary
elif isinstance(arg, QtGui.QPen): elif args is None:
return QtGui.QPen(arg) ## return a copy of this pen return QtGui.QPen( QtCore.Qt.NoPen ) # explicit None means "no pen"
elif isinstance(arg, dict): # no short-circuit, continue parsing to construct QPen or NamedPen
return mkPen(**arg) width = kargs.get('width', 1) # width 1 unless specified otherwise
elif arg is None: if 'hsv' in kargs: # hsv argument takes precedence
style = QtCore.Qt.NoPen qcol = hsvColor( *kargs['hsv'] )
qpen = QtGui.QPen(QtGui.QBrush(qcol), width)
else:
if 'color' in kargs:
args = kargs['color'] # 'color' KW-argument overrides unnamed arguments
if args is None:
return QtGui.QPen( QtCore.Qt.NoPen ) # explicit None means "no pen"
if args == () or args == []:
qpen = NamedPen( 'gr_fg', width=width ) # default foreground color
else: else:
color = arg result = parseNamedColorSpecification(args)
elif len(args) > 1: if result is not None: # make a NamedPen
color = args name, alpha = result
if name == '':
return QtGui.QPen( QtCore.Qt.NoPen ) # empty string means "no pen"
qpen = NamedPen( name, alpha=alpha, width=width )
else: # make a QPen
qcol = mkColor(args)
qpen = QtGui.QPen(QtGui.QBrush(qcol), width)
# now apply styles according to kw arguments:
style = kargs.get('style', None)
width = kargs.get('width', 1) # collect remaining kargs to define properties width = kargs.get('width', 1) # collect remaining kargs to define properties
dash = kargs.get('dash', None) dash = kargs.get('dash', None)
cosmetic = kargs.get('cosmetic', True) cosmetic = kargs.get('cosmetic', True)
hsv = kargs.get('hsv', None) assert qpen is not None
qpen.setCosmetic(cosmetic)
if color is None:
color = mkColor('l')
if hsv is not None:
color = hsvColor(*hsv)
else:
color = mkColor(color)
if pen is None:
pen = QtGui.QPen(QtGui.QBrush(color), width)
pen.setCosmetic(cosmetic)
if style is not None: if style is not None:
pen.setStyle(style) qpen.setStyle(style)
if dash is not None: if dash is not None:
pen.setDashPattern(dash) qpen.setDashPattern(dash)
return pen return qpen
def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0):

View File

@ -104,7 +104,7 @@ class AxisItem(GraphicsWidget):
self.setPen(pen) self.setPen(pen)
if textPen is None: if textPen is None:
self.setTextPen('gr_fg') # default foreground color self.setTextPen('gr_txt') # default text color
else: else:
self.setTextPen(pen) self.setTextPen(pen)
@ -1217,4 +1217,6 @@ class AxisItem(GraphicsWidget):
def styleHasChanged(self): def styleHasChanged(self):
""" self.picture needs to be invalidated to initiate full redraw """ """ self.picture needs to be invalidated to initiate full redraw """
self.picture = None self.picture = None
self.labelStyle['color'] = self._textPen.color().name()
self._updateLabel()
super().styleHasChanged() super().styleHasChanged()

View File

@ -5,6 +5,7 @@ from .GraphicsItem import GraphicsItem
from .. import functions as fn from .. import functions as fn
__all__ = ['GraphicsObject'] __all__ = ['GraphicsObject']
DEBUG = False
try: # prepare common definition for slot decorator across PyQt / Pyside: try: # prepare common definition for slot decorator across PyQt / Pyside:
QT_CORE_SLOT = QtCore.pyqtSlot QT_CORE_SLOT = QtCore.pyqtSlot
@ -58,4 +59,4 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
""" called to trigger redraw after all named colors have been updated """ """ called to trigger redraw after all named colors have been updated """
# self._boundingRect = None # self._boundingRect = None
self.update() self.update()
print('redraw after style change:', self) if DEBUG: print(' GrpahicsObject: redraw after style change:', self)

View File

@ -4,6 +4,7 @@ from .GraphicsItem import GraphicsItem
from .. import functions as fn from .. import functions as fn
__all__ = ['GraphicsWidget'] __all__ = ['GraphicsWidget']
DEBUG = False
try: # prepare common definition for slot decorator across PyQt / Pyside: try: # prepare common definition for slot decorator across PyQt / Pyside:
QT_CORE_SLOT = QtCore.pyqtSlot QT_CORE_SLOT = QtCore.pyqtSlot
@ -22,7 +23,6 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget):
""" """
QtGui.QGraphicsWidget.__init__(self, *args, **kargs) QtGui.QGraphicsWidget.__init__(self, *args, **kargs)
GraphicsItem.__init__(self) GraphicsItem.__init__(self)
# fn.NAMED_COLOR_MANAGER.paletteChangeSignal.connect(self.styleChange)
fn.NAMED_COLOR_MANAGER.paletteHasChangedSignal.connect(self.styleHasChanged) fn.NAMED_COLOR_MANAGER.paletteHasChangedSignal.connect(self.styleHasChanged)
## done by GraphicsItem init ## done by GraphicsItem init
@ -64,16 +64,9 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget):
#print "shape:", p.boundingRect() #print "shape:", p.boundingRect()
return p return p
# @QT_CORE_SLOT(dict)
# # @QtCore.Slot()
# def styleChange(self, color_dict):
# """ stub function called after Palette.apply(), specific ractions to palette redefinitions execute here """
# print('style change request:', self, type(color_dict))
@QT_CORE_SLOT() @QT_CORE_SLOT()
# @QtCore.Slot(dict)
def styleHasChanged(self): def styleHasChanged(self):
""" called to trigger redraw after all named colors have been updated """ """ called to trigger redraw after all named colors have been updated """
# self._boundingRect = None # self._boundingRect = None
self.update() self.update()
print('redraw after style change:', self) if DEBUG: print(' GraphicsWidget: redraw after style change:', self)

View File

@ -15,16 +15,19 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor):
Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use TextItem Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use TextItem
""" """
def __init__(self, text=' ', parent=None, angle=0, **args): def __init__(self, text=' ', parent=None, angle=0, **args):
GraphicsWidget.__init__(self, parent) GraphicsWidget.__init__(self, parent)
GraphicsWidgetAnchor.__init__(self) GraphicsWidgetAnchor.__init__(self)
self.item = QtGui.QGraphicsTextItem(self) self.item = QtGui.QGraphicsTextItem(self)
self.opts = { self.opts = {
'color': None, 'color': 'gr_txt', # default text color. Was: None,
'justify': 'center' 'justify': 'center'
} }
self.opts.update(args) self.opts.update(args)
self._brush = fn.mkBrush(self.opts['color']) # make a NamedBrush by default
self._hex_color_override = None
self._optlist = []
self._sizeHint = {} self._sizeHint = {}
self.setText(text) self.setText(text)
self.setAngle(angle) self.setAngle(angle)
@ -32,6 +35,9 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor):
def setAttr(self, attr, value): def setAttr(self, attr, value):
"""Set default text properties. See setText() for accepted parameters.""" """Set default text properties. See setText() for accepted parameters."""
self.opts[attr] = value self.opts[attr] = value
if attr == 'color':
self._brush = fn.mkBrush(value) # make a new NamedBrush/QBrush
self._hex_color_override = None # to use for all normal output
def setText(self, text, **args): def setText(self, text, **args):
"""Set the text and text properties in the label. Accepts optional arguments for auto-generating """Set the text and text properties in the label. Accepts optional arguments for auto-generating
@ -46,25 +52,28 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor):
==================== ============================== ==================== ==============================
""" """
self.text = text self.text = text
if 'color' in args:
# temporary override for color:
col = fn.mkColor(opts['color'])
self._hex_color_override = col.name()
color_opt = self._hex_color_override
else:
self._hex_color_override = None # return to defined color
color_opt = self._brush.color().name()
opts = self.opts opts = self.opts
for k in args: for k in args:
opts[k] = args[k] opts[k] = args[k]
optlist = [] self.optlist = []
# self.optlist.append('color: ' + col.name()
color = self.opts['color']
if color is None:
color = getConfigOption('foreground')
color = fn.mkColor(color)
optlist.append('color: #' + fn.colorStr(color)[:6])
if 'size' in opts: if 'size' in opts:
optlist.append('font-size: ' + opts['size']) self.optlist.append('font-size: ' + opts['size'])
if 'bold' in opts and opts['bold'] in [True, False]: if 'bold' in opts and opts['bold'] in [True, False]:
optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) self.optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']])
if 'italic' in opts and opts['italic'] in [True, False]: if 'italic' in opts and opts['italic'] in [True, False]:
optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) self.optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']])
full = "<span style='%s'>%s</span>" % ('; '.join(optlist), text) full = "<span style='color: {:s}; {:s}'>{:s}</span>".format(color_opt, '; '.join(self.optlist), self.text)
#print full
self.item.setHtml(full) self.item.setHtml(full)
self.updateMin() self.updateMin()
self.resizeEvent(None) self.resizeEvent(None)
@ -134,9 +143,17 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor):
def itemRect(self): def itemRect(self):
return self.item.mapRectToParent(self.item.boundingRect()) return self.item.mapRectToParent(self.item.boundingRect())
def styleHasChanged(self):
""" overridden to update color without changing the text """
if self._hex_color_override is not None:
return # nothing to do, overridden text color will not change.
color_opt = self._brush.color().name() # get updated color
full = "<span style='color: {:s}; {:s}'>{:s}</span>".format(color_opt, '; '.join(self.optlist), self.text)
self.item.setHtml(full)
super().styleHasChanged()
#def paint(self, p, *args): #def paint(self, p, *args):
#p.setPen(fn.mkPen('r')) #p.setPen(fn.mkPen('r'))
#p.drawRect(self.rect()) #p.drawRect(self.rect())
#p.setPen(fn.mkPen('g')) #p.setPen(fn.mkPen('g'))
#p.drawRect(self.itemRect()) #p.drawRect(self.itemRect())

View File

@ -20,6 +20,7 @@ from ..python2_3 import basestring
__all__ = ['ScatterPlotItem', 'SpotItem'] __all__ = ['ScatterPlotItem', 'SpotItem']
DEBUG = False
# When pxMode=True for ScatterPlotItem, QPainter.drawPixmap is used for drawing, which # When pxMode=True for ScatterPlotItem, QPainter.drawPixmap is used for drawing, which
@ -253,6 +254,7 @@ class SymbolAtlas(object):
images = [] images = []
data = [] data = []
for key, style in styles.items(): for key, style in styles.items():
if DEBUG: print('\nrender:', style[2].color().name(), style[3].color().name(), style)
img = renderSymbol(*style) img = renderSymbol(*style)
arr = fn.imageToArray(img, copy=False, transpose=False) arr = fn.imageToArray(img, copy=False, transpose=False)
images.append(img) # keep these to delay garbage collection images.append(img) # keep these to delay garbage collection
@ -411,8 +413,8 @@ class ScatterPlotItem(GraphicsObject):
'name': None, 'name': None,
'symbol': 'o', 'symbol': 'o',
'size': 7, 'size': 7,
'pen': fn.mkPen(getConfigOption('foreground')), 'pen': fn.mkPen('gr_fg'), # getConfigOption('foreground')),
'brush': fn.mkBrush(100, 100, 150), 'brush': fn.mkBrush(('gr_fg',128)), # (100, 100, 150),
'hoverable': False, 'hoverable': False,
'tip': 'x: {x:.3g}\ny: {y:.3g}\ndata={data}'.format, 'tip': 'x: {x:.3g}\ny: {y:.3g}\ndata={data}'.format,
} }
@ -489,7 +491,6 @@ class ScatterPlotItem(GraphicsObject):
Add new points to the scatter plot. Add new points to the scatter plot.
Arguments are the same as setData() Arguments are the same as setData()
""" """
## deal with non-keyword arguments ## deal with non-keyword arguments
if len(args) == 1: if len(args) == 1:
kargs['spots'] = args[0] kargs['spots'] = args[0]
@ -1236,6 +1237,19 @@ class ScatterPlotItem(GraphicsObject):
return any(self.opts['hover' + opt.title()] != _DEFAULT_STYLE[opt] return any(self.opts['hover' + opt.title()] != _DEFAULT_STYLE[opt]
for opt in ['symbol', 'size', 'pen', 'brush']) for opt in ['symbol', 'size', 'pen', 'brush'])
def styleHasChanged(self):
""" overridden to trigger symbol atlas refresh """
if DEBUG:
print(' ScatterPlotItem: style update!')
print(' pens:',self.data['pen'] )
print(' default pen :', self.opts[ 'pen' ].color().name(), self.opts[ 'pen' ] )
print(' default brush:', self.opts['brush'].color().name(), self.opts['brush'] )
self.fragmentAtlas.clear()
self.data['sourceRect'] = (0, 0, 0, 0)
self.updateSpots(self.data)
super().styleHasChanged()
class SpotItem(object): class SpotItem(object):
""" """

View File

@ -87,7 +87,8 @@ def test_init_spots():
# check data is correct # check data is correct
spots = s.points() spots = s.points()
defPen = pg.mkPen(pg.getConfigOption('foreground')) # defPen = pg.mkPen(pg.getConfigOption('foreground'))
defPen = pg.mkPen('gr_fg')
assert spots[0].pos().x() == 0 assert spots[0].pos().x() == 0
assert spots[0].pos().y() == 1 assert spots[0].pos().y() == 1

80
pyqtgraph/namedBrush.py Normal file
View File

@ -0,0 +1,80 @@
# from ..Qt import QtGui
from .Qt import QtGui, QtCore
from . import functions as fn
__all__ = ['NamedBrush']
DEBUG = False
class NamedBrush(QtGui.QBrush):
""" Extends QPen to retain a functional color description """
def __init__(self, name, alpha=None ):
"""
Creates a new NamedBrush object.
'name' should be in 'functions.Colors'
'alpha' controls opacity which persists over palette changes
"""
if DEBUG: print(' NamedBrush created as',name,alpha)
super().__init__(QtCore.Qt.SolidPattern) # Initialize QBrush superclass
self._identifier = (name, alpha)
self._updateQColor(self._identifier)
fn.NAMED_COLOR_MANAGER.register( self ) # manually register for callbacks
def __eq__(self, other): # make this a hashable object
# return other is self
if isinstance(other, self.__class__):
return self._identifier == other._identifier
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return id(self)
def setColor(self, name=None, alpha=None):
""" update color name. This does not trigger a global redraw. """
if name is None:
name = self._identifier[0]
elif isinstance(name, QtGui.QColor):
# Replicates alpha adjustment workaround in NamedPen, allowing only alpha to be adjusted retroactively
if alpha is None:
alpha = name.alpha() # extract from given QColor
name = self._identifier[0]
if DEBUG: print(' NamedBrush: setColor(QColor) call: set alpha to', alpha)
self._identifier = (name, alpha)
self._updateQColor(self._identifier)
def setAlpha(self, alpha):
""" update opacity value """
self._identifier = (self._identifier[0], alpha)
self._updateQColor(self._identifier)
def _updateQColor(self, identifier, color_dict=None):
""" update super-class QColor """
name, alpha = identifier
if color_dict is None:
color_dict = fn.NAMED_COLOR_MANAGER.colors()
try:
qcol = fn.Colors[name]
except ValueError as exc:
raise ValueError("Color '{:s}' is not in list of defined colors".format(str(name)) ) from exc
if alpha is not None:
qcol.setAlpha( alpha )
if DEBUG: print(' NamedBrush '+name+' updated to QColor ('+str(qcol.name())+', alpha='+str(alpha)+')')
super().setColor( qcol )
def identifier(self):
""" return current color identifier """
return self._identifier
def paletteChange(self, color_dict):
""" refresh QColor according to lookup of identifier in functions.Colors """
if DEBUG: print(' NamedBrush: style change request:', self, type(color_dict))
self._updateQColor(self._identifier, color_dict=color_dict)
if DEBUG:
qcol = super().color()
name, alpha = self._identifier
print(' NamedBrush: retrieved new QColor ('+str(qcol.name())+') '
+ 'for name '+str(name)+' ('+str(alpha)+')' )

View File

@ -64,6 +64,7 @@ class NamedColorManager(QtCore.QObject): # this needs to emit QEvents
def register(self, obj): def register(self, obj):
""" register a function for paletteChange callback """ """ register a function for paletteChange callback """
self.registered_objects.add( obj ) self.registered_objects.add( obj )
# if DEBUG: print(' NamedColorManager: New list', self.registered_objects )
def redefinePalette(self, color_dic): def redefinePalette(self, color_dic):
""" update list of named colors, emitsignals to color objects and widgets """ """ update list of named colors, emitsignals to color objects and widgets """

View File

@ -11,42 +11,31 @@ class NamedPen(QtGui.QPen):
def __init__(self, name, width=1, alpha=None ): def __init__(self, name, width=1, alpha=None ):
""" """
Creates a new NamedPen object. Creates a new NamedPen object.
'identifier' should be a 'name' included in 'functions.Colors' or 'name' should be included in 'functions.Colors'
'(name, alpha)' to include transparency 'width' specifies linewidth and defaults to 1
'alpha' controls opacity which persists over palette changes
""" """
try: if DEBUG: print(' NamedBrush created as',name,alpha)
qcol = fn.Colors[name] super().__init__(QtCore.Qt.SolidLine) # Initialize QPen superclass
# print('QColor alpha is', qcol.alpha() ) super().setWidth(width)
except ValueError as exc:
raise ValueError("Color {:s} is not in list of defined colors".format(str(name)) ) from exc
if alpha is not None:
if DEBUG: print(' NamedPen: setting alpha to',alpha)
qcol.setAlpha( alpha )
super().__init__( QtGui.QBrush(qcol), width) # Initialize QPen superclass
super().setCosmetic(True) super().setCosmetic(True)
self._identifier = (name, alpha) self._identifier = (name, alpha)
self._updateQColor(self._identifier)
fn.NAMED_COLOR_MANAGER.register( self ) # manually register for callbacks fn.NAMED_COLOR_MANAGER.register( self ) # manually register for callbacks
def __eq__(self, other): # make this a hashable object def __eq__(self, other): # make this a hashable object
return other is self # return other is self
if isinstance(other, self.__class__):
return self._identifier == other._identifier
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self): def __hash__(self):
return id(self) return id(self)
# def _parse_identifier(self, identifier):
# """ parse identifier parameter, which can be 'name' or '(name, alpha)' """
# alpha = None
# if isinstance(identifier, str):
# name = identifier
# else:
# try:
# name, alpha = identifier
# except ValueError as exc:
# raise ValueError("Invalid argument. 'identifier' should be 'name' or ('name', 'alpha'), but is {:s}".format(str(color)) ) from exc
# if name[0] == '#':
# raise TypeError("NamedPen should not be used for fixed colors ('name' = {:s} was given)".format(str(name)) )
# return name, alpha
def setColor(self, name=None, alpha=None): def setColor(self, name=None, alpha=None):
""" update color name. This does not trigger a global redraw. """ """ update color name. This does not trigger a global redraw. """
if name is None: if name is None:
@ -54,23 +43,33 @@ class NamedPen(QtGui.QPen):
elif isinstance(name, QtGui.QColor): elif isinstance(name, QtGui.QColor):
# this is a workaround for the alpha adjustements in AxisItem: # this is a workaround for the alpha adjustements in AxisItem:
# While the color will not change, the alpha value can be adjusted as needed. # While the color will not change, the alpha value can be adjusted as needed.
alpha = name.alpha() # extract if alpha is None:
self._identifier = self._identifier[0], alpha alpha = name.alpha() # extract from given QColor
# print(' NamedColor setColor(QColor) call: set alpha to', name.alpha())
name = self._identifier[0] name = self._identifier[0]
if DEBUG: print(' NamedPen: setColor(QColor) call: set alpha to', alpha)
self._identifier = (name, alpha)
self._updateQColor(self._identifier)
def setAlpha(self, alpha):
""" update opacity value """
self._identifier = (self._identifier[0], alpha)
self._updateQColor(self._identifier)
def _updateQColor(self, identifier, color_dict=None):
""" update super-class QColor """
name, alpha = identifier
if color_dict is None:
color_dict = fn.NAMED_COLOR_MANAGER.colors()
try: try:
qcol = fn.Colors[name] qcol = fn.Colors[name]
except ValueError as exc: except ValueError as exc:
raise ValueError("Color {:s} is not in list of defined colors".format(str(name)) ) from exc raise ValueError("Color '{:s}' is not in list of defined colors".format(str(name)) ) from exc
if alpha is not None: if alpha is not None:
qcol.setAlpha( alpha ) qcol.setAlpha( alpha )
if DEBUG: print(' NamedPen updated to QColor ('+str(qcol.name())+')')
super().setColor( qcol ) super().setColor( qcol )
self._identifier = (name, alpha)
# def setBrush(self):
# """ disabled """
# return None
def paletteChange(self, color_dict): def paletteChange(self, color_dict):
""" refresh QColor according to lookup of identifier in functions.Colors """ """ refresh QColor according to lookup of identifier in functions.Colors """

View File

@ -30,12 +30,12 @@ LEGACY_PLOT = [ # plot / accent colors:
] ]
MONOGREEN_RAW = { MONOGREEN_RAW = {
'col_g0':'#000000', 'col_g1':'#014801', 'col_g2':'#077110', 'col_g3':'#159326', 'col_g0':'#001000', 'col_g1':'#014801', 'col_g2':'#077110', 'col_g3':'#159326',
'col_g4':'#2DB143', 'col_g5':'#50CD65', 'col_g6':'#7FE7A0', 'col_g7':'#BFFFD4' 'col_g4':'#2DB143', 'col_g5':'#50CD65', 'col_g6':'#7FE7A0', 'col_g7':'#BFFFD4'
} }
MONOGREEN_FUNC = { MONOGREEN_FUNC = {
'gr_fg' : 'col_g5', 'gr_fg' : 'col_g5',
'gr_bg' : 'col_g1', # for distinction in testing, should be col_g0 'gr_bg' : 'col_g0', # for distinction in testing, should be col_g0
'gr_txt' : 'col_g5', 'gr_txt' : 'col_g5',
'gr_acc' : 'col_g5', 'gr_acc' : 'col_g5',
'gr_hov' : 'col_g7', 'gr_hov' : 'col_g7',
@ -64,11 +64,11 @@ RELAXED_RAW = { # "fresh" raw colors:
'col_grass' :'#7AA621', 'col_l_grass' :'#BCD982', 'col_d_grass' :'#50730B', 'col_grass' :'#7AA621', 'col_l_grass' :'#BCD982', 'col_d_grass' :'#50730B',
'col_yellow':'#BFB226', 'col_l_yellow':'#F2E985', 'col_d_yellow':'#80760D', 'col_yellow':'#BFB226', 'col_l_yellow':'#F2E985', 'col_d_yellow':'#80760D',
'col_gold' :'#A67A21', 'col_l_gold' :'#D9B46C', 'col_d_gold' :'#73500B', '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' :'#242429', 'col_gr2' :'#44444D',
'col_gr3' :'#575763', 'col_gr4' :'#7B7B8C', 'col_gr5' :'#B4B4CC', 'col_black' :'#000000', 'col_gr1' :'#161619', 'col_gr2' :'#43434D',
'col_gr3' :'#70707F', 'col_gr4' :'#9D9DB2', 'col_gr5' :'#C9C9E5',
'col_white' :'#FFFFFF' 'col_white' :'#FFFFFF'
} }
# 'col_gray' :'#666666', 'col_l_gray' :'#B6B6B6', 'col_d_gray' :'#3D3D3D',
RELAXED_DARK_FUNC= { # functional colors: RELAXED_DARK_FUNC= { # functional colors:
'gr_fg' : 'col_gr5', 'gr_fg' : 'col_gr5',
'gr_bg' : 'col_gr1', 'gr_bg' : 'col_gr1',
@ -97,6 +97,36 @@ RELAXED_DARK_PLOT = [ # plot / accent colors:
'col_l_green' '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_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'
]
def block_to_QColor( block, dic=None ): def block_to_QColor( block, dic=None ):
""" convert color information to a QColor """ """ convert color information to a QColor """
# allowed formats: # allowed formats:
@ -152,8 +182,8 @@ DEFAULT_PALETTE = assemble_palette( LEGACY_RAW, LEGACY_FUNC, LEGACY_PLOT )
def get(name): def get(name):
if name == 'relaxed_dark': if name == 'relaxed_dark':
pal = assemble_palette( RELAXED_RAW, RELAXED_DARK_FUNC, RELAXED_DARK_PLOT ) pal = assemble_palette( RELAXED_RAW, RELAXED_DARK_FUNC, RELAXED_DARK_PLOT )
# elif name == 'relaxed_light': elif name == 'relaxed_light':
# pal = assemble_palette( RELAXED_RAW, RELAXED_LIGHT_FUNC, RELAXED_LIGHT_PLOT ) pal = assemble_palette( RELAXED_RAW, RELAXED_LIGHT_FUNC, RELAXED_LIGHT_PLOT )
elif name == 'monogreen': elif name == 'monogreen':
pal = assemble_palette( MONOGREEN_RAW, MONOGREEN_FUNC, MONOGREEN_PLOT ) pal = assemble_palette( MONOGREEN_RAW, MONOGREEN_FUNC, MONOGREEN_PLOT )
else: else:

View File

@ -15,8 +15,15 @@ from .. import functions as fn
from .. import debug as debug from .. import debug as debug
from .. import getConfigOption from .. import getConfigOption
try: # prepare common definition for slot decorator across PyQt / Pyside:
QT_CORE_SLOT = QtCore.pyqtSlot
except AttributeError:
QT_CORE_SLOT = QtCore.Slot
__all__ = ['GraphicsView'] __all__ = ['GraphicsView']
DEBUG = False
class GraphicsView(QtGui.QGraphicsView): class GraphicsView(QtGui.QGraphicsView):
"""Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the
@ -93,7 +100,6 @@ class GraphicsView(QtGui.QGraphicsView):
self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate) self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate)
self.lockedViewports = [] self.lockedViewports = []
self.lastMousePos = None self.lastMousePos = None
self.setMouseTracking(True) self.setMouseTracking(True)
@ -123,6 +129,10 @@ class GraphicsView(QtGui.QGraphicsView):
self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False)
self.clickAccepted = False self.clickAccepted = False
# connect to style update signals from NamedColorManager:
fn.NAMED_COLOR_MANAGER.paletteHasChangedSignal.connect(self.styleHasChanged)
def setAntialiasing(self, aa): def setAntialiasing(self, aa):
"""Enable or disable default antialiasing. """Enable or disable default antialiasing.
Note that this will only affect items that do not specify their own antialiasing options.""" Note that this will only affect items that do not specify their own antialiasing options."""
@ -137,12 +147,17 @@ class GraphicsView(QtGui.QGraphicsView):
To use the defaults specified py pyqtgraph.setConfigOption, use background='default'. To use the defaults specified py pyqtgraph.setConfigOption, use background='default'.
To make the background transparent, use background=None. To make the background transparent, use background=None.
""" """
self._background = background
if background == 'default': if background == 'default':
# background = getConfigOption('background') # background = getConfigOption('background')
background = 'gr_bg' # default graphics background color background = 'gr_bg' # default graphics background color
brush = fn.mkBrush(background) self._background = background # maintained for compatibility
self.setBackgroundBrush(brush) if DEBUG: print(' GraphicsView: Generating BG brush for', self._background)
self._bgBrush = fn.mkBrush(self._background)
if DEBUG: print(' GraphicsView: Background color: ',self._bgBrush.color().name(), self._bgBrush.color().alpha())
self.setBackgroundBrush( self._bgBrush )
# testBrush = QtGui.QBrush( QtGui.QColor('#000000') )
# print(' test brush style:',testBrush.style() )
# self.setBackgroundBrush( testBrush )
def paintEvent(self, ev): def paintEvent(self, ev):
self.scene().prepareForPaint() self.scene().prepareForPaint()
@ -402,3 +417,10 @@ class GraphicsView(QtGui.QGraphicsView):
def dragEnterEvent(self, ev): def dragEnterEvent(self, ev):
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events ev.ignore() ## not sure why, but for some reason this class likes to consume drag events
@QT_CORE_SLOT()
def styleHasChanged(self):
""" called to trigger redraw after all named colors have been updated """
self.setBackgroundBrush( self._bgBrush )
# self.update()
if DEBUG: print(' Background update and redraw after style change:', self)

View File

@ -27,13 +27,15 @@ def test_basics_graphics_view():
assert view.scaleCenter is False assert view.scaleCenter is False
assert view.clickAccepted is False assert view.clickAccepted is False
assert view.centralWidget is not None assert view.centralWidget is not None
assert view._background == "default" # assert view._background == "default"
assert view._background == "gr_bg"
# Set background color # Set background color
# -------------------------------------- # --------------------------------------
view.setBackground("w") view.setBackground("w")
assert view._background == "w" assert view._background == "w"
assert view.backgroundBrush().color() == QtCore.Qt.white # assert view.backgroundBrush().color() == QtCore.Qt.white
assert view.backgroundBrush().color().name() == '#ffffff' #QtCore.Qt.white
# Set anti aliasing # Set anti aliasing
# -------------------------------------- # --------------------------------------