From 8c0064a3230629ab1ff1c5240179922f645b8715 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 11:58:49 -0400 Subject: [PATCH 01/60] DataTreeWidget uses TableWidget to represent arrays --- examples/DataTreeWidget.py | 2 +- pyqtgraph/widgets/DataTreeWidget.py | 34 ++++++++++------------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/examples/DataTreeWidget.py b/examples/DataTreeWidget.py index 8365db2a..2c2b00f9 100644 --- a/examples/DataTreeWidget.py +++ b/examples/DataTreeWidget.py @@ -19,7 +19,7 @@ d = { 'y': 2, 'z': 'three' }, - 'array1 (20x20)': np.ones((10,10)) + 'array1 (40x10)': np.random.randint(10, size=(40,10)) } tree = pg.DataTreeWidget(data=d) diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index 29e60319..b8215b06 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from ..pgcollections import OrderedDict +from .TableWidget import TableWidget import types, traceback import numpy as np @@ -29,12 +30,8 @@ class DataTreeWidget(QtGui.QTreeWidget): def setData(self, data, hideRoot=False): """data should be a dictionary.""" self.clear() + self.tables = [] self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) - #node = self.mkNode('', data) - #while node.childCount() > 0: - #c = node.child(0) - #node.removeChild(c) - #self.invisibleRootItem().addChild(c) self.expandToDepth(3) self.resizeColumnToContents(0) @@ -62,22 +59,15 @@ class DataTreeWidget(QtGui.QTreeWidget): elif isinstance(data, list) or isinstance(data, tuple): for i in range(len(data)): self.buildTree(data[i], node, str(i)) + elif isinstance(data, np.ndarray): + desc = "<%s shape=%s dtype=%s>" % (data.__class__.__name__, data.shape, data.dtype) + node.setText(2, desc) + subnode = QtGui.QTreeWidgetItem(["", "", ""]) + node.addChild(subnode) + table = TableWidget() + table.setData(data) + table.setMaximumHeight(200) + self.setItemWidget(subnode, 2, table) + self.tables.append(table) else: node.setText(2, str(data)) - - - #def mkNode(self, name, v): - #if type(v) is list and len(v) > 0 and isinstance(v[0], dict): - #inds = map(unicode, range(len(v))) - #v = OrderedDict(zip(inds, v)) - #if isinstance(v, dict): - ##print "\nadd tree", k, v - #node = QtGui.QTreeWidgetItem([name]) - #for k in v: - #newNode = self.mkNode(k, v[k]) - #node.addChild(newNode) - #else: - ##print "\nadd value", k, str(v) - #node = QtGui.QTreeWidgetItem([unicode(name), unicode(v)]) - #return node - From cabd9d6bf2ec61701bdc3ab927d1e9444f09e9df Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 15:21:28 -0400 Subject: [PATCH 02/60] Cleanup, better support for tracebacks in DataTreeWidget --- examples/DataTreeWidget.py | 20 +++++- pyqtgraph/widgets/DataTreeWidget.py | 96 ++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/examples/DataTreeWidget.py b/examples/DataTreeWidget.py index 2c2b00f9..70ac49bd 100644 --- a/examples/DataTreeWidget.py +++ b/examples/DataTreeWidget.py @@ -11,15 +11,29 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np +# for generating a traceback object to display +def some_func1(): + return some_func2() +def some_func2(): + try: + raise Exception() + except: + import sys + return sys.exc_info()[2] + + app = QtGui.QApplication([]) d = { - 'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], - 'dict1': { + 'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { 'x': 1, 'y': 2, 'z': 'three' }, - 'array1 (40x10)': np.random.randint(10, size=(40,10)) + 'an array': np.random.randint(10, size=(40,10)), + 'a traceback': some_func1(), + 'a function': some_func1, + 'a class': pg.DataTreeWidget, } tree = pg.DataTreeWidget(data=d) diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index b8215b06..f960cab9 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -2,6 +2,7 @@ from ..Qt import QtGui, QtCore from ..pgcollections import OrderedDict from .TableWidget import TableWidget +from ..python2_3 import asUnicode import types, traceback import numpy as np @@ -18,19 +19,18 @@ class DataTreeWidget(QtGui.QTreeWidget): Widget for displaying hierarchical python data structures (eg, nested dicts, lists, and arrays) """ - - def __init__(self, parent=None, data=None): QtGui.QTreeWidget.__init__(self, parent) self.setVerticalScrollMode(self.ScrollPerPixel) self.setData(data) self.setColumnCount(3) self.setHeaderLabels(['key / index', 'type', 'value']) + self.setAlternatingRowColors(True) def setData(self, data, hideRoot=False): """data should be a dictionary.""" self.clear() - self.tables = [] + self.widgets = [] self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) self.expandToDepth(3) self.resizeColumnToContents(0) @@ -39,35 +39,75 @@ class DataTreeWidget(QtGui.QTreeWidget): if hideRoot: node = parent else: - typeStr = type(data).__name__ - if typeStr == 'instance': - typeStr += ": " + data.__class__.__name__ - node = QtGui.QTreeWidgetItem([name, typeStr, ""]) + node = QtGui.QTreeWidgetItem([name, "", ""]) parent.addChild(node) - - if isinstance(data, types.TracebackType): ## convert traceback to a list of strings - data = list(map(str.strip, traceback.format_list(traceback.extract_tb(data)))) - elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): - data = { - 'data': data.view(np.ndarray), - 'meta': data.infoCopy() - } - - if isinstance(data, dict): - for k in data.keys(): - self.buildTree(data[k], node, str(k)) - elif isinstance(data, list) or isinstance(data, tuple): - for i in range(len(data)): - self.buildTree(data[i], node, str(i)) - elif isinstance(data, np.ndarray): - desc = "<%s shape=%s dtype=%s>" % (data.__class__.__name__, data.shape, data.dtype) - node.setText(2, desc) + + typeStr, desc, childs, widget = self.parse(data) + node.setText(1, typeStr) + node.setText(2, desc) + if widget is not None: + self.widgets.append(widget) subnode = QtGui.QTreeWidgetItem(["", "", ""]) node.addChild(subnode) + self.setItemWidget(subnode, 0, widget) + self.setFirstItemColumnSpanned(subnode, True) + + for name, data in childs.items(): + self.buildTree(data, node, asUnicode(name)) + + def parse(self, data): + """ + Given any python object, return: + * type + * a short string representation + * a dict of sub-objects to be parsed + * optional widget to display as sub-node + """ + # defaults for all objects + typeStr = type(data).__name__ + if typeStr == 'instance': + typeStr += ": " + data.__class__.__name__ + widget = None + desc = "" + childs = {} + + # type-specific changes + if isinstance(data, dict): + desc = "length=%d" % len(data) + if isinstance(data, OrderedDict): + childs = data + else: + childs = OrderedDict(sorted(data.items())) + elif isinstance(data, (list, tuple)): + desc = "length=%d" % len(data) + childs = OrderedDict(enumerate(data)) + elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): + childs = OrderedDict([ + ('data', data.view(np.ndarray)), + ('meta', data.infoCopy()) + ]) + elif isinstance(data, np.ndarray): + desc = "shape=%s dtype=%s" % (data.shape, data.dtype) table = TableWidget() table.setData(data) table.setMaximumHeight(200) - self.setItemWidget(subnode, 2, table) - self.tables.append(table) + widget = table + elif isinstance(data, types.TracebackType): ## convert traceback to a list of strings + frames = list(map(str.strip, traceback.format_list(traceback.extract_tb(data)))) + #childs = OrderedDict([ + #(i, {'file': child[0], 'line': child[1], 'function': child[2], 'code': child[3]}) + #for i, child in enumerate(frames)]) + #childs = OrderedDict([(i, ch) for i,ch in enumerate(frames)]) + widget = QtGui.QPlainTextEdit(asUnicode('\n'.join(frames))) + widget.setMaximumHeight(200) + widget.setReadOnly(True) else: - node.setText(2, str(data)) + desc = asUnicode(data) + if len(desc) > 100: + desc = desc[:97] + '...' + widget = QtGui.QPlainTextEdit(asUnicode(data)) + widget.setMaximumHeight(200) + widget.setReadOnly(True) + + return typeStr, desc, childs, widget + \ No newline at end of file From 4543031ac59eed15adf6a39c82ffb73197a36800 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 17:26:06 -0400 Subject: [PATCH 03/60] Added DiffTreeWidget --- examples/DiffTreeWidget.py | 52 +++++++++ pyqtgraph/__init__.py | 1 + pyqtgraph/widgets/DataTreeWidget.py | 27 +++-- pyqtgraph/widgets/DiffTreeWidget.py | 163 ++++++++++++++++++++++++++++ pyqtgraph/widgets/TableWidget.py | 3 +- 5 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 examples/DiffTreeWidget.py create mode 100644 pyqtgraph/widgets/DiffTreeWidget.py diff --git a/examples/DiffTreeWidget.py b/examples/DiffTreeWidget.py new file mode 100644 index 00000000..fa57a356 --- /dev/null +++ b/examples/DiffTreeWidget.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +""" +Simple use of DiffTreeWidget to display differences between structures of +nested dicts, lists, and arrays. +""" + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +app = QtGui.QApplication([]) +A = { + 'a list': [1,2,2,4,5,6, {'nested1': 'aaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { + 'x': 1, + 'y': 2, + 'z': 'three' + }, + 'an array': np.random.randint(10, size=(40,10)), + #'a traceback': some_func1(), + #'a function': some_func1, + #'a class': pg.DataTreeWidget, +} + +B = { + 'a list': [1,2,3,4,5,5, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { + 'x': 2, + 'y': 2, + 'z': 'three', + 'w': 5 + }, + 'another dict': {1:2, 2:3, 3:4}, + 'an array': np.random.randint(10, size=(40,10)), +} + +tree = pg.DiffTreeWidget() +tree.setData(A, B) +tree.show() +tree.setWindowTitle('pyqtgraph example: DiffTreeWidget') +tree.resize(1000, 800) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() \ No newline at end of file diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f8983455..de9fb278 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -240,6 +240,7 @@ from .widgets.VerticalLabel import * from .widgets.FeedbackButton import * from .widgets.ColorButton import * from .widgets.DataTreeWidget import * +from .widgets.DiffTreeWidget import * from .widgets.GraphicsView import * from .widgets.LayoutWidget import * from .widgets.TableWidget import * diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index f960cab9..39cb0d45 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -31,20 +31,35 @@ class DataTreeWidget(QtGui.QTreeWidget): """data should be a dictionary.""" self.clear() self.widgets = [] + self.nodes = {} self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) self.expandToDepth(3) self.resizeColumnToContents(0) - def buildTree(self, data, parent, name='', hideRoot=False): + def buildTree(self, data, parent, name='', hideRoot=False, path=()): if hideRoot: node = parent else: node = QtGui.QTreeWidgetItem([name, "", ""]) parent.addChild(node) + + # record the path to the node so it can be retrieved later + # (this is used by DiffTreeWidget) + self.nodes[path] = node typeStr, desc, childs, widget = self.parse(data) node.setText(1, typeStr) node.setText(2, desc) + + # Truncate description and add text box if needed + if len(desc) > 100: + desc = desc[:97] + '...' + if widget is None: + widget = QtGui.QPlainTextEdit(asUnicode(data)) + widget.setMaximumHeight(200) + widget.setReadOnly(True) + + # Add widget to new subnode if widget is not None: self.widgets.append(widget) subnode = QtGui.QTreeWidgetItem(["", "", ""]) @@ -52,8 +67,9 @@ class DataTreeWidget(QtGui.QTreeWidget): self.setItemWidget(subnode, 0, widget) self.setFirstItemColumnSpanned(subnode, True) - for name, data in childs.items(): - self.buildTree(data, node, asUnicode(name)) + # recurse to children + for key, data in childs.items(): + self.buildTree(data, node, asUnicode(key), path=path+(key,)) def parse(self, data): """ @@ -103,11 +119,6 @@ class DataTreeWidget(QtGui.QTreeWidget): widget.setReadOnly(True) else: desc = asUnicode(data) - if len(desc) > 100: - desc = desc[:97] + '...' - widget = QtGui.QPlainTextEdit(asUnicode(data)) - widget.setMaximumHeight(200) - widget.setReadOnly(True) return typeStr, desc, childs, widget \ No newline at end of file diff --git a/pyqtgraph/widgets/DiffTreeWidget.py b/pyqtgraph/widgets/DiffTreeWidget.py new file mode 100644 index 00000000..e3869da8 --- /dev/null +++ b/pyqtgraph/widgets/DiffTreeWidget.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +from ..Qt import QtGui, QtCore +from ..pgcollections import OrderedDict +from .DataTreeWidget import DataTreeWidget +from .. import functions as fn +import types, traceback +import numpy as np + +__all__ = ['DiffTreeWidget'] + + +class DiffTreeWidget(QtGui.QWidget): + """ + Widget for displaying differences between hierarchical python data structures + (eg, nested dicts, lists, and arrays) + """ + def __init__(self, parent=None, a=None, b=None): + QtGui.QWidget.__init__(self, parent) + self.layout = QtGui.QHBoxLayout() + self.setLayout(self.layout) + self.trees = [DataTreeWidget(self), DataTreeWidget(self)] + for t in self.trees: + self.layout.addWidget(t) + if a is not None: + self.setData(a, b) + + def setData(self, a, b): + """ + Set the data to be compared in this widget. + """ + self.data = (a, b) + self.trees[0].setData(a) + self.trees[1].setData(b) + + return self.compare(a, b) + + def compare(self, a, b, path=()): + """ + Compare data structure *a* to structure *b*. + + Return True if the objects match completely. + Otherwise, return a structure that describes the differences: + + { 'type': bool + 'len': bool, + 'str': bool, + 'shape': bool, + 'dtype': bool, + 'mask': array, + } + + + """ + bad = (255, 200, 200) + diff = [] + # generate typestr, desc, childs for each object + typeA, descA, childsA, _ = self.trees[0].parse(a) + typeB, descB, childsB, _ = self.trees[1].parse(b) + + if typeA != typeB: + self.setColor(path, 1, bad) + if descA != descB: + self.setColor(path, 2, bad) + + if isinstance(a, dict) and isinstance(b, dict): + keysA = set(a.keys()) + keysB = set(b.keys()) + for key in keysA - keysB: + self.setColor(path+(key,), 0, bad, tree=0) + for key in keysB - keysA: + self.setColor(path+(key,), 0, bad, tree=1) + for key in keysA & keysB: + self.compare(a[key], b[key], path+(key,)) + + elif isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): + for i in range(max(len(a), len(b))): + if len(a) <= i: + self.setColor(path+(i,), 0, bad, tree=1) + elif len(b) <= i: + self.setColor(path+(i,), 0, bad, tree=0) + else: + self.compare(a[i], b[i], path+(i,)) + + elif isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and a.shape == b.shape: + tableNodes = [tree.nodes[path].child(0) for tree in self.trees] + if a.dtype.fields is None and b.dtype.fields is None: + eq = self.compareArrays(a, b) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + #for i in np.argwhere(~eq): + + else: + for i,k in enumerate(info.dtype.fields.keys()): + eq = self.compareArrays(a[k], b[k]) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + #for j in np.argwhere(~eq): + + # dict: compare keys, then values where keys match + # list: + # array: compare elementwise for same shape + + def compareArrays(self, a, b): + intnan = -9223372036854775808 # happens when np.nan is cast to int + anans = np.isnan(a) | (a == intnan) + bnans = np.isnan(b) | (b == intnan) + eq = anans == bnans + mask = ~anans + eq[mask] = np.allclose(a[mask], b[mask]) + return eq + + def setColor(self, path, column, color, tree=None): + brush = fn.mkBrush(color) + + # Color only one tree if specified. + if tree is None: + trees = self.trees + else: + trees = [self.trees[tree]] + + for tree in trees: + item = tree.nodes[path] + item.setBackground(column, brush) + + def _compare(self, a, b): + """ + Compare data structure *a* to structure *b*. + """ + # Check test structures are the same + assert type(info) is type(expect) + if hasattr(info, '__len__'): + assert len(info) == len(expect) + + if isinstance(info, dict): + for k in info: + assert k in expect + for k in expect: + assert k in info + self.compare_results(info[k], expect[k]) + elif isinstance(info, list): + for i in range(len(info)): + self.compare_results(info[i], expect[i]) + elif isinstance(info, np.ndarray): + assert info.shape == expect.shape + assert info.dtype == expect.dtype + if info.dtype.fields is None: + intnan = -9223372036854775808 # happens when np.nan is cast to int + inans = np.isnan(info) | (info == intnan) + enans = np.isnan(expect) | (expect == intnan) + assert np.all(inans == enans) + mask = ~inans + assert np.allclose(info[mask], expect[mask]) + else: + for k in info.dtype.fields.keys(): + self.compare_results(info[k], expect[k]) + else: + try: + assert info == expect + except Exception: + raise NotImplementedError("Cannot compare objects of type %s" % type(info)) + \ No newline at end of file diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 69085a20..db0b6ae2 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -151,7 +151,8 @@ class TableWidget(QtGui.QTableWidget): i += 1 self.setRow(i, [x for x in fn1(row)]) - if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount(): + if (self._sorting and self.horizontalHeadersSet and + self.horizontalHeader().sortIndicatorSection() >= self.columnCount()): self.sortByColumn(0, QtCore.Qt.AscendingOrder) def setEditable(self, editable=True): From 9893fc37353a8f1b02637fb4835a9196df52945b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 26 Sep 2014 15:25:05 -0400 Subject: [PATCH 04/60] corrections for 1d arrays --- pyqtgraph/widgets/DiffTreeWidget.py | 13 +++++++------ pyqtgraph/widgets/TableWidget.py | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/widgets/DiffTreeWidget.py b/pyqtgraph/widgets/DiffTreeWidget.py index e3869da8..eac29489 100644 --- a/pyqtgraph/widgets/DiffTreeWidget.py +++ b/pyqtgraph/widgets/DiffTreeWidget.py @@ -91,12 +91,13 @@ class DiffTreeWidget(QtGui.QWidget): #for i in np.argwhere(~eq): else: - for i,k in enumerate(info.dtype.fields.keys()): - eq = self.compareArrays(a[k], b[k]) - if not np.all(eq): - for n in tableNodes: - n.setBackground(0, fn.mkBrush(bad)) - #for j in np.argwhere(~eq): + if a.dtype == b.dtype: + for i,k in enumerate(a.dtype.fields.keys()): + eq = self.compareArrays(a[k], b[k]) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + #for j in np.argwhere(~eq): # dict: compare keys, then values where keys match # list: diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index db0b6ae2..9e9f2144 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -222,6 +222,8 @@ class TableWidget(QtGui.QTableWidget): return self.iterate, list(map(asUnicode, data.dtype.names)) elif data is None: return (None,None) + elif np.isscalar(data): + return self.iterateScalar, None else: msg = "Don't know how to iterate over data type: {!s}".format(type(data)) raise TypeError(msg) @@ -236,6 +238,9 @@ class TableWidget(QtGui.QTableWidget): for x in data: yield x + def iterateScalar(self, data): + yield data + def appendRow(self, data): self.appendData([data]) From 2dc31b53dae47cef7eb3d84d1406a7b727cf1e9e Mon Sep 17 00:00:00 2001 From: Justin Engel Date: Thu, 13 Jul 2017 08:25:45 -0400 Subject: [PATCH 05/60] Fixed dataBounds error when all values were inf. If all values are inf. d = d[mask] will create an empty array. You cannot call min or max on an empty array. --- pyqtgraph/graphicsItems/PlotCurveItem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d66a8a99..4b876eb5 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -132,7 +132,11 @@ class PlotCurveItem(GraphicsObject): if any(np.isinf(b)): mask = np.isfinite(d) d = d[mask] - b = (d.min(), d.max()) + try: + b = (d.min(), d.max()) + except ValueError: + # d has no size, because all of d is inf. + b = (-1, 1) # Some default bounds elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) From 82b666e2ee0e5c9ae4bd8aca37f2ae5fda35a236 Mon Sep 17 00:00:00 2001 From: Matthew Shun-Shin Date: Fri, 28 Jul 2017 11:30:19 +0100 Subject: [PATCH 06/60] Fix GL Views being half size on hidpi monitors --- pyqtgraph/opengl/GLViewWidget.py | 4 ++-- pyqtgraph/widgets/RawImageWidget.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index e0fee046..40a7d858 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -80,7 +80,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def getViewport(self): vp = self.opts['viewport'] if vp is None: - return (0, 0, self.width(), self.height()) + return (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) else: return vp @@ -99,7 +99,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def projectionMatrix(self, region=None): # Xw = (Xnd + 1) * width/2 + X if region is None: - region = (0, 0, self.width(), self.height()) + region = (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) x0, y0, w, h = self.getViewport() dist = self.opts['distance'] diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index 657701f9..ef1d7a38 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -122,7 +122,7 @@ if HAVE_OPENGL: if not self.uploaded: self.uploadTexture() - glViewport(0, 0, self.width(), self.height()) + glViewport(0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) glColor4f(1,1,1,1) From 72a715753853267f3513cae799ccca831f4f9b29 Mon Sep 17 00:00:00 2001 From: Alexander Manakov Date: Fri, 4 Aug 2017 13:56:37 +0300 Subject: [PATCH 07/60] Added essential if. --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 597491f3..f426c91c 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -648,6 +648,9 @@ class ScatterPlotItem(GraphicsObject): d = d[mask] d2 = d2[mask] + if d.size == 0: + return (None, None) + if frac >= 1.0: self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072) return self.bounds[ax] From 70831245a838e6471ebe493c4587f208dcb10baa Mon Sep 17 00:00:00 2001 From: HashSplat Date: Tue, 8 Aug 2017 14:58:33 -0400 Subject: [PATCH 08/60] Fixed issue where ImageItem data size would be 0 due to colormap. --- pyqtgraph/graphicsItems/ImageItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 3d45ad77..706735bd 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -401,7 +401,8 @@ class ImageItem(GraphicsObject): image = image.transpose((1, 0, 2)[:image.ndim]) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) - self.qimage = fn.makeQImage(argb, alpha, transpose=False) + if argb.size > 0: + self.qimage = fn.makeQImage(argb, alpha, transpose=False) def paint(self, p, *args): profile = debug.Profiler() From e7ba0ca272122952dc2bdbb62bde8df51e6e6966 Mon Sep 17 00:00:00 2001 From: HashSplat Date: Tue, 8 Aug 2017 15:35:54 -0400 Subject: [PATCH 09/60] Found that downsample would set the image size to 0 causing errors. --- pyqtgraph/graphicsItems/ImageItem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 706735bd..411eaf5a 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -368,6 +368,10 @@ class ImageItem(GraphicsObject): image = fn.downsample(self.image, xds, axis=axes[0]) image = fn.downsample(image, yds, axis=axes[1]) self._lastDownsample = (xds, yds) + + # Check if downsampling reduced the image size to zero due to inf values. + if image.size == 0: + return else: image = self.image @@ -401,8 +405,7 @@ class ImageItem(GraphicsObject): image = image.transpose((1, 0, 2)[:image.ndim]) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) - if argb.size > 0: - self.qimage = fn.makeQImage(argb, alpha, transpose=False) + self.qimage = fn.makeQImage(argb, alpha, transpose=False) def paint(self, p, *args): profile = debug.Profiler() From baba93dc604a5aaf27a7c6fd0c008e2ea22073dd Mon Sep 17 00:00:00 2001 From: Justin Engel Date: Tue, 8 Aug 2017 15:51:32 -0400 Subject: [PATCH 10/60] temporarily removed min max fix for pull request --- pyqtgraph/graphicsItems/PlotCurveItem.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 4b876eb5..d66a8a99 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -132,11 +132,7 @@ class PlotCurveItem(GraphicsObject): if any(np.isinf(b)): mask = np.isfinite(d) d = d[mask] - try: - b = (d.min(), d.max()) - except ValueError: - # d has no size, because all of d is inf. - b = (-1, 1) # Some default bounds + b = (d.min(), d.max()) elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) From 4b188c73b0515d6a391d5b5f2c9ae4bea526aa5b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:09:35 -0700 Subject: [PATCH 11/60] Add disconnect() and SignalBlock - make it possible to retrieve previous versions of reloaded objects (needed by disconnect) --- pyqtgraph/functions.py | 46 ++++++++++++++++++++++++++++++++++++++++-- pyqtgraph/reload.py | 44 +++++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1aed6ace..4b4231c8 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -14,8 +14,8 @@ import sys, struct from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, USE_PYSIDE from . import getConfigOption, setConfigOptions -from . import debug - +from . import debug, reload +from .reload import getPreviousVersion Colors = { @@ -2412,3 +2412,45 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0): sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1)) sorted.append(n) return sorted + + +def disconnect(signal, slot): + """Disconnect a Qt signal from a slot. + + This method augments Qt's Signal.disconnect(): + + * Return bool indicating whether disconnection was successful, rather than + raising an exception + * Attempt to disconnect prior versions of the slot when using pg.reload + """ + while True: + try: + signal.disconnect(slot) + return True + except TypeError, RuntimeError: + slot = reload.getPreviousVersion(slot) + if slot is None: + return False + + +class SignalBlock(object): + """Class used to temporarily block a Qt signal connection:: + + with SignalBlock(signal, slot): + # do something that emits a signal; it will + # not be delivered to slot + """ + def __init__(self, signal, slot): + self.signal = signal + self.slot = slot + + def __enter__(self): + self.reconnect = disconnect(self.signal, self.slot) + return self + + def __exit__(self, *args): + if self.reconnect: + self.signal.connect(self.slot) + + + diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index ccf83913..5aa2ed68 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -22,13 +22,14 @@ Does NOT: """ -import inspect, os, sys, gc, traceback +import inspect, os, sys, gc, traceback, types try: import __builtin__ as builtins except ImportError: import builtins from .debug import printExc + def reloadAll(prefix=None, debug=False): """Automatically reload everything whose __file__ begins with prefix. - Skips reload if the file has not been updated (if .pyc is newer than .py) @@ -97,7 +98,9 @@ def reload(module, debug=False, lists=False, dicts=False): if debug: print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new))) updateClass(old, new, debug) - + # don't put this inside updateClass because it is reentrant. + new.__previous_reload_version__ = old + elif inspect.isfunction(old): depth = updateFunction(old, new, debug) if debug: @@ -152,7 +155,6 @@ def updateFunction(old, new, debug, depth=0, visited=None): ## 1) find all instances of the old class and set instance.__class__ to the new class ## 2) update all old class methods to use code from the new class methods def updateClass(old, new, debug): - ## Track town all instances and subclasses of old refs = gc.get_referrers(old) for ref in refs: @@ -174,13 +176,20 @@ def updateClass(old, new, debug): ## This seems to work. Is there any reason not to? ## Note that every time we reload, the class hierarchy becomes more complex. ## (and I presume this may slow things down?) - ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] + newBases = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] + try: + ref.__bases__ = newBases + except TypeError: + print(" Error setting bases for class %s" % ref) + print(" old bases: %s" % repr(ref.__bases__)) + print(" new bases: %s" % repr(newBases)) + raise if debug: print(" Changed superclass for %s" % safeStr(ref)) #else: #if debug: #print " Ignoring reference", type(ref) - except: + except Exception: print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new))) raise @@ -199,6 +208,8 @@ def updateClass(old, new, debug): if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__: depth = updateFunction(oa.__func__, na.__func__, debug) + if not hasattr(na.__func__, '__previous_reload_method__'): + na.__func__.__previous_reload_method__ = oa # important for managing signal connection #oa.im_class = new ## bind old method to new class ## not allowed if debug: extra = "" @@ -208,6 +219,8 @@ def updateClass(old, new, debug): ## And copy in new functions that didn't exist previously for attr in dir(new): + if attr == '__previous_reload_version__': + continue if not hasattr(old, attr): if debug: print(" Adding missing attribute %s" % attr) @@ -223,14 +236,31 @@ def updateClass(old, new, debug): def safeStr(obj): try: s = str(obj) - except: + except Exception: try: s = repr(obj) - except: + except Exception: s = "" % (safeStr(type(obj)), id(obj)) return s +def getPreviousVersion(obj): + """Return the previous version of *obj*, or None if this object has not + been reloaded. + """ + if isinstance(obj, type) or inspect.isfunction(obj): + return getattr(obj, '__previous_reload_version__', None) + elif inspect.ismethod(obj): + if obj.im_self is None: + # unbound method + return getattr(obj.__func__, '__previous_reload_method__', None) + else: + oldmethod = getattr(obj.__func__, '__previous_reload_method__', None) + if oldmethod is None: + return None + self = obj.im_self + cls = oldmethod.im_class + return types.MethodType(oldmethod.__func__, self, cls) From db890b8ed84a7c1c66b4dd17a0969dc1d741636a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:13:56 -0700 Subject: [PATCH 12/60] Add unit test for reload(); make travis use --assert=reinterp (because assert=rewrite does not work with reload) --- .travis.yml | 3 +- pyqtgraph/tests/test_reload.py | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 pyqtgraph/tests/test_reload.py diff --git a/.travis.yml b/.travis.yml index c4a67ac3..1c075090 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,8 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov pyqtgraph -sv; + # NOTE: tests/test_reload.py breaks without --assert=reinterp|plain + PYTHONPATH=. py.test --cov --assert=reinterp pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py new file mode 100644 index 00000000..4b670fb6 --- /dev/null +++ b/pyqtgraph/tests/test_reload.py @@ -0,0 +1,74 @@ +import tempfile, os, sys, shutil, atexit +import pyqtgraph as pg +import pyqtgraph.reload +pgpath = os.path.join(os.path.dirname(pg.__file__), '..') + +# make temporary directory to write module code +path = tempfile.mkdtemp() +sys.path.insert(0, path) +def cleanup(): + shutil.rmtree(path) +atexit.register(cleanup) + + +code = """ +import sys +sys.path.append('{path}') + +import pyqtgraph as pg + +class C(pg.QtCore.QObject): + sig = pg.QtCore.Signal() + def fn(self): + print("{msg}") + +""" + +def test_reload(): + # write a module + mod = os.path.join(path, 'reload_test.py') + open(mod, 'w').write(code.format(path=path, msg="C.fn() Version1")) + + # import the new module + import reload_test + + c = reload_test.C() + c.sig.connect(c.fn) + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + + + + # write again and reload + open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + os.remove(mod+'c') + pg.reload.reloadAll(path, debug=True) + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + + assert c.fn.im_class is v2[0] + oldcfn = pg.reload.getPreviousVersion(c.fn) + assert oldcfn.im_class is v1[0] + assert oldcfn.im_func is v1[2].im_func + assert oldcfn.im_self is c + + + + # write again and reload + open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + os.remove(mod+'c') + pg.reload.reloadAll(path, debug=True) + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + + #for i in range(len(old)): + #print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i] + + cfn1 = pg.reload.getPreviousVersion(c.fn) + cfn2 = pg.reload.getPreviousVersion(cfn1) + + assert cfn1.im_class is v2[0] + assert cfn1.im_func is v2[2].im_func + assert cfn1.im_self is c + assert cfn2.im_class is v1[0] + assert cfn2.im_func is v1[2].im_func + assert cfn2.im_self is c + c.sig.disconnect(cfn2) + From 4bca2ae87916cc21bac659db1a72d5253dd55155 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:39:26 -0700 Subject: [PATCH 13/60] fix reload test pycache removal Ad a warning about using assert=rewrite --- pyqtgraph/tests/test_reload.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 4b670fb6..3f2195e2 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -24,6 +24,14 @@ class C(pg.QtCore.QObject): """ +def remove_cache(mod): + if os.path.isfile(mod+'c'): + os.remove(mod+'c') + cachedir = os.path.join(os.path.dirname(mod), '__pycache__') + if os.path.isdir(cachedir): + shutil.rmtree(cachedir) + + def test_reload(): # write a module mod = os.path.join(path, 'reload_test.py') @@ -40,12 +48,16 @@ def test_reload(): # write again and reload open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) - os.remove(mod+'c') + remove_cache(mod) pg.reload.reloadAll(path, debug=True) v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) assert c.fn.im_class is v2[0] oldcfn = pg.reload.getPreviousVersion(c.fn) + if oldcfn is None: + # Function did not reload; are we using pytest's assertion rewriting? + raise Exception("Function did not reload. (This can happen when using py.test" + " with assertion rewriting; use --assert=reinterp or --assert=plain)") assert oldcfn.im_class is v1[0] assert oldcfn.im_func is v1[2].im_func assert oldcfn.im_self is c @@ -54,7 +66,7 @@ def test_reload(): # write again and reload open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) - os.remove(mod+'c') + remove_cache(mod) pg.reload.reloadAll(path, debug=True) v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) From 1bd97c67e29c1c642bc9a41e0413f7723d638172 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:42:59 -0700 Subject: [PATCH 14/60] Fix travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1c075090..0a6cf699 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,6 @@ script: # Run unit tests - start_test "unit tests"; - # NOTE: tests/test_reload.py breaks without --assert=reinterp|plain PYTHONPATH=. py.test --cov --assert=reinterp pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" From 7e5b40d265f1e8b0fa2c2b9c96a4d91ef9571599 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:58:42 -0700 Subject: [PATCH 15/60] Switch to assert=plain; apparently assert=reinterp is no longer supported --- .travis.yml | 2 +- pyqtgraph/tests/test_reload.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a6cf699..4478f0a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov --assert=reinterp pyqtgraph -sv; + PYTHONPATH=. py.test --cov --assert=plain pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 3f2195e2..7ceed546 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -57,7 +57,7 @@ def test_reload(): if oldcfn is None: # Function did not reload; are we using pytest's assertion rewriting? raise Exception("Function did not reload. (This can happen when using py.test" - " with assertion rewriting; use --assert=reinterp or --assert=plain)") + " with assertion rewriting; use --assert=plain for this test.)") assert oldcfn.im_class is v1[0] assert oldcfn.im_func is v1[2].im_func assert oldcfn.im_self is c From f77d7409ba368edaf63bc9e41e47c1683b94c6e5 Mon Sep 17 00:00:00 2001 From: HashSplat Date: Tue, 12 Sep 2017 17:15:26 -0400 Subject: [PATCH 16/60] Fixed legend size after remove item When the legend removed an item with a large label name the legend would not shrink in width. This fix uses the sample and label minimum width to allow the legend to shrink when an item is removed. --- pyqtgraph/graphicsItems/LegendItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 20d6416e..527b814a 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -110,7 +110,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): #print("-------") for sample, label in self.items: height += max(sample.height(), label.height()) + 3 - width = max(width, sample.width()+label.width()) + width = max(width, (sample.sizeHint(QtCore.Qt.MinimumSize, sample.size()).width() + + label.sizeHint(QtCore.Qt.MinimumSize, label.size()).width())) #print(width, height) #print width, height self.setGeometry(0, 0, width+25, height) From 8a956bfddb3d31cadc80ab392995ddf6de22d507 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Fri, 2 Feb 2018 22:40:06 +0800 Subject: [PATCH 17/60] Add main window title for the examples --- examples/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/__main__.py b/examples/__main__.py index 03c41119..6b8a07f9 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -26,6 +26,7 @@ class ExampleLoader(QtGui.QMainWindow): self.cw = QtGui.QWidget() self.setCentralWidget(self.cw) self.ui.setupUi(self.cw) + self.setWindowTitle("Examples") self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() From 81562b02528072e4bac98152bbbc07704ae9e431 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Sat, 3 Feb 2018 10:16:09 +0800 Subject: [PATCH 18/60] Refactor MultiPlotSpeedTest.py * Change variable to meaningful name, * Remove the outdated commented out code. --- examples/MultiPlotSpeedTest.py | 40 ++++++++++++++-------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/examples/MultiPlotSpeedTest.py b/examples/MultiPlotSpeedTest.py index 0d0d701b..f4295687 100644 --- a/examples/MultiPlotSpeedTest.py +++ b/examples/MultiPlotSpeedTest.py @@ -12,32 +12,27 @@ from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg from pyqtgraph.ptime import time -#QtGui.QApplication.setGraphicsSystem('raster') app = QtGui.QApplication([]) -#mw = QtGui.QMainWindow() -#mw.resize(800,800) -p = pg.plot() -p.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest') -#p.setRange(QtCore.QRectF(0, -10, 5000, 20)) -p.setLabel('bottom', 'Index', units='B') +plot = pg.plot() +plot.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest') +plot.setLabel('bottom', 'Index', units='B') nPlots = 100 nSamples = 500 -#curves = [p.plot(pen=(i,nPlots*1.3)) for i in range(nPlots)] curves = [] -for i in range(nPlots): - c = pg.PlotCurveItem(pen=(i,nPlots*1.3)) - p.addItem(c) - c.setPos(0,i*6) - curves.append(c) +for idx in range(nPlots): + curve = pg.PlotCurveItem(pen=(idx,nPlots*1.3)) + plot.addItem(curve) + curve.setPos(0,idx*6) + curves.append(curve) -p.setYRange(0, nPlots*6) -p.setXRange(0, nSamples) -p.resize(600,900) +plot.setYRange(0, nPlots*6) +plot.setXRange(0, nSamples) +plot.resize(600,900) rgn = pg.LinearRegionItem([nSamples/5.,nSamples/3.]) -p.addItem(rgn) +plot.addItem(rgn) data = np.random.normal(size=(nPlots*23,nSamples)) @@ -46,13 +41,12 @@ lastTime = time() fps = None count = 0 def update(): - global curve, data, ptr, p, lastTime, fps, nPlots, count + global curve, data, ptr, plot, lastTime, fps, nPlots, count count += 1 - #print "---------", count + for i in range(nPlots): curves[i].setData(data[(ptr+i)%data.shape[0]]) - - #print " setData done." + ptr += nPlots now = time() dt = now - lastTime @@ -62,13 +56,11 @@ def update(): else: s = np.clip(dt*3., 0, 1) fps = fps * (1-s) + (1.0/dt) * s - p.setTitle('%0.2f fps' % fps) + plot.setTitle('%0.2f fps' % fps) #app.processEvents() ## force complete redraw for every plot timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) - - ## Start Qt event loop unless running in interactive mode. if __name__ == '__main__': From f3d5273f81ba804871b581e1584175201facae6f Mon Sep 17 00:00:00 2001 From: Fekete Imre Date: Wed, 7 Feb 2018 14:25:15 +0100 Subject: [PATCH 19/60] Cylinder now generates the correct amount of sides Before the first and the last vertex was overlapping. --- pyqtgraph/opengl/MeshData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index f83fcdf6..5bab4626 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -485,7 +485,7 @@ class MeshData(object): if isinstance(radius, int): radius = [radius, radius] # convert to list ## compute vertexes - th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols) + th = np.linspace(2 * np.pi, (2 * np.pi)/cols, cols).reshape(1, cols) r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z if offset: From afd8a6f423cb236bdcd9565ed1d893ec427e2fa7 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Fri, 16 Feb 2018 12:15:32 +0800 Subject: [PATCH 20/60] Replace deprecate class in examples Using class GraphicsLayoutWidget to replace the deprecated class GraphicsWindow, cc #631. --- examples/CustomGraphItem.py | 2 +- examples/GraphItem.py | 2 +- examples/InfiniteLine.py | 2 +- examples/LogPlotTest.py | 2 +- examples/PanningPlot.py | 2 +- examples/PlotAutoRange.py | 2 +- examples/Plotting.py | 2 +- examples/ROIExamples.py | 2 +- examples/ROItypes.py | 2 +- examples/ScaleBar.py | 2 +- examples/Symbols.py | 2 +- examples/ViewBoxFeatures.py | 2 +- examples/contextMenu.py | 2 +- examples/crosshair.py | 2 +- examples/histogram.py | 2 +- examples/isocurve.py | 2 +- examples/linkedViews.py | 2 +- examples/logAxis.py | 2 +- examples/optics_demos.py | 2 +- examples/scrollingPlots.py | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/CustomGraphItem.py b/examples/CustomGraphItem.py index 695768e2..8e494c3a 100644 --- a/examples/CustomGraphItem.py +++ b/examples/CustomGraphItem.py @@ -12,7 +12,7 @@ import numpy as np # Enable antialiasing for prettier plots pg.setConfigOptions(antialias=True) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: CustomGraphItem') v = w.addViewBox() v.setAspectLocked() diff --git a/examples/GraphItem.py b/examples/GraphItem.py index c6362295..094b84bd 100644 --- a/examples/GraphItem.py +++ b/examples/GraphItem.py @@ -13,7 +13,7 @@ import numpy as np # Enable antialiasing for prettier plots pg.setConfigOptions(antialias=True) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: GraphItem') v = w.addViewBox() v.setAspectLocked() diff --git a/examples/InfiniteLine.py b/examples/InfiniteLine.py index 50efbd04..55020776 100644 --- a/examples/InfiniteLine.py +++ b/examples/InfiniteLine.py @@ -10,7 +10,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Plotting items examples") +win = pg.GraphicsLayoutWidget(show=True, title="Plotting items examples") win.resize(1000,600) # Enable antialiasing for prettier plots diff --git a/examples/LogPlotTest.py b/examples/LogPlotTest.py index d408a2b4..5ae9d17e 100644 --- a/examples/LogPlotTest.py +++ b/examples/LogPlotTest.py @@ -12,7 +12,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Basic plotting examples") +win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples") win.resize(1000,600) win.setWindowTitle('pyqtgraph example: LogPlotTest') diff --git a/examples/PanningPlot.py b/examples/PanningPlot.py index 165240b2..874bf330 100644 --- a/examples/PanningPlot.py +++ b/examples/PanningPlot.py @@ -9,7 +9,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: PanningPlot') plt = win.addPlot() diff --git a/examples/PlotAutoRange.py b/examples/PlotAutoRange.py index 46aa3a44..0e3cd422 100644 --- a/examples/PlotAutoRange.py +++ b/examples/PlotAutoRange.py @@ -16,7 +16,7 @@ app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() #mw.resize(800,800) -win = pg.GraphicsWindow(title="Plot auto-range examples") +win = pg.GraphicsLayoutWidget(show=True, title="Plot auto-range examples") win.resize(800,600) win.setWindowTitle('pyqtgraph example: PlotAutoRange') diff --git a/examples/Plotting.py b/examples/Plotting.py index 44996ae5..130698a4 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -17,7 +17,7 @@ app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() #mw.resize(800,800) -win = pg.GraphicsWindow(title="Basic plotting examples") +win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples") win.resize(1000,600) win.setWindowTitle('pyqtgraph example: Plotting') diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index a48fa7b5..2b922359 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -33,7 +33,7 @@ arr[8:13, 44:46] = 10 ## create GUI app = QtGui.QApplication([]) -w = pg.GraphicsWindow(size=(1000,800), border=True) +w = pg.GraphicsLayoutWidget(show=True, size=(1000,800), border=True) w.setWindowTitle('pyqtgraph example: ROI Examples') text = """Data Selection From Image.
\n diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 9e67ebe1..1a064d33 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -13,7 +13,7 @@ pg.setConfigOptions(imageAxisOrder='row-major') ## create GUI app = QtGui.QApplication([]) -w = pg.GraphicsWindow(size=(800,800), border=True) +w = pg.GraphicsLayoutWidget(show=True, size=(800,800), border=True) v = w.addViewBox(colspan=2) v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) diff --git a/examples/ScaleBar.py b/examples/ScaleBar.py index 5f9675e4..f125eb73 100644 --- a/examples/ScaleBar.py +++ b/examples/ScaleBar.py @@ -9,7 +9,7 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np pg.mkQApp() -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: ScaleBar') vb = win.addViewBox() diff --git a/examples/Symbols.py b/examples/Symbols.py index 3dd28e13..417df35e 100755 --- a/examples/Symbols.py +++ b/examples/Symbols.py @@ -11,7 +11,7 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Scatter Plot Symbols") +win = pg.GraphicsLayoutWidget(show=True, title="Scatter Plot Symbols") win.resize(1000,600) pg.setConfigOptions(antialias=True) diff --git a/examples/ViewBoxFeatures.py b/examples/ViewBoxFeatures.py index 6388e41b..5757924b 100644 --- a/examples/ViewBoxFeatures.py +++ b/examples/ViewBoxFeatures.py @@ -16,7 +16,7 @@ x = np.arange(1000, dtype=float) y = np.random.normal(size=1000) y += 5 * np.sin(x/100) -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: ____') win.resize(1000, 800) win.ci.setBorder((50, 50, 100)) diff --git a/examples/contextMenu.py b/examples/contextMenu.py index c2c5918d..c08008aa 100644 --- a/examples/contextMenu.py +++ b/examples/contextMenu.py @@ -14,7 +14,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: context menu') diff --git a/examples/crosshair.py b/examples/crosshair.py index 076fab49..584eced8 100644 --- a/examples/crosshair.py +++ b/examples/crosshair.py @@ -13,7 +13,7 @@ from pyqtgraph.Point import Point #generate layout app = QtGui.QApplication([]) -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: crosshair') label = pg.LabelItem(justify='right') win.addItem(label) diff --git a/examples/histogram.py b/examples/histogram.py index 2674ba30..a25f0947 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -8,7 +8,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.resize(800,350) win.setWindowTitle('pyqtgraph example: Histogram') plt1 = win.addPlot() diff --git a/examples/isocurve.py b/examples/isocurve.py index b401dfe1..6d89bbec 100644 --- a/examples/isocurve.py +++ b/examples/isocurve.py @@ -20,7 +20,7 @@ data = np.concatenate([data, data], axis=0) data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] data[:, 15:16, 15:17] += 1 -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: Isocurve') vb = win.addViewBox() img = pg.ImageItem(data[0]) diff --git a/examples/linkedViews.py b/examples/linkedViews.py index e7eb18af..34f2b698 100644 --- a/examples/linkedViews.py +++ b/examples/linkedViews.py @@ -20,7 +20,7 @@ app = QtGui.QApplication([]) x = np.linspace(-50, 50, 1000) y = np.sin(x) / x -win = pg.GraphicsWindow(title="pyqtgraph example: Linked Views") +win = pg.GraphicsLayoutWidget(show=True, title="pyqtgraph example: Linked Views") win.resize(800,600) win.addLabel("Linked Views", colspan=2) diff --git a/examples/logAxis.py b/examples/logAxis.py index a0c7fc53..3b30c50b 100644 --- a/examples/logAxis.py +++ b/examples/logAxis.py @@ -11,7 +11,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: logAxis') p1 = w.addPlot(0,0, title="X Semilog") p2 = w.addPlot(1,0, title="Y Semilog") diff --git a/examples/optics_demos.py b/examples/optics_demos.py index 36bfc7f9..b2ac5c8a 100644 --- a/examples/optics_demos.py +++ b/examples/optics_demos.py @@ -17,7 +17,7 @@ from pyqtgraph import Point app = pg.QtGui.QApplication([]) -w = pg.GraphicsWindow(border=0.5) +w = pg.GraphicsLayoutWidget(show=True, border=0.5) w.resize(1000, 900) w.show() diff --git a/examples/scrollingPlots.py b/examples/scrollingPlots.py index 313d4e8d..d370aa46 100644 --- a/examples/scrollingPlots.py +++ b/examples/scrollingPlots.py @@ -8,7 +8,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: Scrolling Plots') From 927fe44ab9d7c7fdb1f0021f669e44183d48e85e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:34:15 -0800 Subject: [PATCH 21/60] move mkQApp to Qt.py to make it easier to import internally GraphicsLayoutWidget now calls mkQApp --- pyqtgraph/Qt.py | 7 +++++++ pyqtgraph/__init__.py | 13 +------------ pyqtgraph/graphicsWindows.py | 8 +------- pyqtgraph/widgets/GraphicsLayoutWidget.py | 3 ++- pyqtgraph/widgets/__init__.py | 21 --------------------- 5 files changed, 11 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 749943f2..ad2a8007 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -272,3 +272,10 @@ m = re.match(r'(\d+)\.(\d+).*', QtVersion) if m is not None and list(map(int, m.groups())) < versionReq: print(list(map(int, m.groups()))) raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) + + +def mkQApp(): + app = QtGui.QApplication.instance() + if app is None: + app = QtGui.QApplication([]) + return app diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 520ea196..583aeaa0 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -10,7 +10,7 @@ __version__ = '0.10.0' ## 'Qt' is a local module; it is intended mainly to cover up the differences ## between PyQt4 and PySide. -from .Qt import QtGui +from .Qt import QtGui, mkQApp ## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause) #if QtGui.QApplication.instance() is None: @@ -466,14 +466,3 @@ def stack(*args, **kwds): except NameError: consoles = [c] return c - - -def mkQApp(): - global QAPP - inst = QtGui.QApplication.instance() - if inst is None: - QAPP = QtGui.QApplication([]) - else: - QAPP = inst - return QAPP - diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 41c8b4d2..b6598685 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -6,17 +6,11 @@ it is possible to place any widget into its own window by simply calling its show() method. """ -from .Qt import QtCore, QtGui +from .Qt import QtCore, QtGui, mkQApp from .widgets.PlotWidget import * from .imageview import * from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget from .widgets.GraphicsView import GraphicsView -QAPP = None - -def mkQApp(): - if QtGui.QApplication.instance() is None: - global QAPP - QAPP = QtGui.QApplication([]) class GraphicsWindow(GraphicsLayoutWidget): diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index d42378d5..3b41a3ca 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui +from ..Qt import QtGui, mkQApp from ..graphicsItems.GraphicsLayout import GraphicsLayout from .GraphicsView import GraphicsView @@ -48,6 +48,7 @@ class GraphicsLayoutWidget(GraphicsView): :func:`clear ` """ def __init__(self, parent=None, show=False, size=None, title=None, **kargs): + mkQApp() GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: diff --git a/pyqtgraph/widgets/__init__.py b/pyqtgraph/widgets/__init__.py index a81fe391..e69de29b 100644 --- a/pyqtgraph/widgets/__init__.py +++ b/pyqtgraph/widgets/__init__.py @@ -1,21 +0,0 @@ -## just import everything from sub-modules - -#import os - -#d = os.path.split(__file__)[0] -#files = [] -#for f in os.listdir(d): - #if os.path.isdir(os.path.join(d, f)): - #files.append(f) - #elif f[-3:] == '.py' and f != '__init__.py': - #files.append(f[:-3]) - -#for modName in files: - #mod = __import__(modName, globals(), locals(), fromlist=['*']) - #if hasattr(mod, '__all__'): - #names = mod.__all__ - #else: - #names = [n for n in dir(mod) if n[0] != '_'] - #for k in names: - #print modName, k - #globals()[k] = getattr(mod, k) From 3240f7a435e6e744491a8f7a9544bc9d1f4e44f4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:40:36 -0800 Subject: [PATCH 22/60] fix qapp storage bug --- pyqtgraph/Qt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ad2a8007..8c0041df 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -274,8 +274,9 @@ if m is not None and list(map(int, m.groups())) < versionReq: raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) +QAPP = None def mkQApp(): - app = QtGui.QApplication.instance() - if app is None: - app = QtGui.QApplication([]) - return app + global QAPP + if QtGui.QApplication.instance() is None: + QAPP = QtGui.QApplication([]) + return QAPP From e318bc041b34ac939809dcae8f11799781641e67 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:50:44 -0800 Subject: [PATCH 23/60] Fix isosurface error: TypeError('only integer scalar arrays can be converted to a scalar index',) --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index a6c06ad4..e83237c6 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2172,7 +2172,7 @@ def isosurface(data, level): ## compute lookup table of index: vertexes mapping faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte) faceTableInds = np.argwhere(nTableFaces == i) - faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) + faceTableI[faceTableInds[:,0]] = np.array([triTable[j[0]] for j in faceTableInds]) faceTableI = faceTableI.reshape((len(triTable), i, 3)) faceShiftTables.append(edgeShifts[faceTableI]) From b25196067add5989b530cfd0d9a978deb97465a2 Mon Sep 17 00:00:00 2001 From: Tanuj Date: Sun, 25 Feb 2018 10:54:12 +0000 Subject: [PATCH 24/60] Fix grame typo --- pyqtgraph/opengl/items/GLGridItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 4d6bc9d6..63a558ea 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -10,7 +10,7 @@ class GLGridItem(GLGraphicsItem): """ **Bases:** :class:`GLGraphicsItem ` - Displays a wire-grame grid. + Displays a wire-frame grid. """ def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'): From d09fe6bd478770862dffc3157f25e2dfcfa2a4ed Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Thu, 1 Mar 2018 15:34:15 +0100 Subject: [PATCH 25/60] SVGExporter: logicalDpiX instead of physicalDpiX Fixes problems with non-aligning axes on Qt5 svg exports. In the output svg, the axes were (individually) scaled for physical/logical ration. --- pyqtgraph/exporters/SVGExporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index fdc65080..700b63da 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -169,7 +169,7 @@ def _generateItemSvg(item, nodes=None, root=None): buf = QtCore.QBuffer(arr) svg = QtSvg.QSvgGenerator() svg.setOutputDevice(buf) - dpi = QtGui.QDesktopWidget().physicalDpiX() + dpi = QtGui.QDesktopWidget().logicalDpiX() svg.setResolution(dpi) p = QtGui.QPainter() From c7a32d83111dfa6b57fc37d9387e02e747e00bd5 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Wed, 28 Feb 2018 11:04:50 +0100 Subject: [PATCH 26/60] SVG export: handle Qt.NoPen on Qt5 --- pyqtgraph/exporters/SVGExporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index fdc65080..97975fcf 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -372,7 +372,7 @@ def correctCoordinates(node, defs, item): ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families])) ## correct line widths if needed - if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke': + if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width'): w = float(grp.getAttribute('stroke-width')) s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) w = ((s[0]-s[1])**2).sum()**0.5 From a5276c3bd3ccf478662f7dd42ed0d37ac26c847d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 Mar 2018 15:09:36 -0800 Subject: [PATCH 27/60] fix: scatterplotwidget behaves nicely when data contains infs Add methods to make it easier to programatically configure scatterplotwidget --- pyqtgraph/widgets/ColorMapWidget.py | 5 +++++ pyqtgraph/widgets/DataFilterWidget.py | 13 +++++++++---- pyqtgraph/widgets/ScatterPlotWidget.py | 19 +++++++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index bd5668ae..7e6bfab7 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -42,6 +42,11 @@ class ColorMapWidget(ptree.ParameterTree): def restoreState(self, state): self.params.restoreState(state) + def addColorMap(self, name): + """Add a new color mapping and return the created parameter. + """ + return self.params.addNew(name) + class ColorMapParameter(ptree.types.GroupParameter): sigColorMapChanged = QtCore.Signal(object) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index cae8be86..23cf930f 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -30,7 +30,12 @@ class DataFilterWidget(ptree.ParameterTree): def parameters(self): return self.params - + + def addFilter(self, name): + """Add a new filter and return the created parameter item. + """ + return self.params.addNew(name) + class DataFilterParameter(ptree.types.GroupParameter): @@ -47,10 +52,10 @@ class DataFilterParameter(ptree.types.GroupParameter): def addNew(self, name): mode = self.fields[name].get('mode', 'range') if mode == 'range': - self.addChild(RangeFilterItem(name, self.fields[name])) + child = self.addChild(RangeFilterItem(name, self.fields[name])) elif mode == 'enum': - self.addChild(EnumFilterItem(name, self.fields[name])) - + child = self.addChild(EnumFilterItem(name, self.fields[name])) + return child def fieldNames(self): return self.fields.keys() diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index e0071f24..bd0eb908 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -83,7 +83,19 @@ class ScatterPlotWidget(QtGui.QSplitter): item = self.fieldList.addItem(item) self.filter.setFields(fields) self.colorMap.setFields(fields) - + + def setSelectedFields(self, *fields): + self.fieldList.itemSelectionChanged.disconnect(self.fieldSelectionChanged) + try: + self.fieldList.clearSelection() + for f in fields: + i = self.fields.keys().index(f) + item = self.fieldList.item(i) + item.setSelected(True) + finally: + self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged) + self.fieldSelectionChanged() + def setData(self, data): """ Set the data to be processed and displayed. @@ -114,7 +126,6 @@ class ScatterPlotWidget(QtGui.QSplitter): else: self.filterText.setText('\n'.join(desc)) self.filterText.setVisible(True) - def updatePlot(self): self.plot.clear() @@ -177,9 +188,9 @@ class ScatterPlotWidget(QtGui.QSplitter): ## mask out any nan values mask = np.ones(len(xy[0]), dtype=bool) if xy[0].dtype.kind == 'f': - mask &= ~np.isnan(xy[0]) + mask &= np.isfinite(xy[0]) if xy[1] is not None and xy[1].dtype.kind == 'f': - mask &= ~np.isnan(xy[1]) + mask &= np.isfinite(xy[1]) xy[0] = xy[0][mask] style['symbolBrush'] = colors[mask] From 96a3d216e2e2128e89263a2f84a9ea6f81eeaaec Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 15 Mar 2018 11:59:45 -0700 Subject: [PATCH 28/60] Make it easier to track data in and out of scatterplotwidget --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 56 +++-------------- pyqtgraph/widgets/ScatterPlotWidget.py | 70 +++++++++++++++++++--- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 443cc220..564c5ef0 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -701,16 +701,12 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.setExportMode(self, *args, **kwds) self.invalidate() - def mapPointsToDevice(self, pts): # Map point locations to device tr = self.deviceTransform() if tr is None: return None - #pts = np.empty((2,len(self.data['x']))) - #pts[0] = self.data['x'] - #pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) pts -= self.data['width'] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. @@ -731,7 +727,6 @@ class ScatterPlotItem(GraphicsObject): (pts[1] - w < viewBounds.bottom())) ## remove out of view points return mask - @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): cmode = self.opts.get('compositionMode', None) @@ -758,8 +753,6 @@ class ScatterPlotItem(GraphicsObject): # Cull points that are outside view viewMask = self.getViewMask(pts) - #pts = pts[:,mask] - #data = self.data[mask] if self.opts['useCache'] and self._exportOpts is False: # Draw symbols from pre-rendered atlas @@ -804,9 +797,9 @@ class ScatterPlotItem(GraphicsObject): self.picture.play(p) def points(self): - for rec in self.data: + for i,rec in enumerate(self.data): if rec['item'] is None: - rec['item'] = SpotItem(rec, self) + rec['item'] = SpotItem(rec, self, i) return self.data['item'] def pointsAt(self, pos): @@ -854,18 +847,19 @@ class SpotItem(object): by connecting to the ScatterPlotItem's click signals. """ - def __init__(self, data, plot): - #GraphicsItem.__init__(self, register=False) + def __init__(self, data, plot, index): self._data = data self._plot = plot - #self.setParentItem(plot) - #self.setPos(QtCore.QPointF(data['x'], data['y'])) - #self.updateItem() + self._index = index def data(self): """Return the user data associated with this spot.""" return self._data['data'] + def index(self): + """Return the index of this point as given in the scatter plot data.""" + return self._index + def size(self): """Return the size of this spot. If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" @@ -949,37 +943,3 @@ class SpotItem(object): self._data['sourceRect'] = None self._plot.updateSpots(self._data.reshape(1)) self._plot.invalidate() - -#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): - #def __init__(self, data, plot): - #QtGui.QGraphicsPixmapItem.__init__(self) - #self.setFlags(self.flags() | self.ItemIgnoresTransformations) - #SpotItem.__init__(self, data, plot) - - #def setPixmap(self, pixmap): - #QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) - #self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) - - #def updateItem(self): - #symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) - - ### If all symbol options are default, use default pixmap - #if symbolOpts == (None, None, -1, ''): - #pixmap = self._plot.defaultSpotPixmap() - #else: - #pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol()) - #self.setPixmap(pixmap) - - -#class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem): - #def __init__(self, data, plot): - #QtGui.QGraphicsPathItem.__init__(self) - #SpotItem.__init__(self, data, plot) - - #def updateItem(self): - #QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()]) - #QtGui.QGraphicsPathItem.setPen(self, self.pen()) - #QtGui.QGraphicsPathItem.setBrush(self, self.brush()) - #size = self.size() - #self.resetTransform() - #self.scale(size, size) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index bd0eb908..96ad39cb 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -59,9 +59,16 @@ class ScatterPlotWidget(QtGui.QSplitter): self.filterText.setParentItem(self.plot.plotItem) self.data = None + self.indices = None self.mouseOverField = None self.scatterPlot = None + self.selectionScatter = None + self.selectedIndices = [] self.style = dict(pen=None, symbol='o') + self._visibleXY = None # currently plotted points + self._visibleData = None # currently plotted records + self._visibleIndices = None + self._indexMap = None self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged) self.filter.sigFilterChanged.connect(self.filterChanged) @@ -102,9 +109,26 @@ class ScatterPlotWidget(QtGui.QSplitter): Argument must be a numpy record array. """ self.data = data + self.indices = np.arange(len(data)) self.filtered = None + self.filteredIndices = None self.updatePlot() + def setSelectedIndices(self, inds): + """Mark the specified indices as selected. + + Must be a sequence of integers that index into the array given in setData(). + """ + self.selectedIndices = inds + self.updateSelected() + + def setSelectedPoints(self, points): + """Mark the specified points as selected. + + Must be a list of points as generated by the sigScatterPlotClicked signal. + """ + self.setSelectedIndices([pt.originalIndex for pt in points]) + def fieldSelectionChanged(self): sel = self.fieldList.selectedItems() if len(sel) > 2: @@ -129,11 +153,13 @@ class ScatterPlotWidget(QtGui.QSplitter): def updatePlot(self): self.plot.clear() - if self.data is None: + if self.data is None or len(self.data) == 0: return if self.filtered is None: - self.filtered = self.filter.filterData(self.data) + mask = self.filter.generateMask(self.data) + self.filtered = self.data[mask] + self.filteredIndices = self.indices[mask] data = self.filtered if len(data) == 0: return @@ -194,6 +220,8 @@ class ScatterPlotWidget(QtGui.QSplitter): xy[0] = xy[0][mask] style['symbolBrush'] = colors[mask] + data = data[mask] + indices = self.filteredIndices[mask] ## Scatter y-values for a histogram-like appearance if xy[1] is None: @@ -215,16 +243,44 @@ class ScatterPlotWidget(QtGui.QSplitter): if smax != 0: scatter *= 0.2 / smax xy[ax][keymask] += scatter - + + if self.scatterPlot is not None: try: self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked) except: pass - self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style) - self.scatterPlot.sigPointsClicked.connect(self.plotClicked) + self._visibleXY = xy + self._visibleData = data + self._visibleIndices = indices + self._indexMap = None + self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data, **style) + self.scatterPlot.sigPointsClicked.connect(self.plotClicked) + self.updateSelected() + + def updateSelected(self): + if self._visibleXY is None: + return + # map from global index to visible index + indMap = self._getIndexMap() + inds = [indMap[i] for i in self.selectedIndices if i in indMap] + x,y = self._visibleXY[0][inds], self._visibleXY[1][inds] + + if self.selectionScatter is not None: + self.plot.plotItem.removeItem(self.selectionScatter) + if len(x) == 0: + return + self.selectionScatter = self.plot.plot(x, y, pen=None, symbol='s', symbolSize=12, symbolBrush=None, symbolPen='y') + + def _getIndexMap(self): + # mapping from original data index to visible point index + if self._indexMap is None: + self._indexMap = {j:i for i,j in enumerate(self._visibleIndices)} + return self._indexMap + def plotClicked(self, plot, points): + # Tag each point with its index into the original dataset + for pt in points: + pt.originalIndex = self._visibleIndices[pt.index()] self.sigScatterPlotClicked.emit(self, points) - - From 27f37d0fd1ae56039936a9af96d8b0688078ee97 Mon Sep 17 00:00:00 2001 From: Vanessa Date: Mon, 19 Mar 2018 12:09:49 +0100 Subject: [PATCH 29/60] fix color ignored in GLGridItem. Issue #283 solved --- pyqtgraph/opengl/items/GLGridItem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 4d6bc9d6..dece73a5 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -13,7 +13,7 @@ class GLGridItem(GLGraphicsItem): Displays a wire-grame grid. """ - def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'): + def __init__(self, size=None, color=(1, 1, 1, .3), antialias=True, glOptions='translucent'): GLGraphicsItem.__init__(self) self.setGLOptions(glOptions) self.antialias = antialias @@ -21,6 +21,7 @@ class GLGridItem(GLGraphicsItem): size = QtGui.QVector3D(20,20,1) self.setSize(size=size) self.setSpacing(1, 1, 1) + self.color = color def setSize(self, x=None, y=None, z=None, size=None): """ @@ -66,8 +67,8 @@ class GLGridItem(GLGraphicsItem): x,y,z = self.size() xs,ys,zs = self.spacing() xvals = np.arange(-x/2., x/2. + xs*0.001, xs) - yvals = np.arange(-y/2., y/2. + ys*0.001, ys) - glColor4f(1, 1, 1, .3) + yvals = np.arange(-y/2., y/2. + ys*0.001, ys) + glColor4f(*self.color) for x in xvals: glVertex3f(x, yvals[0], 0) glVertex3f(x, yvals[-1], 0) From b3a579fd004f9a841038b6adf6d4ec14dbe8748a Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 29 Mar 2018 12:03:57 +0200 Subject: [PATCH 30/60] ScatterPlotItem: Fix a GC memory leak due to numpy issue 6581 --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 443cc220..30e6cf89 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -857,11 +857,18 @@ class SpotItem(object): def __init__(self, data, plot): #GraphicsItem.__init__(self, register=False) self._data = data - self._plot = plot + # SpotItems are kept in plot.data["items"] numpy object array which + # does not support cyclic garbage collection (numpy issue 6581). + # Keeping a strong ref to plot here would leak the cycle + self.__plot_ref = weakref.ref(plot) #self.setParentItem(plot) #self.setPos(QtCore.QPointF(data['x'], data['y'])) #self.updateItem() + @property + def _plot(self): + return self.__plot_ref() + def data(self): """Return the user data associated with this spot.""" return self._data['data'] From 358b1539eee6d45f75169bf4471604d540c8e8d5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 29 Mar 2018 08:49:42 -0700 Subject: [PATCH 31/60] add reduce import to fractal demo --- examples/fractal.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/examples/fractal.py b/examples/fractal.py index eeb1bdb0..d91133a5 100644 --- a/examples/fractal.py +++ b/examples/fractal.py @@ -4,6 +4,7 @@ Displays an interactive Koch fractal """ import initExample ## Add path to library (just for examples; you do not need this) +from functools import reduce import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np @@ -111,12 +112,4 @@ if __name__ == '__main__': import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_() - - - - - - - - \ No newline at end of file From f79bd8d4fb6d4b13c4dfdd377d07644968964b37 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 29 Mar 2018 09:06:20 -0700 Subject: [PATCH 32/60] svg export fix: more explicit check --- pyqtgraph/exporters/SVGExporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 97975fcf..0ab9a736 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -372,7 +372,7 @@ def correctCoordinates(node, defs, item): ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families])) ## correct line widths if needed - if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width'): + if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width') != '': w = float(grp.getAttribute('stroke-width')) s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) w = ((s[0]-s[1])**2).sum()**0.5 From 043a3b4d6d7faf84faa563853e55f319884ebd70 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 29 Mar 2018 18:30:46 -0700 Subject: [PATCH 33/60] Better title --- examples/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/__main__.py b/examples/__main__.py index 6b8a07f9..084fe2c3 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -26,7 +26,7 @@ class ExampleLoader(QtGui.QMainWindow): self.cw = QtGui.QWidget() self.setCentralWidget(self.cw) self.ui.setupUi(self.cw) - self.setWindowTitle("Examples") + self.setWindowTitle("PyQtGraph Examples") self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() From 20bcc39eb1039987fa411821a440ee1323dd3a63 Mon Sep 17 00:00:00 2001 From: "Ilya A. Kriveshko" Date: Fri, 30 Mar 2018 14:48:05 -0400 Subject: [PATCH 34/60] examples: use integer division in indexing In python3 / operator produces a float, which is not a valid index. Replace with // integer division. --- examples/GLImageItem.py | 6 +++--- examples/isocurve.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/GLImageItem.py b/examples/GLImageItem.py index 581474fd..70bf5306 100644 --- a/examples/GLImageItem.py +++ b/examples/GLImageItem.py @@ -26,9 +26,9 @@ data += pg.gaussianFilter(np.random.normal(size=shape), (15,15,15))*15 ## slice out three planes, convert to RGBA for OpenGL texture levels = (-0.08, 0.08) -tex1 = pg.makeRGBA(data[shape[0]/2], levels=levels)[0] # yz plane -tex2 = pg.makeRGBA(data[:,shape[1]/2], levels=levels)[0] # xz plane -tex3 = pg.makeRGBA(data[:,:,shape[2]/2], levels=levels)[0] # xy plane +tex1 = pg.makeRGBA(data[shape[0]//2], levels=levels)[0] # yz plane +tex2 = pg.makeRGBA(data[:,shape[1]//2], levels=levels)[0] # xz plane +tex3 = pg.makeRGBA(data[:,:,shape[2]//2], levels=levels)[0] # xy plane #tex1[:,:,3] = 128 #tex2[:,:,3] = 128 #tex3[:,:,3] = 128 diff --git a/examples/isocurve.py b/examples/isocurve.py index 6d89bbec..63b1699e 100644 --- a/examples/isocurve.py +++ b/examples/isocurve.py @@ -17,7 +17,7 @@ app = QtGui.QApplication([]) frames = 200 data = np.random.normal(size=(frames,30,30), loc=0, scale=100) data = np.concatenate([data, data], axis=0) -data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] +data = pg.gaussianFilter(data, (10, 10, 10))[frames//2:frames + frames//2] data[:, 15:16, 15:17] += 1 win = pg.GraphicsLayoutWidget(show=True) From ce704a1baaf58d4adb0abfd9d5bf7bad83a0023a Mon Sep 17 00:00:00 2001 From: Fekete Imre Date: Fri, 13 Apr 2018 16:23:41 +0200 Subject: [PATCH 35/60] Fix issue #145 Disable textures after painting is finished. --- pyqtgraph/opengl/items/GLScatterPlotItem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index dc4b298a..fe794d48 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -152,7 +152,9 @@ class GLScatterPlotItem(GLGraphicsItem): glDisableClientState(GL_VERTEX_ARRAY) glDisableClientState(GL_COLOR_ARRAY) #posVBO.unbind() - + ##fixes #145 + glDisable( GL_TEXTURE_2D ) + #for i in range(len(self.pos)): #pos = self.pos[i] From 063e9c91a999853a47675a2befcdfef4c0e30d53 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 24 Apr 2018 08:59:33 -0700 Subject: [PATCH 36/60] Make high-dpi handling conditional on Qt version --- pyqtgraph/opengl/GLViewWidget.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index c6d0c1fa..540fce7d 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -16,9 +16,13 @@ class GLViewWidget(QtOpenGL.QGLWidget): - Axis/grid display - Export options + + High-DPI displays: Qt5 should automatically detect the correct resolution. + For Qt4, specify the ``devicePixelRatio`` argument when initializing the + widget (usually this value is 1-2). """ - def __init__(self, parent=None): + def __init__(self, parent=None, devicePixelRatio=None): global ShareWidget if ShareWidget is None: @@ -37,6 +41,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): 'azimuth': 45, ## camera's azimuthal angle in degrees ## (rotation around z-axis 0 points along x-axis) 'viewport': None, ## glViewport params; None == whole widget + 'devicePixelRatio': devicePixelRatio, } self.setBackgroundColor('k') self.items = [] @@ -79,10 +84,21 @@ class GLViewWidget(QtOpenGL.QGLWidget): def getViewport(self): vp = self.opts['viewport'] + dpr = self.devicePixelRatio() if vp is None: - return (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) + return (0, 0, int(self.width() * dpr), int(self.height() * dpr)) else: - return vp + return tuple([int(x * dpr) for x in vp]) + + def devicePixelRatio(self): + dpr = self.opts['devicePixelRatio'] + if dpr is not None: + return dpr + + if hasattr(QtOpenGL.QGLWidget, 'devicePixelRatio'): + return QtOpenGL.QGLWidget.devicePixelRatio(self) + else: + return 1.0 def resizeGL(self, w, h): pass @@ -99,7 +115,8 @@ class GLViewWidget(QtOpenGL.QGLWidget): def projectionMatrix(self, region=None): # Xw = (Xnd + 1) * width/2 + X if region is None: - region = (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) + dpr = self.devicePixelRatio() + region = (0, 0, self.width() * dpr, self.height() * dpr) x0, y0, w, h = self.getViewport() dist = self.opts['distance'] From 80ff4ebfe6196c59c3e5e834cb769f39a8ae93b8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 08:50:37 -0700 Subject: [PATCH 37/60] Remove prints from console --- pyqtgraph/console/Console.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 634aab4a..02f00c44 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -359,7 +359,6 @@ class ConsoleWidget(QtGui.QWidget): for index, line in enumerate(traceback.extract_stack(frame)): # extract_stack return value changed in python 3.5 if 'FrameSummary' in str(type(line)): - print(dir(line)) line = (line.filename, line.lineno, line.name, line._line) self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) @@ -380,7 +379,6 @@ class ConsoleWidget(QtGui.QWidget): for index, line in enumerate(traceback.extract_tb(tb)): # extract_stack return value changed in python 3.5 if 'FrameSummary' in str(type(line)): - print(dir(line)) line = (line.filename, line.lineno, line.name, line._line) self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) From cbff3a58d78257a70b20507c0c89538171c1c4b2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 11:10:39 -0700 Subject: [PATCH 38/60] Fix scatterplotwidget label color --- pyqtgraph/widgets/ScatterPlotWidget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index 96ad39cb..bf8a0f42 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -52,9 +52,9 @@ class ScatterPlotWidget(QtGui.QSplitter): self.ctrlPanel.addWidget(self.ptree) self.addWidget(self.plot) - bg = fn.mkColor(getConfigOption('background')) - bg.setAlpha(150) - self.filterText = TextItem(border=getConfigOption('foreground'), color=bg) + fg = fn.mkColor(getConfigOption('foreground')) + fg.setAlpha(150) + self.filterText = TextItem(border=getConfigOption('foreground'), color=fg) self.filterText.setPos(60,20) self.filterText.setParentItem(self.plot.plotItem) From 56aae02821c13e080e138247e65615df8bf83586 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 11:54:06 -0700 Subject: [PATCH 39/60] fix svg export failure when first line point is masked --- pyqtgraph/exporters/SVGExporter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 3e060e62..ae1ec91c 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -344,6 +344,10 @@ def correctCoordinates(node, defs, item): t = '' nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' ' + # If coords start with L instead of M, then the entire path will not be rendered. + # (This can happen if the first point had nan values in it--Qt will skip it on export) + if newCoords[0] != 'M': + newCoords = 'M' + newCoords[1:] ch.setAttribute('d', newCoords) elif ch.tagName == 'text': removeTransform = False From ee2d00c42ae38460d1071bffcc4ff360570edc69 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 12:15:14 -0700 Subject: [PATCH 40/60] Add option to remove non-scaling-stroke from svg output --- pyqtgraph/exporters/SVGExporter.py | 42 ++++++++++++------------------ 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index ae1ec91c..ce2a90c0 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -23,7 +23,8 @@ class SVGExporter(Exporter): #{'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)}, #{'name': 'viewbox clipping', 'type': 'bool', 'value': True}, #{'name': 'normalize coordinates', 'type': 'bool', 'value': True}, - #{'name': 'normalize line width', 'type': 'bool', 'value': True}, + {'name': 'scaling stroke', 'type': 'bool', 'value': False, 'tip': "If False, strokes are non-scaling, " + "which means that they appear the same width on screen regardless of how they are scaled or how the view is zoomed."}, ]) #self.params.param('width').sigValueChanged.connect(self.widthChanged) #self.params.param('height').sigValueChanged.connect(self.heightChanged) @@ -49,7 +50,8 @@ class SVGExporter(Exporter): ## Qt's SVG generator is not complete. (notably, it lacks clipping) ## Instead, we will use Qt to generate SVG for each item independently, ## then manually reconstruct the entire document. - xml = generateSvg(self.item) + options = {ch.name():ch.value() for ch in self.params.children()} + xml = generateSvg(self.item, options) if toBytes: return xml.encode('UTF-8') @@ -69,10 +71,10 @@ xmlHeader = """\ Generated with Qt and pyqtgraph """ -def generateSvg(item): +def generateSvg(item, options={}): global xmlHeader try: - node, defs = _generateItemSvg(item) + node, defs = _generateItemSvg(item, options=options) finally: ## reset export mode for all items in the tree if isinstance(item, QtGui.QGraphicsScene): @@ -94,7 +96,7 @@ def generateSvg(item): return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n\n" -def _generateItemSvg(item, nodes=None, root=None): +def _generateItemSvg(item, nodes=None, root=None, options={}): ## This function is intended to work around some issues with Qt's SVG generator ## and SVG in general. ## 1) Qt SVG does not implement clipping paths. This is absurd. @@ -209,18 +211,8 @@ def _generateItemSvg(item, nodes=None, root=None): ## Get rid of group transformation matrices by applying ## transformation to inner coordinates - correctCoordinates(g1, defs, item) + correctCoordinates(g1, defs, item, options) profiler('correct') - ## make sure g1 has the transformation matrix - #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) - #g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m) - - #print "=================",item,"=====================" - #print g1.toprettyxml(indent=" ", newl='') - - ## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1) - ## So we need to correct anything attempting to use this. - #correctStroke(g1, item, root) ## decide on a name for this item baseName = item.__class__.__name__ @@ -239,15 +231,10 @@ def _generateItemSvg(item, nodes=None, root=None): ## See if this item clips its children if int(item.flags() & item.ItemClipsChildrenToShape) > 0: ## Generate svg for just the path - #if isinstance(root, QtGui.QGraphicsScene): - #path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) - #else: - #path = QtGui.QGraphicsPathItem(root.mapToParent(item.mapToItem(root, item.shape()))) path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) item.scene().addItem(path) try: - #pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0] - pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0] + pathNode = _generateItemSvg(path, root=root, options=options)[0].getElementsByTagName('path')[0] # assume for this path is empty.. possibly problematic. finally: item.scene().removeItem(path) @@ -267,17 +254,18 @@ def _generateItemSvg(item, nodes=None, root=None): ## Add all child items as sub-elements. childs.sort(key=lambda c: c.zValue()) for ch in childs: - csvg = _generateItemSvg(ch, nodes, root) + csvg = _generateItemSvg(ch, nodes, root, options=options) if csvg is None: continue cg, cdefs = csvg childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now) defs.extend(cdefs) - + profiler('children') return g1, defs -def correctCoordinates(node, defs, item): + +def correctCoordinates(node, defs, item, options): # TODO: correct gradient coordinates inside defs ## Remove transformation matrices from tags by applying matrix to coordinates inside. @@ -382,6 +370,10 @@ def correctCoordinates(node, defs, item): w = ((s[0]-s[1])**2).sum()**0.5 ch.setAttribute('stroke-width', str(w)) + # Remove non-scaling-stroke if requested + if options.get('scaling stroke') is True and ch.getAttribute('vector-effect') == 'non-scaling-stroke': + ch.removeAttribute('vector-effect') + if removeTransform: grp.removeAttribute('transform') From d4a4fd7fd5565276dabd7990721210bf9df6dd42 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:04:26 -0700 Subject: [PATCH 41/60] fix busy cursor nesting Prevents permenent busy cursor --- pyqtgraph/widgets/BusyCursor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py index d99fe589..5fab3358 100644 --- a/pyqtgraph/widgets/BusyCursor.py +++ b/pyqtgraph/widgets/BusyCursor.py @@ -18,7 +18,7 @@ class BusyCursor(object): BusyCursor.active.append(self) def __exit__(self, *args): - BusyCursor.active.pop(-1) - if len(BusyCursor.active) == 0: + if self._active: + BusyCursor.active.pop(-1) QtGui.QApplication.restoreOverrideCursor() \ No newline at end of file From 7daa5bdff13149f246fb6d13b330ec7fe3d44785 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:04:52 -0700 Subject: [PATCH 42/60] Ignore BusyCursor when called from thread --- pyqtgraph/widgets/BusyCursor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py index 5fab3358..e7a26810 100644 --- a/pyqtgraph/widgets/BusyCursor.py +++ b/pyqtgraph/widgets/BusyCursor.py @@ -9,13 +9,19 @@ class BusyCursor(object): with pyqtgraph.BusyCursor(): doLongOperation() - May be nested. + May be nested. If called from a non-gui thread, then the cursor will not be affected. """ active = [] def __enter__(self): - QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) - BusyCursor.active.append(self) + app = QtCore.QCoreApplication.instance() + isGuiThread = (app is not None) and (QtCore.QThread.currentThread() == app.thread()) + if isGuiThread and QtGui.QApplication.instance() is not None: + QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + BusyCursor.active.append(self) + self._active = True + else: + self._active = False def __exit__(self, *args): if self._active: From 58a865e088ba5d047ffab23a6cff645048060ecc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:19:50 -0700 Subject: [PATCH 43/60] Add signal documentation for graphicsscene --- pyqtgraph/GraphicsScene/GraphicsScene.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 952a2415..0fca2684 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -36,6 +36,18 @@ class GraphicsScene(QtGui.QGraphicsScene): This lets us indicate unambiguously to the user which item they are about to click/drag on * Eats mouseMove events that occur too soon after a mouse press. * Reimplements items() and itemAt() to circumvent PyQt bug + + ====================== ================================================================== + **Signals** + sigMouseClicked(event) Emitted when the mouse is clicked over the scene. Use ev.pos() to + get the click position relative to the item that was clicked on, + or ev.scenePos() to get the click position in scene coordinates. + See :class:`pyqtgraph.GraphicsScene.MouseClickEvent`. + sigMouseMoved(pos) Emitted when the mouse cursor moves over the scene. The position + is given in scene coordinates. + sigMouseHover(items) Emitted when the mouse is moved over the scene. Items is a list + of items under the cursor. + ====================== ================================================================== Mouse interaction is as follows: From 465ea2d940e6ef5b9b55b8556506a345265616a4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:22:23 -0700 Subject: [PATCH 44/60] Prevent parallelizer leaking file handles --- pyqtgraph/multiprocess/parallelizer.py | 2 ++ pyqtgraph/multiprocess/processes.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 86298023..ef00be7c 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -195,6 +195,8 @@ class Parallelize(object): finally: if self.showProgress: self.progressDlg.__exit__(None, None, None) + for ch in self.childs: + ch.join() if len(self.exitCodes) < len(self.childs): raise Exception("Parallelizer started %d processes but only received exit codes from %d." % (len(self.childs), len(self.exitCodes))) for code in self.exitCodes: diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 1be7e50b..0a372ef0 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -165,6 +165,7 @@ class Process(RemoteEventHandler): if timeout is not None and time.time() - start > timeout: raise Exception('Timed out waiting for remote process to end.') time.sleep(0.05) + self.conn.close() self.debugMsg('Child process exited. (%d)' % self.proc.returncode) def debugMsg(self, msg, *args): @@ -341,6 +342,7 @@ class ForkedProcess(RemoteEventHandler): except OSError: ## probably remote process has already quit pass + self.conn.close() # don't leak file handles! self.hasJoined = True def kill(self): From 51267f569649a6ca668ab1c275ba467b4fa431f0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 14:54:35 -0700 Subject: [PATCH 45/60] int parameters use integer formatting --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 42a18fe0..8d65767d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -105,6 +105,7 @@ class WidgetParameterItem(ParameterItem): if t == 'int': defs['int'] = True defs['minStep'] = 1.0 + defs['format'] = '{value:d}' for k in defs: if k in opts: defs[k] = opts[k] From 4ead3f923bcb7e2816c378edbf4ee9de885c5491 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 15:00:44 -0700 Subject: [PATCH 46/60] Add changelog note --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 7b6c916b..921d0616 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,9 @@ pyqtgraph-0.11.0 (in development) To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. - Deprecated graphicsWindow classes; these have been unnecessary for many years because widgets can be placed into a new window just by calling show(). + - Integer values in ParameterTree are now formatted as integer (%d) by default, rather than + scientific notation (%g). This can be overridden by providing `format={value:g}` when + creating the parameter. pyqtgraph-0.10.0 From 7593d4ea83e9007a27008913291fa10d2983fad0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 15:07:21 -0700 Subject: [PATCH 47/60] fix Profiler when called from top of stack --- pyqtgraph/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 61ae9fd5..dd956620 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -510,7 +510,7 @@ class Profiler(object): try: caller_object_type = type(caller_frame.f_locals["self"]) except KeyError: # we are in a regular function - qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] + qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1] else: # we are in a method qualifier = caller_object_type.__name__ func_qualname = qualifier + "." + caller_frame.f_code.co_name From 0c1cda49730bdb7da4f9600dd69f55e8fd0cd4d1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 16:11:25 -0700 Subject: [PATCH 48/60] Gracefully handle case where image data has size==0 --- pyqtgraph/graphicsItems/ImageItem.py | 6 +++--- pyqtgraph/imageview/ImageView.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index d1e4aa9e..7d6c2596 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -465,11 +465,11 @@ class ImageItem(GraphicsObject): This method is also used when automatically computing levels. """ - if self.image is None: + if self.image is None or self.image.size == 0: return None,None if step == 'auto': - step = (int(np.ceil(self.image.shape[0] / targetImageSize)), - int(np.ceil(self.image.shape[1] / targetImageSize))) + step = (max(1, int(np.ceil(self.image.shape[0] / targetImageSize))), + max(1, int(np.ceil(self.image.shape[1] / targetImageSize)))) if np.isscalar(step): step = (step, step) stepData = self.image[::step[0], ::step[1]] diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c64953de..1d38a6c3 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -633,8 +633,12 @@ class ImageView(QtGui.QWidget): cax = self.axes['c'] if cax is None: + if data.size == 0: + return [(0, 0)] return [(float(nanmin(data)), float(nanmax(data)))] else: + if data.size == 0: + return [(0, 0)] * data.shape[-1] return [(float(nanmin(data.take(i, axis=cax))), float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])] From 5f778f905c784c788f17d240412721336b00e921 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 16:55:10 -0700 Subject: [PATCH 49/60] Clean up README Removed contributor list; the actual list of contributors is way too long to hold here :D --- README.md | 64 +++++++++++++------------------------------------------ 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 30268796..85f4f9e7 100644 --- a/README.md +++ b/README.md @@ -6,80 +6,46 @@ PyQtGraph A pure-Python graphics library for PyQt/PySide -Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill +Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill -Maintainer ----------- +PyQtGraph is intended for use in mathematics / scientific / engineering applications. +Despite being written entirely in python, the library is fast due to its +heavy leverage of numpy for number crunching, Qt's GraphicsView framework for +2D display, and OpenGL for 3D display. - * Luke Campagnola - -Contributors ------------- - - * Megan Kratz - * Paul Manis - * Ingo Breßler - * Christian Gavin - * Michael Cristopher Hogg - * Ulrich Leutner - * Felix Schill - * Guillaume Poulin - * Antony Lee - * Mattias Põldaru - * Thomas S. - * Fabio Zadrozny - * Mikhail Terekhov - * Pietro Zambelli - * Stefan Holzmann - * Nicholas TJ - * John David Reaver - * David Kaplan - * Martin Fitzpatrick - * Daniel Lidstrom - * Eric Dill - * Vincent LeSaux Requirements ------------ * PyQt 4.7+, PySide, or PyQt5 - * python 2.6, 2.7, or 3.x + * python 2.7, or 3.x * NumPy * For 3D graphics: pyopengl and qt-opengl * Known to run on Windows, Linux, and Mac. Support ------- - - Post at the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) + + * Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues) + * Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph) Installation Methods -------------------- + * From pypi: + `pip install pyqtgraph` * To use with a specific project, simply copy the pyqtgraph subdirectory - anywhere that is importable from your project. PyQtGraph may also be - used as a git subtree by cloning the git-core repository from github. + anywhere that is importable from your project. * To install system-wide from source distribution: `$ python setup.py install` * For installation packages, see the website (pyqtgraph.org) - * On debian-like systems, pyqtgraph requires the following packages: - python-numpy, python-qt4 | python-pyside - For 3D support: python-opengl, python-qt4-gl | python-pyside.qtopengl Documentation ------------- -There are many examples; run `python -m pyqtgraph.examples` for a menu. +The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. + +The official documentation lives at http://pyqtgraph.org/documentation -Some (incomplete) documentation exists at this time. - * Easiest place to get documentation is at - * If you acquired this code as a .tar.gz file from the website, then you can also look in - doc/html. - * If you acquired this code via GitHub, then you can build the documentation using sphinx. - From the documentation directory, run: - `$ make html` - -Please feel free to pester Luke or post to the forum if you need a specific - section of documentation to be expanded. From b64294064ff6f75437038d2805b69ac9e8cf798c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Poulhi=C3=A8s?= Date: Thu, 26 Apr 2018 21:57:51 +0200 Subject: [PATCH 50/60] typo fix in comment trivial typo fix --- pyqtgraph/SRTTransform3D.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 9b54843b..3c4edcc8 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -113,7 +113,7 @@ class SRTTransform3D(Transform3D): def setFromMatrix(self, m): """ - Set this transform mased on the elements of *m* + Set this transform based on the elements of *m* The input matrix must be affine AND have no shear, otherwise the conversion will most likely fail. """ From 60a48ed2e4ef9fcecaffca33e009f4e0a35bd972 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 13:22:47 -0700 Subject: [PATCH 51/60] reload tests pass in python 3 --- pyqtgraph/functions.py | 2 +- pyqtgraph/reload.py | 46 ++++++++++++++++-------- pyqtgraph/tests/test_reload.py | 65 ++++++++++++++++++++++++---------- 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 21e6c006..8857c052 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2444,7 +2444,7 @@ def disconnect(signal, slot): try: signal.disconnect(slot) return True - except TypeError, RuntimeError: + except (TypeError, RuntimeError): slot = reload.getPreviousVersion(slot) if slot is None: return False diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index 5aa2ed68..f682a025 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -21,15 +21,18 @@ Does NOT: print module.someObject """ - +from __future__ import print_function import inspect, os, sys, gc, traceback, types try: - import __builtin__ as builtins + from builtins import reload as orig_reload except ImportError: - import builtins + from importlib import reload as orig_reload from .debug import printExc +py3 = sys.version_info >= (3,) + + def reloadAll(prefix=None, debug=False): """Automatically reload everything whose __file__ begins with prefix. - Skips reload if the file has not been updated (if .pyc is newer than .py) @@ -80,7 +83,7 @@ def reload(module, debug=False, lists=False, dicts=False): ## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison oldDict = module.__dict__.copy() - builtins.reload(module) + orig_reload(module) newDict = module.__dict__ ## Allow modules access to the old dictionary after they reload @@ -130,6 +133,9 @@ def updateFunction(old, new, debug, depth=0, visited=None): old.__code__ = new.__code__ old.__defaults__ = new.__defaults__ + if hasattr(old, '__kwdefaults'): + old.__kwdefaults__ = new.__kwdefaults__ + old.__doc__ = new.__doc__ if visited is None: visited = [] @@ -154,6 +160,8 @@ def updateFunction(old, new, debug, depth=0, visited=None): ## For classes: ## 1) find all instances of the old class and set instance.__class__ to the new class ## 2) update all old class methods to use code from the new class methods + + def updateClass(old, new, debug): ## Track town all instances and subclasses of old refs = gc.get_referrers(old) @@ -198,7 +206,8 @@ def updateClass(old, new, debug): ## but it fixes a few specific cases (pyqt signals, for one) for attr in dir(old): oa = getattr(old, attr) - if inspect.ismethod(oa): + if (py3 and inspect.isfunction(oa)) or inspect.ismethod(oa): + # note python2 has unbound methods, whereas python3 just uses plain functions try: na = getattr(new, attr) except AttributeError: @@ -206,11 +215,14 @@ def updateClass(old, new, debug): print(" Skipping method update for %s; new class does not have this attribute" % attr) continue - if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__: - depth = updateFunction(oa.__func__, na.__func__, debug) - if not hasattr(na.__func__, '__previous_reload_method__'): - na.__func__.__previous_reload_method__ = oa # important for managing signal connection - #oa.im_class = new ## bind old method to new class ## not allowed + ofunc = getattr(oa, '__func__', oa) # in py2 we have to get the __func__ from unbound method, + nfunc = getattr(na, '__func__', na) # in py3 the attribute IS the function + + if ofunc is not nfunc: + depth = updateFunction(ofunc, nfunc, debug) + if not hasattr(nfunc, '__previous_reload_method__'): + nfunc.__previous_reload_method__ = oa # important for managing signal connection + #oa.__class__ = new ## bind old method to new class ## not allowed if debug: extra = "" if depth > 0: @@ -251,16 +263,22 @@ def getPreviousVersion(obj): if isinstance(obj, type) or inspect.isfunction(obj): return getattr(obj, '__previous_reload_version__', None) elif inspect.ismethod(obj): - if obj.im_self is None: + if obj.__self__ is None: # unbound method return getattr(obj.__func__, '__previous_reload_method__', None) else: oldmethod = getattr(obj.__func__, '__previous_reload_method__', None) if oldmethod is None: return None - self = obj.im_self - cls = oldmethod.im_class - return types.MethodType(oldmethod.__func__, self, cls) + self = obj.__self__ + oldfunc = getattr(oldmethod, '__func__', oldmethod) + if hasattr(oldmethod, 'im_class'): + # python 2 + cls = oldmethod.im_class + return types.MethodType(oldfunc, self, cls) + else: + # python 3 + return types.MethodType(oldfunc, self) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 7ceed546..57924d60 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -1,14 +1,23 @@ -import tempfile, os, sys, shutil, atexit +import tempfile, os, sys, shutil import pyqtgraph as pg import pyqtgraph.reload + + pgpath = os.path.join(os.path.dirname(pg.__file__), '..') # make temporary directory to write module code -path = tempfile.mkdtemp() -sys.path.insert(0, path) -def cleanup(): +path = None + +def setup_module(): + # make temporary directory to write module code + global path + path = tempfile.mkdtemp() + sys.path.insert(0, path) + +def teardown_module(): + global path shutil.rmtree(path) -atexit.register(cleanup) + sys.path.remove(path) code = """ @@ -33,6 +42,8 @@ def remove_cache(mod): def test_reload(): + py3 = sys.version_info >= (3,) + # write a module mod = os.path.join(path, 'reload_test.py') open(mod, 'w').write(code.format(path=path, msg="C.fn() Version1")) @@ -42,7 +53,10 @@ def test_reload(): c = reload_test.C() c.sig.connect(c.fn) - v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) @@ -50,25 +64,34 @@ def test_reload(): open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) - v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) - assert c.fn.im_class is v2[0] + if not py3: + assert c.fn.im_class is v2[0] oldcfn = pg.reload.getPreviousVersion(c.fn) if oldcfn is None: # Function did not reload; are we using pytest's assertion rewriting? raise Exception("Function did not reload. (This can happen when using py.test" " with assertion rewriting; use --assert=plain for this test.)") - assert oldcfn.im_class is v1[0] - assert oldcfn.im_func is v1[2].im_func - assert oldcfn.im_self is c - + if py3: + assert oldcfn.__func__ is v1[2] + else: + assert oldcfn.im_class is v1[0] + assert oldcfn.__func__ is v1[2].__func__ + assert oldcfn.__self__ is c # write again and reload open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) - v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) #for i in range(len(old)): #print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i] @@ -76,11 +99,15 @@ def test_reload(): cfn1 = pg.reload.getPreviousVersion(c.fn) cfn2 = pg.reload.getPreviousVersion(cfn1) - assert cfn1.im_class is v2[0] - assert cfn1.im_func is v2[2].im_func - assert cfn1.im_self is c - assert cfn2.im_class is v1[0] - assert cfn2.im_func is v1[2].im_func - assert cfn2.im_self is c + if py3: + assert cfn1.__func__ is v2[2] + assert cfn2.__func__ is v1[2] + else: + assert cfn1.__func__ is v2[2].__func__ + assert cfn2.__func__ is v1[2].__func__ + assert cfn1.im_class is v2[0] + assert cfn2.im_class is v1[0] + assert cfn1.__self__ is c + assert cfn2.__self__ is c c.sig.disconnect(cfn2) From 986ccb81f489e791b442f1296e0efe27bbfda601 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 13:28:51 -0700 Subject: [PATCH 52/60] fix reload import --- pyqtgraph/reload.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index f682a025..766ec9d0 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -23,11 +23,11 @@ Does NOT: from __future__ import print_function import inspect, os, sys, gc, traceback, types -try: - from builtins import reload as orig_reload -except ImportError: - from importlib import reload as orig_reload from .debug import printExc +try: + from importlib import reload as orig_reload +except ImportError: + orig_reload = reload py3 = sys.version_info >= (3,) From 6e392c2e0eae4adbd353e01c45b2134f8e585d8d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 13:49:34 -0700 Subject: [PATCH 53/60] Fix disconnect test for pyside --- pyqtgraph/tests/test_reload.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 57924d60..b90072f1 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -109,5 +109,6 @@ def test_reload(): assert cfn2.im_class is v1[0] assert cfn1.__self__ is c assert cfn2.__self__ is c - c.sig.disconnect(cfn2) + + pg.functions.disconnect(c.sig, c.fn) From a1145b5cbfc1db32d8dffb7ffaa1b1d18b843923 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 14:43:20 -0700 Subject: [PATCH 54/60] Fix pytest assertion rewrite conflicting with reload by giving temporary module a different name --- .travis.yml | 2 +- pyqtgraph/tests/test_reload.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1c422f19..acfde8ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov --assert=plain pyqtgraph -sv; + PYTHONPATH=. py.test --cov pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index b90072f1..6adbeeb6 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -45,29 +45,31 @@ def test_reload(): py3 = sys.version_info >= (3,) # write a module - mod = os.path.join(path, 'reload_test.py') - open(mod, 'w').write(code.format(path=path, msg="C.fn() Version1")) + mod = os.path.join(path, 'reload_test_mod.py') + print("\nRELOAD FILE:", mod) + open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1")) # import the new module - import reload_test + import reload_test_mod + print("RELOAD MOD:", reload_test_mod.__file__) - c = reload_test.C() + c = reload_test_mod.C() c.sig.connect(c.fn) if py3: - v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__) else: - v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__) # write again and reload - open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: - v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__) else: - v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__) if not py3: assert c.fn.im_class is v2[0] @@ -85,13 +87,13 @@ def test_reload(): # write again and reload - open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: - v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__) else: - v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__) #for i in range(len(old)): #print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i] From 436fcccf8273c463e06862cbeae8c221ee57da9d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Apr 2018 19:07:22 -0700 Subject: [PATCH 55/60] Avoid overflow in Point.length by using trig functions or returning inf --- pyqtgraph/Point.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index 4d04f01c..3fb43cac 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -105,7 +105,13 @@ class Point(QtCore.QPointF): def length(self): """Returns the vector length of this Point.""" - return (self[0]**2 + self[1]**2) ** 0.5 + try: + return (self[0]**2 + self[1]**2) ** 0.5 + except OverflowError: + try: + return self[1] / np.sin(np.arctan2(self[1], self[0])) + except OverflowError: + return np.inf def norm(self): """Returns a vector in the same direction with unit length.""" From 0fd0f08384343ceee73a3f205169372d98229873 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 May 2018 15:30:19 -0700 Subject: [PATCH 56/60] Doc updates for datafilterwidget --- pyqtgraph/widgets/DataFilterWidget.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index 23cf930f..a26bf0bf 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -10,6 +10,9 @@ class DataFilterWidget(ptree.ParameterTree): """ This class allows the user to filter multi-column data sets by specifying multiple criteria + + Wraps methods from DataFilterParameter: setFields, generateMask, + filterData, and describe. """ sigFilterChanged = QtCore.Signal(object) @@ -22,6 +25,7 @@ class DataFilterWidget(ptree.ParameterTree): self.params.sigTreeStateChanged.connect(self.filterChanged) self.setFields = self.params.setFields + self.generateMask = self.params.generateMask self.filterData = self.params.filterData self.describe = self.params.describe @@ -38,7 +42,8 @@ class DataFilterWidget(ptree.ParameterTree): class DataFilterParameter(ptree.types.GroupParameter): - + """A parameter group that specifies a set of filters to apply to tabular data. + """ sigFilterChanged = QtCore.Signal(object) def __init__(self): @@ -61,6 +66,17 @@ class DataFilterParameter(ptree.types.GroupParameter): return self.fields.keys() def setFields(self, fields): + """Set the list of fields that are available to be filtered. + + *fields* must be a dict or list of tuples that maps field names + to a specification describing the field. Each specification is + itself a dict with either ``'mode':'range'`` or ``'mode':'enum'``:: + + filter.setFields([ + ('field1', {'mode': 'range'}), + ('field2', {'mode': 'enum', 'values': ['val1', 'val2', 'val3']}), + ]) + """ self.fields = OrderedDict(fields) names = self.fieldNames() self.setAddList(names) @@ -71,6 +87,9 @@ class DataFilterParameter(ptree.types.GroupParameter): return data[self.generateMask(data)] def generateMask(self, data): + """Return a boolean mask indicating whether each item in *data* passes + the filter critera. + """ mask = np.ones(len(data), dtype=bool) if len(data) == 0: return mask @@ -94,6 +113,7 @@ class DataFilterParameter(ptree.types.GroupParameter): desc.append(fp.describe()) return desc + class RangeFilterItem(ptree.types.SimpleParameter): def __init__(self, name, opts): self.fieldName = name From 11463c5a0b21d91783e24844d068115d6689e827 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 4 May 2018 11:42:37 -0700 Subject: [PATCH 57/60] Fix: mkQApp returned None if a QApplication was already created elsewhere --- pyqtgraph/Qt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 8c0041df..4ce571cc 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -276,7 +276,8 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None def mkQApp(): - global QAPP - if QtGui.QApplication.instance() is None: + global QAPP + QAPP = QtGui.QApplication.instance() + if QAPP is None: QAPP = QtGui.QApplication([]) return QAPP From 9460b746fa95c6682637b8870e70f9c2c76961c5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 4 May 2018 14:17:14 -0700 Subject: [PATCH 58/60] Allow data filter entries to be updated after they are created --- pyqtgraph/widgets/DataFilterWidget.py | 49 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index a26bf0bf..bd92a765 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -6,6 +6,7 @@ from .. import functions as fn __all__ = ['DataFilterWidget'] + class DataFilterWidget(ptree.ParameterTree): """ This class allows the user to filter multi-column data sets by specifying @@ -80,6 +81,12 @@ class DataFilterParameter(ptree.types.GroupParameter): self.fields = OrderedDict(fields) names = self.fieldNames() self.setAddList(names) + + # update any existing filters + for ch in self.children(): + name = ch.fieldName + if name in fields: + ch.updateFilter(fields[name]) def filterData(self, data): if len(data) == 0: @@ -134,25 +141,17 @@ class RangeFilterItem(ptree.types.SimpleParameter): def describe(self): return "%s < %s < %s" % (fn.siFormat(self['Min'], suffix=self.units), self.fieldName, fn.siFormat(self['Max'], suffix=self.units)) + + def updateFilter(self, opts): + pass + class EnumFilterItem(ptree.types.SimpleParameter): def __init__(self, name, opts): self.fieldName = name - vals = opts.get('values', []) - childs = [] - if isinstance(vals, list): - vals = OrderedDict([(v,str(v)) for v in vals]) - for val,vname in vals.items(): - ch = ptree.Parameter.create(name=vname, type='bool', value=True) - ch.maskValue = val - childs.append(ch) - ch = ptree.Parameter.create(name='(other)', type='bool', value=True) - ch.maskValue = '__other__' - childs.append(ch) - ptree.types.SimpleParameter.__init__(self, - name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, - children=childs) + name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True) + self.setEnumVals(opts) def generateMask(self, data, startMask): vals = data[self.fieldName][startMask] @@ -172,4 +171,24 @@ class EnumFilterItem(ptree.types.SimpleParameter): def describe(self): vals = [ch.name() for ch in self if ch.value() is True] - return "%s: %s" % (self.fieldName, ', '.join(vals)) \ No newline at end of file + return "%s: %s" % (self.fieldName, ', '.join(vals)) + + def updateFilter(self, opts): + self.setEnumVals(opts) + + def setEnumVals(self, opts): + vals = opts.get('values', []) + prevState = {} + for ch in self.children(): + prevState[ch.name()] = ch.value() + self.removeChild(ch) + + if isinstance(vals, list): + vals = OrderedDict([(v,str(v)) for v in vals]) + for val,vname in vals.items(): + ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, True)) + ch.maskValue = val + self.addChild(ch) + ch = ptree.Parameter.create(name='(other)', type='bool', value=prevState.get('(other)', True)) + ch.maskValue = '__other__' + self.addChild(ch) From 4d26b4bb20013eb5b90b5c5f5b058b26d0eb8906 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 9 May 2018 10:56:01 -0700 Subject: [PATCH 59/60] Fix py3 string handling in scatterplotwidget example --- examples/ScatterPlotWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ScatterPlotWidget.py b/examples/ScatterPlotWidget.py index 33503cab..f3766d56 100644 --- a/examples/ScatterPlotWidget.py +++ b/examples/ScatterPlotWidget.py @@ -28,7 +28,7 @@ pg.mkQApp() # Make up some tabular data with structure data = np.empty(1000, dtype=[('x_pos', float), ('y_pos', float), ('count', int), ('amplitude', float), - ('decay', float), ('type', 'S10')]) + ('decay', float), ('type', 'U10')]) strings = ['Type-A', 'Type-B', 'Type-C', 'Type-D', 'Type-E'] typeInds = np.random.randint(5, size=1000) data['type'] = np.array(strings)[typeInds] From a8610bfb4390766a080829efda322aa109554775 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 9 May 2018 10:57:42 -0700 Subject: [PATCH 60/60] Add option to set enum default values in DataFilterWidget --- pyqtgraph/widgets/DataFilterWidget.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index bd92a765..7b03725c 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -3,6 +3,7 @@ from .. import parametertree as ptree import numpy as np from ..pgcollections import OrderedDict from .. import functions as fn +from ..python2_3 import basestring __all__ = ['DataFilterWidget'] @@ -76,6 +77,7 @@ class DataFilterParameter(ptree.types.GroupParameter): filter.setFields([ ('field1', {'mode': 'range'}), ('field2', {'mode': 'enum', 'values': ['val1', 'val2', 'val3']}), + ('field3', {'mode': 'enum', 'values': {'val1':True, 'val2':False, 'val3':True}}), ]) """ self.fields = OrderedDict(fields) @@ -177,16 +179,30 @@ class EnumFilterItem(ptree.types.SimpleParameter): self.setEnumVals(opts) def setEnumVals(self, opts): - vals = opts.get('values', []) + vals = opts.get('values', {}) + prevState = {} for ch in self.children(): prevState[ch.name()] = ch.value() self.removeChild(ch) - if isinstance(vals, list): - vals = OrderedDict([(v,str(v)) for v in vals]) - for val,vname in vals.items(): - ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, True)) + if not isinstance(vals, dict): + vals = OrderedDict([(v,(str(v), True)) for v in vals]) + + # Each filterable value can come with either (1) a string name, (2) a bool + # indicating whether the value is enabled by default, or (3) a tuple providing + # both. + for val,valopts in vals.items(): + if isinstance(valopts, bool): + enabled = valopts + vname = str(val) + elif isinstance(valopts, basestring): + enabled = True + vname = valopts + elif isinstance(valopts, tuple): + vname, enabled = valopts + + ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, enabled)) ch.maskValue = val self.addChild(ch) ch = ptree.Parameter.create(name='(other)', type='bool', value=prevState.get('(other)', True))