From 90436251c43a9ad1914ac6e9812de8a14832cca7 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 31 May 2022 18:01:07 -0400 Subject: [PATCH 001/306] Bugfix: "children" in checklist state can contaminate item logic --- .../parametertree/parameterTypes/checklist.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyqtgraph/parametertree/parameterTypes/checklist.py b/pyqtgraph/parametertree/parameterTypes/checklist.py index 97e96b5ec2..9d9b515e04 100644 --- a/pyqtgraph/parametertree/parameterTypes/checklist.py +++ b/pyqtgraph/parametertree/parameterTypes/checklist.py @@ -136,6 +136,11 @@ class ChecklistParameter(GroupParameter): itemClass = ChecklistParameterItem def __init__(self, **opts): + # Child options are populated through values, not explicit "children" + if 'children' in opts: + raise ValueError( + "Cannot pass 'children' to ChecklistParameter. Pass a 'value' key only." + ) self.targetValue = None limits = opts.setdefault('limits', []) self.forward, self.reverse = ListParameter.mapping(limits) @@ -225,3 +230,27 @@ def setValue(self, value, blockSignal=None): checked = chParam.name() in names chParam.setValue(checked, self._onChildChanging) super().setValue(self.childrenValue(), blockSignal) + + def saveState(self, filter=None): + # Unlike the normal GroupParameter, child states shouldn't be separately + # preserved + state = super().saveState(filter) + state.pop("children", None) + return state + + def restoreState( + self, + state, + recursive=True, + addChildren=True, + removeChildren=True, + blockSignals=True + ): + # Child management shouldn't happen through state + return super().restoreState( + state, + recursive, + addChildren=False, + removeChildren=False, + blockSignals=blockSignals + ) From ec45beb4ca76f54be8cee201cfd2b8da218bf979 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 31 May 2022 18:02:40 -0400 Subject: [PATCH 002/306] Enhanced testing for saving param states. Revealed bug in Calendar item when no initial value was set, so that was changed too --- pyqtgraph/parametertree/parameterTypes/calendar.py | 3 ++- tests/parametertree/test_parametertypes.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes/calendar.py b/pyqtgraph/parametertree/parameterTypes/calendar.py index 5c61792c4e..48f1fac1c1 100644 --- a/pyqtgraph/parametertree/parameterTypes/calendar.py +++ b/pyqtgraph/parametertree/parameterTypes/calendar.py @@ -51,5 +51,6 @@ def _interpretValue(self, v): def saveState(self, filter=None): state = super().saveState(filter) fmt = self._interpretFormat() - state['value'] = state['value'].toString(fmt) + if state['value'] is not None: + state['value'] = state['value'].toString(fmt) return state diff --git a/tests/parametertree/test_parametertypes.py b/tests/parametertree/test_parametertypes.py index 749504a3db..53fd2cd950 100644 --- a/tests/parametertree/test_parametertypes.py +++ b/tests/parametertree/test_parametertypes.py @@ -173,3 +173,11 @@ def test_pen_settings(): # Opts from changing child p["width"] = 10 assert p.pen.width() == 10 + + +def test_recreate_from_savestate(): + from pyqtgraph.examples import _buildParamTypes + created = _buildParamTypes.makeAllParamTypes() + state = created.saveState() + created2 = pt.Parameter.create(**state) + assert pg.eq(state, created2.saveState()) \ No newline at end of file From ff955b7a2b403d61e29340733297dc5cffd946f2 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 31 May 2022 19:48:59 -0400 Subject: [PATCH 003/306] Bugfix: Checklist button behavior. - Previously, no code checked for proper conditions to enable the default button for a checklist. Note that this implementation still doesn't define behavior for comparing to default when changing exclusive from "false" to "true" - When a button changes param to default, all, or none selected, make sure this isn't overridden by a pending proxy signal from child changes --- .../parametertree/parameterTypes/checklist.py | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/checklist.py b/pyqtgraph/parametertree/parameterTypes/checklist.py index 9d9b515e04..5edbaa99e6 100644 --- a/pyqtgraph/parametertree/parameterTypes/checklist.py +++ b/pyqtgraph/parametertree/parameterTypes/checklist.py @@ -32,12 +32,9 @@ def _constructMetaBtns(self): self.metaBtnLayout.addWidget(btn) btn.clicked.connect(getattr(self, f'{title.lower()}AllClicked')) - self.metaBtns['default'] = WidgetParameterItem.makeDefaultButton(self) + self.metaBtns['default'] = self.makeDefaultButton() self.metaBtnLayout.addWidget(self.metaBtns['default']) - def defaultClicked(self): - self.param.setToDefault() - def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) tw = self.treeWidget() @@ -46,9 +43,13 @@ def treeWidgetChanged(self): tw.setItemWidget(self, 1, self.metaBtnWidget) def selectAllClicked(self): + # timer stop: see explanation on param.setToDefault() + self.param.valChangingProxy.timer.stop() self.param.setValue(self.param.reverse[0]) def clearAllClicked(self): + # timer stop: see explanation on param.setToDefault() + self.param.valChangingProxy.timer.stop() self.param.setValue([]) def insertChild(self, pos, item): @@ -71,14 +72,32 @@ def optsChanged(self, param, opts): btn.setVisible(opts['expanded']) exclusive = opts.get('exclusive', param.opts['exclusive']) enabled = opts.get('enabled', param.opts['enabled']) - for btn in self.metaBtns.values(): - btn.setDisabled(exclusive or (not enabled)) + for name, btn in self.metaBtns.items(): + if name != 'default': + btn.setDisabled(exclusive or (not enabled)) self.btnGrp.setExclusive(exclusive) + # "Limits" will force update anyway, no need to duplicate if it's present + if 'limits' not in opts and ('enabled' in opts or 'readonly' in opts): + self.updateDefaultBtn() def expandedChangedEvent(self, expanded): for btn in self.metaBtns.values(): btn.setVisible(expanded) + def valueChanged(self, param, val): + self.updateDefaultBtn() + + def updateDefaultBtn(self): + self.metaBtns["default"].setEnabled( + not self.param.valueIsDefault() + and self.param.opts["enabled"] + and self.param.writable() + ) + return + + makeDefaultButton = WidgetParameterItem.makeDefaultButton + defaultClicked = WidgetParameterItem.defaultClicked + class RadioParameterItem(BoolParameterItem): """ Allows radio buttons to function as booleans when `exclusive` is *True* @@ -210,7 +229,7 @@ def optsChanged(self, param, opts): self.updateLimits(None, self.opts.get('limits', [])) if 'delay' in opts: self.valChangingProxy.setDelay(opts['delay']) - + def setValue(self, value, blockSignal=None): self.targetValue = value exclusive = self.opts['exclusive'] @@ -231,6 +250,13 @@ def setValue(self, value, blockSignal=None): chParam.setValue(checked, self._onChildChanging) super().setValue(self.childrenValue(), blockSignal) + def setToDefault(self): + # Since changing values are covered by a proxy, this method must be overridden + # to flush changes. Otherwise, setting to default while waiting for changes + # to finalize will override the request to take default values + self.valChangingProxy.timer.stop() + super().setToDefault() + def saveState(self, filter=None): # Unlike the normal GroupParameter, child states shouldn't be separately # preserved From 21fde92020f814b1b7ac35bba191f1d8130e3c14 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 31 May 2022 19:49:16 -0400 Subject: [PATCH 004/306] Bugfix: Checklist "sigValueChanging" incorrect with exclusive parameter. When set to "exclusive", the comparison logic failed to set only one parameter to "True"! --- .../parametertree/parameterTypes/checklist.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/checklist.py b/pyqtgraph/parametertree/parameterTypes/checklist.py index 5edbaa99e6..9cf22d75e1 100644 --- a/pyqtgraph/parametertree/parameterTypes/checklist.py +++ b/pyqtgraph/parametertree/parameterTypes/checklist.py @@ -187,8 +187,13 @@ def childrenValue(self): else: return vals - def _onChildChanging(self, _ch, _val): - self.sigValueChanging.emit(self, self.childrenValue()) + def _onChildChanging(self, child, value): + # When exclusive, ensure only this value is True + if self.opts['exclusive'] and value: + value = self.forward[child.name()] + else: + value = self.childrenValue() + self.sigValueChanging.emit(self, value) def updateLimits(self, _param, limits): oldOpts = self.names @@ -216,11 +221,8 @@ def updateLimits(self, _param, limits): def _finishChildChanges(self, paramAndValue): param, value = paramAndValue - if self.opts['exclusive']: - val = self.reverse[0][self.reverse[1].index(param.name())] - return self.setValue(val) # Interpret value, fire sigValueChanged - return self.setValue(self.childrenValue()) + return self.setValue(value) def optsChanged(self, param, opts): if 'exclusive' in opts: From befa5de46339c42bf527a2337351458f93cf2311 Mon Sep 17 00:00:00 2001 From: Anton Yablokov Date: Sun, 28 Aug 2022 04:44:48 +0300 Subject: [PATCH 005/306] Make auto downsample factor calculation more robust (#2253) * Make auto downsample factor calculation more robust The proposed changes enable ignoring the infinite and NaN values of `x` and avoiding a division by zero if `self.opts['autoDownsampleFactor'] == 0`. * Check against infinity before casting to `int` * Simplified non-zero check, used `math` https://github.com/pyqtgraph/pyqtgraph/pull/2253#issuecomment-1104211065 * Simplified the calculation Consider the following lines: ```python x0 = (view_range.left()-finite_x[0]) / dx x1 = (view_range.right()-finite_x[0]) / dx ``` And later, ```python ds_float = max(1.0, (x1 - x0) / (width * self.opts['autoDownsampleFactor'])) ``` See that `(x1 - x0)` holds the width of `view_range`, divided by `dx`. The values of `x0` and `x1` are not used anywhere else. The values of `x0` and `x1` might be infinite after the division by `dx`, whereas the difference might be finite. So, firstly, I calculate the difference `view_range.right() - view_range.left()`, and then I divide it by `dx`. The initial code utilizes the direction of the axis implicitly, having `(x1 - x0)` always non-negative. I enclose the expression into `abs` to ensure the same behavior. --- pyqtgraph/graphicsItems/PlotDataItem.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 15e87b12ee..2228b47a69 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1,3 +1,4 @@ +import math import warnings import numpy as np @@ -984,14 +985,16 @@ def getDisplayDataset(self): if self.opts['autoDownsample']: # this option presumes that x-values have uniform spacing - if view_range is not None and len(x) > 1: - dx = float(x[-1]-x[0]) / (len(x)-1) + + finite_x = x[np.isfinite(x)] # ignore infinite and nan values + if view_range is not None and len(finite_x) > 1: + dx = float(finite_x[-1]-finite_x[0]) / (len(finite_x)-1) if dx != 0.0: - x0 = (view_range.left()-x[0]) / dx - x1 = (view_range.right()-x[0]) / dx width = self.getViewBox().width() - if width != 0.0: - ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor'])))) + if width != 0.0: # autoDownsampleFactor _should_ be > 1.0 + ds_float = max(1.0, abs(view_range.width() / dx / (width * self.opts['autoDownsampleFactor']))) + if math.isfinite(ds_float): + ds = int(ds_float) ## downsampling is expensive; delay until after clipping. if self.opts['clipToView']: From ad32cb1e6ad5254d3a082661a2610b07be52cc0f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 28 Aug 2022 10:27:04 -0700 Subject: [PATCH 006/306] Bump CI Qt versions --- .github/workflows/main.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c7f71c254..d51b7dfc8c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,19 +24,19 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] qt-lib: [pyqt, pyside] python-version: ["3.8", "3.9", "3.10"] - exclude: - - qt-lib: pyside - python-version: "3.8" include: - python-version: "3.8" qt-lib: "pyqt" - qt-version: "PyQt5~=5.12.0" + qt-version: "PyQt5~=5.15.0" + - python-version: "3.8" + qt-lib: "pyside" + qt-version: "PySide2~=5.15.0" - python-version: "3.9" qt-lib: "pyqt" - qt-version: "PyQt5~=5.15.0" + qt-version: "PyQt6~=6.2.0 PyQt6-Qt6~=6.2.0" - python-version: "3.9" qt-lib: "pyside" - qt-version: "PySide2~=5.15.0" + qt-version: "PySide6~=6.2.0" - python-version: "3.10" qt-lib: "pyqt" qt-version: "PyQt6" @@ -161,7 +161,7 @@ jobs: miniforge-variant: Mambaforge environment-file: ${{ matrix.environment-file }} auto-update-conda: false - python-version: "3.8" + python-version: "3.9" - name: "Install Test Framework" run: pip install pytest pytest-xdist - name: "Install Windows-Mesa OpenGL DLL" From 99780944cfd4629230ca701b023a4bd3b25f3b89 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 28 Aug 2022 10:27:14 -0700 Subject: [PATCH 007/306] Update README to reflect new Qt requirements --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 69bc6fc947..ac978e0d36 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,12 @@ This project supports: * All minor versions of Python released 42 months prior to the project, and at minimum the two latest minor versions. * All minor versions of numpy released in the 24 months prior to the project, and at minimum the last three minor versions. -* All Qt5 versions from 5.12-5.15, and Qt6 6.1+ +* Qt5 5.15, and Qt6 6.2+ Currently this means: * Python 3.8+ -* Qt 5.12-5.15, 6.1+ +* Qt 5.15, 6.2+ * [PyQt5](https://www.riverbankcomputing.com/software/pyqt/), [PyQt6](https://www.riverbankcomputing.com/software/pyqt/), [PySide2](https://wiki.qt.io/Qt_for_Python), or @@ -74,16 +74,15 @@ The following table represents the python environments we test in our CI system. | Qt-Bindings |Python 3.8 | Python 3.9 | Python 3.10 | | :------------- |:---------------------: | :---------------------: | :---------------------: | -| PySide2-5.12 |:eight_spoked_asterisk: | :eight_spoked_asterisk: | :eight_spoked_asterisk: | -| PyQt5-5.12 |:white_check_mark: | :x: | :x: | -| PySide2-5.15 |:white_check_mark: | :white_check_mark: | | -| PyQt5-5.15 |:white_check_mark: | :white_check_mark: | | +| PySide2-5.15 | :white_check_mark: | :white_check_mark: | | +| PyQt5-5.15 | :white_check_mark: | :white_check_mark: | | +| PySide6-6.2 | | :white_check_mark: | | +| PyQt6-6.2 | | :white_check_mark: | | | PySide6-6.3 | | | :white_check_mark: | | PyQt6-6.3 | | | :white_check_mark: | * :x: - Not compatible * :white_check_mark: - Tested -* :eight_spoked_asterisk: - only available with `conda-forge` package * No icon means supported configuration but we do not explicitely test it Support From 650d69c43484eea2526dabae32c55b8e68ee7bba Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 28 Aug 2022 10:27:36 -0700 Subject: [PATCH 008/306] Bump tox config for use supported Qt versions --- tox.ini | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index cfad5eb066..abc2c85859 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,6 @@ envlist = ; qt 5.15.x py{38,39,310}-{pyqt5,pyside2}_515 - ; py38-pyside2_512 doesn't work due to PYSIDE-1140 - py38-pyqt5_512 - ; qt 6.2 py{38,39,310}-{pyqt6,pyside6}_62 @@ -26,14 +23,13 @@ passenv = DISPLAY XAUTHORITY, PYTHON_VERSION setenv = PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command deps= {[base]deps} - pyqt5_512: pyqt5~=5.12.0 pyside2_515: pyside2 pyqt5_515: pyqt5 pyqt6_62: pyqt6~=6.2.0 pyqt6_62: PyQt6-Qt6~=6.2.0 pyside6_62: pyside6~=6.2.0 pyqt6: pyqt6 - pyside6: pyside6 + pyside6: PySide6-Essentials commands= python -c "import pyqtgraph as pg; pg.systemInfo()" From 895be36cd7bfc4dbc5d22f288b539085cf789a9f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 28 Aug 2022 11:44:59 -0700 Subject: [PATCH 009/306] Remove deprecated functionality --- pyqtgraph/Qt/__init__.py | 50 -------- pyqtgraph/__init__.py | 4 +- pyqtgraph/flowchart/Node.py | 16 --- pyqtgraph/functions.py | 12 +- pyqtgraph/graphicsItems/AxisItem.py | 11 -- pyqtgraph/graphicsItems/GraphicsItem.py | 8 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 40 +------ pyqtgraph/graphicsItems/ROI.py | 50 +------- pyqtgraph/graphicsItems/ScatterPlotItem.py | 113 ------------------ pyqtgraph/graphicsItems/TargetItem.py | 34 ------ pyqtgraph/graphicsWindows.py | 117 ------------------- pyqtgraph/metaarray/MetaArray.py | 11 -- pyqtgraph/opengl/GLViewWidget.py | 11 -- pyqtgraph/parametertree/Parameter.py | 18 --- pyqtgraph/util/lru_cache.py | 93 --------------- pyqtgraph/util/pil_fix.py | 69 ----------- tests/test_ref_cycles.py | 11 -- 17 files changed, 10 insertions(+), 658 deletions(-) delete mode 100644 pyqtgraph/graphicsWindows.py delete mode 100644 pyqtgraph/util/lru_cache.py delete mode 100644 pyqtgraph/util/pil_fix.py diff --git a/pyqtgraph/Qt/__init__.py b/pyqtgraph/Qt/__init__.py index 040718d384..744a246fdd 100644 --- a/pyqtgraph/Qt/__init__.py +++ b/pyqtgraph/Qt/__init__.py @@ -258,56 +258,6 @@ def _copy_attrs(src, dst): if QT_LIB in [PYQT5, PYSIDE2]: __QGraphicsItem_scale = QtWidgets.QGraphicsItem.scale - - def scale(self, *args): - warnings.warn( - "Deprecated Qt API, will be removed in 0.13.0.", - DeprecationWarning, stacklevel=2 - ) - if args: - sx, sy = args - tr = self.transform() - tr.scale(sx, sy) - self.setTransform(tr) - else: - return __QGraphicsItem_scale(self) - QtWidgets.QGraphicsItem.scale = scale - - def rotate(self, angle): - warnings.warn( - "Deprecated Qt API, will be removed in 0.13.0.", - DeprecationWarning, stacklevel=2 - ) - tr = self.transform() - tr.rotate(angle) - self.setTransform(tr) - QtWidgets.QGraphicsItem.rotate = rotate - - def translate(self, dx, dy): - warnings.warn( - "Deprecated Qt API, will be removed in 0.13.0.", - DeprecationWarning, stacklevel=2 - ) - tr = self.transform() - tr.translate(dx, dy) - self.setTransform(tr) - QtWidgets.QGraphicsItem.translate = translate - - def setMargin(self, i): - warnings.warn( - "Deprecated Qt API, will be removed in 0.13.0.", - DeprecationWarning, stacklevel=2 - ) - self.setContentsMargins(i, i, i, i) - QtWidgets.QGridLayout.setMargin = setMargin - - def setResizeMode(self, *args): - warnings.warn( - "Deprecated Qt API, will be removed in 0.13.0.", - DeprecationWarning, stacklevel=2 - ) - self.setSectionResizeMode(*args) - QtWidgets.QHeaderView.setResizeMode = setResizeMode # Import all QtWidgets objects into QtGui _fallbacks = dir(QtWidgets) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 4910798154..3349872e97 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -7,9 +7,9 @@ ### import all the goodies and add some helper functions for easy CLI use +import importlib import os import sys -import importlib import numpy # # pyqtgraph requires numpy @@ -243,7 +243,6 @@ def renamePyc(startDir): # indirect imports used within library from .GraphicsScene import GraphicsScene -from .graphicsWindows import * from .imageview import * # indirect imports known to be used outside of the library @@ -289,7 +288,6 @@ def renamePyc(startDir): from .widgets.ValueLabel import * from .widgets.VerticalLabel import * - ############################################################## ## PyQt and PySide both are prone to crashing on exit. ## There are two general approaches to dealing with this: diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index dc6c582a73..00503da0fe 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -190,22 +190,6 @@ def graphicsItem(self): self._graphicsItem = NodeGraphicsItem(self) return self._graphicsItem - ## this is just bad planning. Causes too many bugs. - def __getattr__(self, attr): - """Return the terminal with the given name""" - warnings.warn( - "Use of note.terminalName is deprecated, use node['terminalName'] instead" - "Will be removed from 0.13.0", - DeprecationWarning, stacklevel=2 - ) - - if attr not in self.terminals: - raise AttributeError(attr) - else: - import traceback - traceback.print_stack() - print("Warning: use of node.terminalName is deprecated; use node['terminalName'] instead.") - return self.terminals[attr] def __getitem__(self, item): #return getattr(self, item) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 31880c9408..96a45121fe 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -241,9 +241,9 @@ def mkColor(*args): float greyscale, 0.0-1.0 int see :func:`intColor() ` (int, hues) see :func:`intColor() ` - "#RGB" hexadecimal strings prefixed with '#' - "#RGBA" previously allowed use without prefix is deprecated and - "#RRGGBB" will be removed in 0.13 + "#RGB" + "#RGBA" + "#RRGGBB" "#RRGGBBAA" QColor QColor instance; makes a copy. ================ ================================================ @@ -270,11 +270,7 @@ def mkColor(*args): if c[0] == '#': c = c[1:] else: - warnings.warn( - "Parsing of hex strings that do not start with '#' is" - "deprecated and support will be removed in 0.13", - DeprecationWarning, stacklevel=2 - ) + raise ValueError(f"Unable to convert {c} to QColor") if len(c) == 3: r = int(c[0]*2, 16) g = int(c[1]*2, 16) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 278572c509..a29d622a3b 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -515,17 +515,6 @@ def setScale(self, scale=None): the view coordinate system were scaled. By default, the axis scaling is 1.0. """ - # Deprecated usage, kept for backward compatibility - if scale is None: - warnings.warn( - 'AxisItem.setScale(None) is deprecated, will be removed in 0.13.0' - 'instead use AxisItem.enableAutoSIPrefix(bool) to enable/disable' - 'SI prefix scaling', - DeprecationWarning, stacklevel=2 - ) - scale = 1.0 - self.enableAutoSIPrefix(True) - if scale != self.scale: self.scale = scale self._updateLabel() diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 61ae1f144c..c81987adb3 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -48,7 +48,7 @@ class GraphicsItem(object): """ _pixelVectorGlobalCache = LRU(100) - def __init__(self, register=None): + def __init__(self): if not hasattr(self, '_qtBaseClass'): for b in self.__class__.__bases__: if issubclass(b, QtWidgets.QGraphicsItem): @@ -63,11 +63,7 @@ def __init__(self, register=None): self._connectedView = None self._exportOpts = False ## If False, not currently exporting. Otherwise, contains dict of export options. self._cachedView = None - if register is not None and register: - warnings.warn( - "'register' argument is deprecated and does nothing, will be removed in 0.13", - DeprecationWarning, stacklevel=2 - ) + def getViewWidget(self): """ diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 5c641812ba..7f1256d035 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -21,7 +21,6 @@ from ..ScatterPlotItem import ScatterPlotItem from ..ViewBox import ViewBox - translate = QtCore.QCoreApplication.translate from . import plotConfigTemplate_generic as ui_template @@ -502,17 +501,6 @@ def autoBtnClicked(self): def viewStateChanged(self): self.updateButtons() - - def enableAutoScale(self): - """ - Enable auto-scaling. The plot will continuously scale to fit the boundaries of its data. - """ - warnings.warn( - 'PlotItem.enableAutoScale is deprecated, and will be removed in 0.13' - 'Use PlotItem.enableAutoRange(axis, enable) instead', - DeprecationWarning, stacklevel=2 - ) - self.vb.enableAutoRange(self.vb.XYAxes) def addItem(self, item, *args, **kargs): """ @@ -568,27 +556,11 @@ def addItem(self, item, *args, **kargs): if name is not None and hasattr(self, 'legend') and self.legend is not None: self.legend.addItem(item, name=name) - def addDataItem(self, item, *args): - warnings.warn( - 'PlotItem.addDataItem is deprecated and will be removed in 0.13. ' - 'Use PlotItem.addItem instead', - DeprecationWarning, stacklevel=2 - ) - self.addItem(item, *args) - def listDataItems(self): """Return a list of all data items (:class:`~pyqtgrpah.PlotDataItem`, :class:`~pyqtgraph.PlotCurveItem`, :class:`~pyqtgraph.ScatterPlotItem`, etc) contained in this PlotItem.""" return self.dataItems[:] - - def addCurve(self, c, params=None): - warnings.warn( - 'PlotItem.addCurve is deprecated and will be removed in 0.13. ' - 'Use PlotItem.addItem instead.', - DeprecationWarning, stacklevel=2 - ) - self.addItem(c, params) def addLine(self, x=None, y=None, z=None, **kwds): """ @@ -693,7 +665,7 @@ def addColorBar(self, image, **kargs): A call like `plot.addColorBar(img, colorMap='viridis')` is a convenient method to assign and show a color map. """ - from ..ColorBarItem import ColorBarItem # avoid circular import + from ..ColorBarItem import ColorBarItem # avoid circular import bar = ColorBarItem(**kargs) bar.setImageItem( image, insert_in=self ) return bar @@ -1264,15 +1236,7 @@ def showAxes(self, selection, showValues=True, size=False): elif axis_key in ('top', 'bottom'): if show_value: ax.setHeight(size[1]) else : ax.setHeight( None ) - - def showScale(self, *args, **kargs): - warnings.warn( - 'PlotItem.showScale has been deprecated and will be removed in 0.13. ' - 'Use PlotItem.showAxis() instead', - DeprecationWarning, stacklevel=2 - ) - return self.showAxis(*args, **kargs) - + def hideButtons(self): """Causes auto-scale button ('A' in lower-left corner) to be hidden for this PlotItem""" #self.ctrlBtn.hide() diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 76bb40c466..36c4c7f21c 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -30,7 +30,7 @@ __all__ = [ 'ROI', - 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', + 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'CrosshairROI','TriangleROI' ] @@ -1935,54 +1935,6 @@ def _addHandles(self): self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) -class PolygonROI(ROI): - - def __init__(self, positions, pos=None, **args): - warnings.warn( - 'PolygonROI has been deprecated, will be removed in 0.13' - 'use PolyLineROI instead', - DeprecationWarning, stacklevel=2 - ) - - if pos is None: - pos = [0,0] - ROI.__init__(self, pos, [1,1], **args) - for p in positions: - self.addFreeHandle(p) - self.setZValue(1000) - - def listPoints(self): - return [p['item'].pos() for p in self.handles] - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) - p.setPen(self.currentPen) - for i in range(len(self.handles)): - h1 = self.handles[i]['item'].pos() - h2 = self.handles[i-1]['item'].pos() - p.drawLine(h1, h2) - - def boundingRect(self): - r = QtCore.QRectF() - for h in self.handles: - r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs - return r - - def shape(self): - p = QtGui.QPainterPath() - p.moveTo(self.handles[0]['item'].pos()) - for i in range(len(self.handles)): - p.lineTo(self.handles[i]['item'].pos()) - return p - - def stateCopy(self): - sc = {} - sc['pos'] = Point(self.state['pos']) - sc['size'] = Point(self.state['size']) - sc['angle'] = self.state['angle'] - return sc - - class PolyLineROI(ROI): r""" Container class for multiple connected LineSegmentROIs. diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 0168a2fb44..2436f3e4f8 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -109,15 +109,6 @@ def renderSymbol(symbol, size, pen, brush, device=None): p.end() return device -def makeSymbolPixmap(size, pen, brush, symbol): - warnings.warn( - "This is an internal function that is no longer being used. " - "Will be removed in 0.13", - DeprecationWarning, stacklevel=2 - ) - img = renderSymbol(symbol, size, pen, brush) - return QtGui.QPixmap(img) - def _mkPen(*args, **kwargs): """ @@ -512,12 +503,6 @@ def setData(self, *args, **kargs): generating LegendItem entries and by some exporters. ====================== =============================================================================================== """ - if 'identical' in kargs: - warnings.warn( - "The *identical* functionality is handled automatically now. " - "Will be removed in 0.13.", - DeprecationWarning, stacklevel=2 - ) oldData = self.data ## this causes cached pixmaps to be preserved while new data is registered. self.clear() ## clear out all old data self.addPoints(*args, **kargs) @@ -651,14 +636,6 @@ def invalidate(self): def getData(self): return self.data['x'], self.data['y'] - def setPoints(self, *args, **kargs): - warnings.warn( - "ScatterPlotItem.setPoints is deprecated, use ScatterPlotItem.setData " - "instead. Will be removed in 0.13", - DeprecationWarning, stacklevel=2 - ) - return self.setData(*args, **kargs) - def implements(self, interface=None): ints = ['plotData'] if interface is None: @@ -909,58 +886,6 @@ def _measureSpotSizes(self, **kwargs): else: yield size + pen.widthF(), 0 - def getSpotOpts(self, recs, scale=1.0): - warnings.warn( - "This is an internal method that is no longer being used. Will be " - "removed in 0.13", - DeprecationWarning, stacklevel=2 - ) - if recs.ndim == 0: - rec = recs - symbol = rec['symbol'] - if symbol is None: - symbol = self.opts['symbol'] - size = rec['size'] - if size < 0: - size = self.opts['size'] - pen = rec['pen'] - if pen is None: - pen = self.opts['pen'] - brush = rec['brush'] - if brush is None: - brush = self.opts['brush'] - return (symbol, size*scale, fn.mkPen(pen), fn.mkBrush(brush)) - else: - recs = recs.copy() - recs['symbol'][np.equal(recs['symbol'], None)] = self.opts['symbol'] - recs['size'][np.equal(recs['size'], -1)] = self.opts['size'] - recs['size'] *= scale - recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen']) - recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush']) - return recs - - def measureSpotSizes(self, dataSet): - warnings.warn( - "This is an internal method that is no longer being used. " - "Will be removed in 0.13.", - DeprecationWarning, stacklevel=2 - ) - for size, pen in zip(*self._style(['size', 'pen'], data=dataSet)): - ## keep track of the maximum spot size and pixel size - width = 0 - pxWidth = 0 - if self.opts['pxMode']: - pxWidth = size + pen.widthF() - else: - width = size - if pen.isCosmetic(): - pxWidth += pen.widthF() - else: - width += pen.widthF() - self._maxSpotWidth = max(self._maxSpotWidth, width) - self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) - self.bounds = [None, None] - def clear(self): """Remove all spots from the scatter plot""" #self.clearItems() @@ -1045,44 +970,6 @@ def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) self.invalidate() - def mapPointsToDevice(self, pts): - warnings.warn( - "This is an internal method that is no longer being used. " - "Will be removed in 0.13", - DeprecationWarning, stacklevel=2 - ) - # Map point locations to device - tr = self.deviceTransform() - if tr is None: - return None - - pts = fn.transformCoordinates(tr, pts) - pts -= self.data['sourceRect']['w'] / 2 - pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. - - return pts - - def getViewMask(self, pts): - warnings.warn( - "This is an internal method that is no longer being used. " - "Will be removed in 0.13", - DeprecationWarning, stacklevel=2 - ) - # Return bool mask indicating all points that are within viewbox - # pts is expressed in *device coordiantes* - vb = self.getViewBox() - if vb is None: - return None - viewBounds = vb.mapRectToDevice(vb.boundingRect()) - w = self.data['sourceRect']['w'] / 2 - mask = ((pts[0] + w > viewBounds.left()) & - (pts[0] - w < viewBounds.right()) & - (pts[1] + w > viewBounds.top()) & - (pts[1] - w < viewBounds.bottom())) ## remove out of view points - - mask &= self.data['visible'] - return mask - @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): profiler = debug.Profiler() diff --git a/pyqtgraph/graphicsItems/TargetItem.py b/pyqtgraph/graphicsItems/TargetItem.py index c78c93c67d..010bdbb581 100644 --- a/pyqtgraph/graphicsItems/TargetItem.py +++ b/pyqtgraph/graphicsItems/TargetItem.py @@ -27,7 +27,6 @@ def __init__( self, pos=None, size=10, - radii=None, symbol="crosshair", pen=None, hoverPen=None, @@ -44,8 +43,6 @@ def __init__( Initial position of the symbol. Default is (0, 0) size : int Size of the symbol in pixels. Default is 10. - radii : tuple of int - Deprecated. Gives size of crosshair in screen pixels. pen : QPen, tuple, list or str Pen to use when drawing line. Can be any arguments that are valid for :func:`~pyqtgraph.mkPen`. Default pen is transparent yellow. @@ -84,16 +81,6 @@ def __init__( self._label = None self.mouseHovering = False - if radii is not None: - warnings.warn( - "'radii' is now deprecated, and will be removed in 0.13.0. Use 'size' " - "parameter instead", - DeprecationWarning, - stacklevel=2, - ) - symbol = makeCrosshair(*radii) - size = 1 - if pen is None: pen = (255, 255, 0) self.setPen(pen) @@ -134,16 +121,6 @@ def __init__( self.setPath(self._path) self.setLabel(label, labelOpts) - @property - def sigDragged(self): - warnings.warn( - "'sigDragged' has been deprecated and will be removed in 0.13.0. Use " - "`sigPositionChangeFinished` instead", - DeprecationWarning, - stacklevel=2, - ) - return self.sigPositionChangeFinished - def setPos(self, *args): """Method to set the position to ``(x, y)`` within the plot view @@ -346,17 +323,6 @@ def setLabel(self, text=None, labelOpts=None): self._label.scene().removeItem(self._label) self._label = TargetLabel(self, text=text, **labelOpts) - def setLabelAngle(self, angle): - warnings.warn( - "TargetItem.setLabelAngle is deprecated and will be removed in 0.13.0." - "Use TargetItem.label().setAngle() instead", - DeprecationWarning, - stacklevel=2, - ) - if self.label() is not None and angle != self.label().angle: - self.label().setAngle(angle) - return None - class TargetLabel(TextItem): """A TextItem that attaches itself to a TargetItem. diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py deleted file mode 100644 index 7aa57d4cde..0000000000 --- a/pyqtgraph/graphicsWindows.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -DEPRECATED: The classes below are convenience classes that create a new window -containting a single, specific widget. These classes are now unnecessary because -it is possible to place any widget into its own window by simply calling its -show() method. -""" - -__all__ = ['GraphicsWindow', 'TabWindow', 'PlotWindow', 'ImageWindow'] - -import warnings - -from .imageview import * -from .Qt import QtCore, QtWidgets, mkQApp -from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget -from .widgets.PlotWidget import * - - -class GraphicsWindow(GraphicsLayoutWidget): - """ - (deprecated; use :class:`~pyqtgraph.GraphicsLayoutWidget` instead) - - Convenience subclass of :class:`~pyqtgraph.GraphicsLayoutWidget`. This class - is intended for use from the interactive python prompt. - """ - def __init__(self, title=None, size=(800,600), **kargs): - warnings.warn( - 'GraphicsWindow is deprecated, use GraphicsLayoutWidget instead,' - 'will be removed in 0.13', - DeprecationWarning, stacklevel=2 - ) - mkQApp() - GraphicsLayoutWidget.__init__(self, **kargs) - self.resize(*size) - if title is not None: - self.setWindowTitle(title) - self.show() - - -class TabWindow(QtWidgets.QMainWindow): - """ - (deprecated) - """ - def __init__(self, title=None, size=(800,600)): - warnings.warn( - 'TabWindow is deprecated, will be removed in 0.13', - DeprecationWarning, stacklevel=2 - ) - mkQApp() - QtWidgets.QMainWindow.__init__(self) - self.resize(*size) - self.cw = QtWidgets.QTabWidget() - self.setCentralWidget(self.cw) - if title is not None: - self.setWindowTitle(title) - self.show() - - def __getattr__(self, attr): - return getattr(self.cw, attr) - - -class PlotWindow(PlotWidget): - sigClosed = QtCore.Signal(object) - - """ - (deprecated; use :class:`~pyqtgraph.PlotWidget` instead) - """ - def __init__(self, title=None, **kargs): - warnings.warn( - 'PlotWindow is deprecated, use PlotWidget instead,' - 'will be removed in 0.13', - DeprecationWarning, stacklevel=2 - ) - mkQApp() - self.win = QtWidgets.QMainWindow() - PlotWidget.__init__(self, **kargs) - self.win.setCentralWidget(self) - for m in ['resize']: - setattr(self, m, getattr(self.win, m)) - if title is not None: - self.win.setWindowTitle(title) - self.win.show() - - def closeEvent(self, event): - PlotWidget.closeEvent(self, event) - self.sigClosed.emit(self) - - -class ImageWindow(ImageView): - sigClosed = QtCore.Signal(object) - - """ - (deprecated; use :class:`~pyqtgraph.ImageView` instead) - """ - def __init__(self, *args, **kargs): - warnings.warn( - 'ImageWindow is deprecated, use ImageView instead' - 'will be removed in 0.13', - DeprecationWarning, stacklevel=2 - ) - mkQApp() - self.win = QtWidgets.QMainWindow() - self.win.resize(800,600) - if 'title' in kargs: - self.win.setWindowTitle(kargs['title']) - del kargs['title'] - ImageView.__init__(self, self.win) - if len(args) > 0 or len(kargs) > 0: - self.setImage(*args, **kargs) - - self.win.setCentralWidget(self) - for m in ['resize']: - setattr(self, m, getattr(self.win, m)) - self.win.show() - - def closeEvent(self, event): - ImageView.closeEvent(self, event) - self.sigClosed.emit(self) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 480256b1f8..000b55c6fd 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -323,17 +323,6 @@ def __array__(self, dtype=None): return self.asarray() else: return self.asarray().astype(dtype) - - def view(self, typ): - warnings.warn( - 'MetaArray.view is deprecated and will be removed in 0.13. ' - 'Use MetaArray.asarray() instead.', - DeprecationWarning, stacklevel=2 - ) - if typ is np.ndarray: - return self.asarray() - else: - raise Exception('invalid view type: %s' % str(typ)) def axisValues(self, axis): """Return the list of values for an axis""" diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 282145e051..56cd90e6bc 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -377,18 +377,7 @@ def pan(self, dx, dy, dz, relative='global'): Distances are scaled roughly such that a value of 1.0 moves by one pixel on screen. - - Prior to version 0.11, *relative* was expected to be either True (x-aligned) or - False (global). These values are deprecated but still recognized. """ - # for backward compatibility: - if isinstance(relative, bool): - warnings.warn( - "'relative' as a boolean is deprecated, and will not be recognized in 0.13. " - "Acceptable values are 'global', 'view', or 'view-upright'", - DeprecationWarning, stacklevel=2 - ) - relative = {True: "view-upright", False: "global"}.get(relative, relative) if relative == 'global': self.opts['center'] += QtGui.QVector3D(dx, dy, dz) elif relative == 'view-upright': diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index cd2a53e65c..28e0dcffbd 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -755,24 +755,6 @@ def param(self, *names): def __repr__(self): return "<%s '%s' at 0x%x>" % (self.__class__.__name__, self.name(), id(self)) - def __getattr__(self, attr): - ## Leaving this undocumented because I might like to remove it in the future.. - #print type(self), attr - warnings.warn( - 'Use of Parameter.subParam is deprecated and will be removed in 0.13 ' - 'Use Parameter.param(name) instead.', - DeprecationWarning, stacklevel=2 - ) - if 'names' not in self.__dict__: - raise AttributeError(attr) - if attr in self.names: - import traceback - traceback.print_stack() - print("Warning: Use of Parameter.subParam is deprecated. Use Parameter.param(name) instead.") - return self.param(attr) - else: - raise AttributeError(attr) - def _renameChild(self, child, name): ## Only to be called from Parameter.rename if name in self.names: diff --git a/pyqtgraph/util/lru_cache.py b/pyqtgraph/util/lru_cache.py deleted file mode 100644 index 6c5817ead7..0000000000 --- a/pyqtgraph/util/lru_cache.py +++ /dev/null @@ -1,93 +0,0 @@ -import warnings - -warnings.warn( - "No longer used in pyqtgraph. Will be removed in 0.13", - DeprecationWarning, stacklevel=2 -) - -import itertools -import operator - - -class LRUCache(object): - ''' - This LRU cache should be reasonable for short collections (until around 100 items), as it does a - sort on the items if the collection would become too big (so, it is very fast for getting and - setting but when its size would become higher than the max size it does one sort based on the - internal time to decide which items should be removed -- which should be Ok if the resizeTo - isn't too close to the maxSize so that it becomes an operation that doesn't happen all the - time). - ''' - - def __init__(self, maxSize=100, resizeTo=70): - ''' - ============== ========================================================= - **Arguments:** - maxSize (int) This is the maximum size of the cache. When some - item is added and the cache would become bigger than - this, it's resized to the value passed on resizeTo. - resizeTo (int) When a resize operation happens, this is the size - of the final cache. - ============== ========================================================= - ''' - assert resizeTo < maxSize - self.maxSize = maxSize - self.resizeTo = resizeTo - self._counter = 0 - self._dict = {} - self._nextTime = itertools.count(0).__next__ - - def __getitem__(self, key): - item = self._dict[key] - item[2] = self._nextTime() - return item[1] - - def __len__(self): - return len(self._dict) - - def __setitem__(self, key, value): - item = self._dict.get(key) - if item is None: - if len(self._dict) + 1 > self.maxSize: - self._resizeTo() - - item = [key, value, self._nextTime()] - self._dict[key] = item - else: - item[1] = value - item[2] = self._nextTime() - - def __delitem__(self, key): - del self._dict[key] - - def get(self, key, default=None): - try: - return self[key] - except KeyError: - return default - - def clear(self): - self._dict.clear() - - def values(self): - return [i[1] for i in self._dict.values()] - - def keys(self): - return [x[0] for x in self._dict.values()] - - def _resizeTo(self): - ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resizeTo] - for i in ordered: - del self._dict[i[0]] - - def items(self, accessTime=False): - ''' - :param bool accessTime: - If True sorts the returned items by the internal access time. - ''' - if accessTime: - for x in sorted(self._dict.values(), key=operator.itemgetter(2)): - yield x[0], x[1] - else: - for x in self._dict.items(): - yield x[0], x[1] diff --git a/pyqtgraph/util/pil_fix.py b/pyqtgraph/util/pil_fix.py deleted file mode 100644 index 7746162023..0000000000 --- a/pyqtgraph/util/pil_fix.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Importing this module installs support for 16-bit images in PIL. -This works by patching objects in the PIL namespace; no files are -modified. -""" - -import warnings - -warnings.warn( - "Not used in pyqtgraph. Will be removed in 0.13", - DeprecationWarning, stacklevel=2 -) - -from PIL import Image - -if Image.VERSION == '1.1.7': - Image._MODE_CONV["I;16"] = ('%su2' % Image._ENDIAN, None) - Image._fromarray_typemap[((1, 1), " ndmax: - raise ValueError("Too many dimensions.") - - size = shape[:2][::-1] - if strides is not None: - obj = obj.tostring() - - return frombuffer(mode, size, obj, "raw", mode, 0, 1) - - Image.fromarray=fromarray diff --git a/tests/test_ref_cycles.py b/tests/test_ref_cycles.py index 391709f745..f602116e01 100644 --- a/tests/test_ref_cycles.py +++ b/tests/test_ref_cycles.py @@ -53,17 +53,6 @@ def mkobjs(*args, **kwds): for _ in range(5): assert_alldead(mkobjs()) -def test_GraphicsWindow(): - def mkobjs(): - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - w = pg.GraphicsWindow() - p1 = w.addPlot() - v1 = w.addViewBox() - return mkrefs(w, p1, v1) - - for _ in range(5): - assert_alldead(mkobjs()) def test_ImageView(): def mkobjs(): From f91ee4d97fc395b94fd7e82c6c1f09802ddced32 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 28 Aug 2022 12:04:45 -0700 Subject: [PATCH 010/306] Remove tests that use deprecated parts of the library --- pyqtgraph/examples/MultiPlotWidget.py | 35 ------------------ pyqtgraph/examples/utils.py | 1 - tests/util/test_lru_cache.py | 51 --------------------------- 3 files changed, 87 deletions(-) delete mode 100644 pyqtgraph/examples/MultiPlotWidget.py delete mode 100644 tests/util/test_lru_cache.py diff --git a/pyqtgraph/examples/MultiPlotWidget.py b/pyqtgraph/examples/MultiPlotWidget.py deleted file mode 100644 index 84802288e8..0000000000 --- a/pyqtgraph/examples/MultiPlotWidget.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/python - -import numpy as np -from numpy import linspace - -import pyqtgraph as pg -from pyqtgraph import MultiPlotWidget -from pyqtgraph.Qt import QtWidgets - -try: - from pyqtgraph.metaarray import * -except: - print("MultiPlot is only used with MetaArray for now (and you do not have the metaarray package)") - exit() - -app = pg.mkQApp("MultiPlot Widget Example") -mw = QtWidgets.QMainWindow() -mw.resize(800,800) -pw = MultiPlotWidget() -mw.setCentralWidget(pw) -mw.show() - -data = np.random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]]) -ma = MetaArray(data, info=[ - {'name': 'Signal', 'cols': [ - {'name': 'Col1', 'units': 'V'}, - {'name': 'Col2', 'units': 'A'}, - {'name': 'Col3'}, - ]}, - {'name': 'Time', 'values': linspace(0., 1., 1000), 'units': 's'} - ]) -pw.plot(ma, pen='y') - -if __name__ == '__main__': - pg.exec() diff --git a/pyqtgraph/examples/utils.py b/pyqtgraph/examples/utils.py index ac300de300..3badf69e48 100644 --- a/pyqtgraph/examples/utils.py +++ b/pyqtgraph/examples/utils.py @@ -112,7 +112,6 @@ ('GradientEditor', 'GradientEditor.py'), ('GLViewWidget', 'GLViewWidget.py'), ('DiffTreeWidget', 'DiffTreeWidget.py'), - ('MultiPlotWidget', 'MultiPlotWidget.py'), ('RemoteGraphicsView', 'RemoteGraphicsView.py'), ('contextMenu', 'contextMenu.py'), ('designerExample', 'designerExample.py'), diff --git a/tests/util/test_lru_cache.py b/tests/util/test_lru_cache.py deleted file mode 100644 index be1eb5d956..0000000000 --- a/tests/util/test_lru_cache.py +++ /dev/null @@ -1,51 +0,0 @@ -import warnings - -with warnings.catch_warnings(): - warnings.simplefilter('ignore') - from pyqtgraph.util.lru_cache import LRUCache - - -def testLRU(): - lru = LRUCache(2, 1) - # check twice - checkLru(lru) - checkLru(lru) - -def checkLru(lru): - lru[1] = 1 - lru[2] = 2 - lru[3] = 3 - - assert len(lru) == 2 - assert set([2, 3]) == set(lru.keys()) - assert set([2, 3]) == set(lru.values()) - - lru[2] = 2 - assert set([2, 3]) == set(lru.values()) - - lru[1] = 1 - set([2, 1]) == set(lru.values()) - - #Iterates from the used in the last access to others based on access time. - assert [(2, 2), (1, 1)] == list(lru.items(accessTime=True)) - lru[2] = 2 - assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) - - del lru[2] - assert [(1, 1), ] == list(lru.items(accessTime=True)) - - lru[2] = 2 - assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) - - _ = lru[1] - assert [(2, 2), (1, 1)] == list(lru.items(accessTime=True)) - - _ = lru[2] - assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) - - assert lru.get(2) == 2 - assert lru.get(3) is None - assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) - - lru.clear() - assert [] == list(lru.items()) From 9607089941e02de2086515b384ac8f0a10ee03a8 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 28 Aug 2022 12:25:47 -0700 Subject: [PATCH 011/306] Remove graphicswindow references in docs --- doc/source/apireference.rst | 1 - doc/source/graphicswindow.rst | 16 ---------------- doc/source/how_to_use.rst | 2 +- pyqtgraph/graphicsItems/GraphicsLayout.py | 2 +- 4 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 doc/source/graphicswindow.rst diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index 2392a65ebc..27a8ae8848 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -16,4 +16,3 @@ Contents: dockarea graphicsscene/index flowchart/index - graphicswindow diff --git a/doc/source/graphicswindow.rst b/doc/source/graphicswindow.rst deleted file mode 100644 index 0602ae7ed2..0000000000 --- a/doc/source/graphicswindow.rst +++ /dev/null @@ -1,16 +0,0 @@ -Deprecated Window Classes -========================= - -.. automodule:: pyqtgraph.graphicsWindows - -.. autoclass:: pyqtgraph.GraphicsWindow - :members: - -.. autoclass:: pyqtgraph.TabWindow - :members: - -.. autoclass:: pyqtgraph.PlotWindow - :members: - -.. autoclass:: pyqtgraph.ImageWindow - :members: diff --git a/doc/source/how_to_use.rst b/doc/source/how_to_use.rst index ca2a9f7d18..c957c565b4 100644 --- a/doc/source/how_to_use.rst +++ b/doc/source/how_to_use.rst @@ -24,7 +24,7 @@ Further examples:: pw = pg.plot(xVals, yVals, pen='r') # plot x vs y in red pw.plot(xVals, yVals2, pen='b') - win = pg.GraphicsWindow() # Automatically generates grids with multiple items + win = pg.GraphicsLayoutWidget() # Automatically generates grids with multiple items win.addPlot(data1, row=0, col=0) win.addPlot(data2, row=0, col=1) win.addPlot(data3, row=1, col=0, colspan=2) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index 0cb8658b3f..2e90b41ec8 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -11,7 +11,7 @@ class GraphicsLayout(GraphicsWidget): """ Used for laying out GraphicsWidgets in a grid. - This is usually created automatically as part of a :class:`GraphicsWindow ` or :class:`GraphicsLayoutWidget `. + This is usually created automatically as part of a :class:`GraphicsLayoutWidget `. """ def __init__(self, parent=None, border=None): From b2e1c0e9dec0d6b04655d85c53fd510ab5f3d58e Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 28 Aug 2022 15:51:18 -0700 Subject: [PATCH 012/306] Remove more deprecated things --- pyqtgraph/GraphicsScene/GraphicsScene.py | 7 - pyqtgraph/SRTTransform.py | 8 - pyqtgraph/__init__.py | 2 - pyqtgraph/colormap.py | 5 - pyqtgraph/examples/GraphicsScene.py | 1 - pyqtgraph/graphicsItems/ColorBarItem.py | 26 +- pyqtgraph/graphicsItems/PlotDataItem.py | 11 - pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 1 - pyqtgraph/icons/__init__.py | 12 - pyqtgraph/ordereddict.py | 12 - pyqtgraph/pgcollections.py | 484 ------------------- pyqtgraph/ptime.py | 39 -- tests/image_testing.py | 10 - 13 files changed, 1 insertion(+), 617 deletions(-) delete mode 100644 pyqtgraph/ordereddict.py delete mode 100644 pyqtgraph/pgcollections.py delete mode 100644 pyqtgraph/ptime.py diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 5378476fd4..6e41bf3170 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -83,13 +83,6 @@ class GraphicsScene(QtWidgets.QGraphicsScene): _addressCache = weakref.WeakValueDictionary() ExportDirectory = None - - @classmethod - def registerObject(cls, obj): - warnings.warn( - "'registerObject' is deprecated and does nothing.", - DeprecationWarning, stacklevel=2 - ) def __init__(self, clickRadius=2, moveDistance=5, parent=None): QtWidgets.QGraphicsScene.__init__(self, parent) diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index 6170a92ba2..ea8bc9f3ca 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -37,14 +37,6 @@ def __init__(self, init=None): def getScale(self): return self._state['scale'] - def getAngle(self): - warnings.warn( - 'SRTTransform.getAngle() is deprecated, use SRTTransform.getRotation() instead' - 'will be removed in 0.13', - DeprecationWarning, stacklevel=2 - ) - return self.getRotation() - def getRotation(self): return self._state['angle'] diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 3349872e97..ead6a1eb48 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -247,9 +247,7 @@ def renamePyc(startDir): # indirect imports known to be used outside of the library from .metaarray import MetaArray -from .ordereddict import OrderedDict from .Point import Point -from .ptime import time from .Qt import isQObjectAlive from .SignalProxy import * from .SRTTransform import SRTTransform diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index eb69cb5379..3160ea5111 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -391,11 +391,6 @@ def __init__(self, pos, color, mapping=CLIP, mode=None, linearize=False, name='' the colors assigned to 0 and 1 for all values below or above this range, respectively. """ self.name = name # storing a name helps identify ColorMaps sampled by Palette - if mode is not None: - warnings.warn( - "'mode' argument is deprecated and does nothing.", - DeprecationWarning, stacklevel=2 - ) if pos is None: order = range(len(color)) self.pos = np.linspace(0.0, 1.0, num=len(color)) diff --git a/pyqtgraph/examples/GraphicsScene.py b/pyqtgraph/examples/GraphicsScene.py index 7b74f757b1..a33965b0e3 100644 --- a/pyqtgraph/examples/GraphicsScene.py +++ b/pyqtgraph/examples/GraphicsScene.py @@ -10,7 +10,6 @@ class Obj(QtWidgets.QGraphicsObject): def __init__(self): QtWidgets.QGraphicsObject.__init__(self) - GraphicsScene.registerObject(self) def paint(self, p, *args): p.setPen(pg.mkPen(200,200,200)) diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py index 9ab841b91a..371073a550 100644 --- a/pyqtgraph/graphicsItems/ColorBarItem.py +++ b/pyqtgraph/graphicsItems/ColorBarItem.py @@ -4,12 +4,12 @@ import numpy as np +from .. import colormap from .. import functions as fn from ..Qt import QtCore from .ImageItem import ImageItem from .LinearRegionItem import LinearRegionItem from .PlotItem import PlotItem -from .. import colormap __all__ = ['ColorBarItem'] @@ -72,13 +72,6 @@ def __init__(self, values=(0,1), width=25, colorMap=None, label=None, Sets the color of movable center region when hovered over. """ super().__init__() - if cmap is not None: - warnings.warn( - "The parameter 'cmap' has been renamed to 'colorMap' for clarity. " - "The old name will no longer be available in any version of PyQtGraph released after July 2022.", - DeprecationWarning, stacklevel=2 - ) - colorMap = cmap self.img_list = [] # list of controlled ImageItems self.values = values self._colorMap = None @@ -193,15 +186,6 @@ def setImageItem(self, img, insert_in=None): insert_in.layout.setColumnFixedWidth(4, 5) # enforce some space to axis on the left self._update_items( update_cmap = True ) - # Maintain compatibility for old name of color bar setting method. - def setCmap(self, cmap): - warnings.warn( - "The method 'setCmap' has been renamed to 'setColorMap' for clarity. " - "The old name will no longer be available in any version of PyQtGraph released after July 2022.", - DeprecationWarning, stacklevel=2 - ) - self.setColorMap(cmap) - def setColorMap(self, colorMap): """ Sets a color map to determine the ColorBarItem's look-up table. The same @@ -220,14 +204,6 @@ def colorMap(self): Returns the assigned ColorMap object. """ return self._colorMap - - @property - def cmap(self): - warnings.warn( - "Direct access to ColorMap.cmap is deprecated and will no longer be available in any " - "version of PyQtGraph released after July 2022. Please use 'ColorMap.colorMap()' instead.", - DeprecationWarning, stacklevel=2) - return self._colorMap def setLevels(self, values=None, low=None, high=None ): """ diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 2228b47a69..e3a8fea00d 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -470,17 +470,6 @@ def setPhasemapMode(self, state): self.updateItems(styleUpdate=False) self.informViewBoundsChanged() - def setPointMode(self, state): - # This does not seem to do anything, but PlotItem still seems to call it. - # warnings.warn( - # 'setPointMode has been deprecated, and has no effect. It will be removed from the library in the first release following April, 2022.', - # DeprecationWarning, stacklevel=2 - # ) - if self.opts['pointMode'] == state: - return - self.opts['pointMode'] = state - self.update() - def setPen(self, *args, **kargs): """ Sets the pen used to draw lines between points. diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 7f1256d035..d511688aba 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -539,7 +539,6 @@ def addItem(self, item, *args, **kargs): item.setFftMode(self.ctrl.fftCheck.isChecked()) item.setDownsampling(*self.downsampleMode()) item.setClipToView(self.clipToViewMode()) - item.setPointMode(self.pointMode()) ## Hide older plots if needed self.updateDecimation() diff --git a/pyqtgraph/icons/__init__.py b/pyqtgraph/icons/__init__.py index 418a0c3241..229237f0ae 100644 --- a/pyqtgraph/icons/__init__.py +++ b/pyqtgraph/icons/__init__.py @@ -59,18 +59,6 @@ def getGraphPixmap(name, size=(20, 20)): return icon.pixmap(*size) -def getPixmap(name, size=(20, 20)): - """Historic `getPixmap` function - - (eg. getPixmap('auto') loads pyqtgraph/icons/auto.png) - """ - warnings.warn( - "'getPixmap' is deprecated and will be removed soon, " - "please use `getGraphPixmap` in the future", - DeprecationWarning, stacklevel=2) - return getGraphPixmap(name, size=size) - - # Note: List all graph icons here ... auto = GraphIcon("auto.png") ctrl = GraphIcon("ctrl.png") diff --git a/pyqtgraph/ordereddict.py b/pyqtgraph/ordereddict.py deleted file mode 100644 index 4c99a631ac..0000000000 --- a/pyqtgraph/ordereddict.py +++ /dev/null @@ -1,12 +0,0 @@ -import collections -import warnings - - -class OrderedDict(collections.OrderedDict): - def __init__(self, *args, **kwds): - warnings.warn( - "OrderedDict is in the standard library for supported versions of Python. Will be removed in 0.13", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwds) diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py deleted file mode 100644 index b88d34f600..0000000000 --- a/pyqtgraph/pgcollections.py +++ /dev/null @@ -1,484 +0,0 @@ -""" -advancedTypes.py - Basic data structures not included with python -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more information. - -Includes: - - OrderedDict - Dictionary which preserves the order of its elements - - BiDict, ReverseDict - Bi-directional dictionaries - - ThreadsafeDict, ThreadsafeList - Self-mutexed data structures -""" - -import warnings - -warnings.warn( - "None of these are used in pyqtgraph. Will be removed in 0.13", - DeprecationWarning, stacklevel=2 -) - -import copy -import threading -from collections import OrderedDict - -try: - from collections.abc import Sequence -except ImportError: - # fallback for python < 3.3 - from collections import Sequence - - -class ReverseDict(dict): - """extends dict so that reverse lookups are possible by requesting the key as a list of length 1: - d = BiDict({'x': 1, 'y': 2}) - d['x'] - 1 - d[[2]] - 'y' - """ - def __init__(self, data=None): - if data is None: - data = {} - self.reverse = {} - for k in data: - self.reverse[data[k]] = k - dict.__init__(self, data) - - def __getitem__(self, item): - if type(item) is list: - return self.reverse[item[0]] - else: - return dict.__getitem__(self, item) - - def __setitem__(self, item, value): - self.reverse[value] = item - dict.__setitem__(self, item, value) - - def __deepcopy__(self, memo): - raise Exception("deepcopy not implemented") - - -class BiDict(dict): - """extends dict so that reverse lookups are possible by adding each reverse combination to the dict. - This only works if all values and keys are unique.""" - def __init__(self, data=None): - if data is None: - data = {} - dict.__init__(self) - for k in data: - self[data[k]] = k - - def __setitem__(self, item, value): - dict.__setitem__(self, item, value) - dict.__setitem__(self, value, item) - - def __deepcopy__(self, memo): - raise Exception("deepcopy not implemented") - -class ThreadsafeDict(dict): - """Extends dict so that getitem, setitem, and contains are all thread-safe. - Also adds lock/unlock functions for extended exclusive operations - Converts all sub-dicts and lists to threadsafe as well. - """ - - def __init__(self, *args, **kwargs): - self.mutex = threading.RLock() - dict.__init__(self, *args, **kwargs) - for k in self: - if type(self[k]) is dict: - self[k] = ThreadsafeDict(self[k]) - - def __getitem__(self, attr): - self.lock() - try: - val = dict.__getitem__(self, attr) - finally: - self.unlock() - return val - - def __setitem__(self, attr, val): - if type(val) is dict: - val = ThreadsafeDict(val) - self.lock() - try: - dict.__setitem__(self, attr, val) - finally: - self.unlock() - - def __contains__(self, attr): - self.lock() - try: - val = dict.__contains__(self, attr) - finally: - self.unlock() - return val - - def __len__(self): - self.lock() - try: - val = dict.__len__(self) - finally: - self.unlock() - return val - - def clear(self): - self.lock() - try: - dict.clear(self) - finally: - self.unlock() - - def lock(self): - self.mutex.acquire() - - def unlock(self): - self.mutex.release() - - def __deepcopy__(self, memo): - raise Exception("deepcopy not implemented") - -class ThreadsafeList(list): - """Extends list so that getitem, setitem, and contains are all thread-safe. - Also adds lock/unlock functions for extended exclusive operations - Converts all sub-lists and dicts to threadsafe as well. - """ - - def __init__(self, *args, **kwargs): - self.mutex = threading.RLock() - list.__init__(self, *args, **kwargs) - for k in self: - self[k] = mkThreadsafe(self[k]) - - def __getitem__(self, attr): - self.lock() - try: - val = list.__getitem__(self, attr) - finally: - self.unlock() - return val - - def __setitem__(self, attr, val): - val = makeThreadsafe(val) - self.lock() - try: - list.__setitem__(self, attr, val) - finally: - self.unlock() - - def __contains__(self, attr): - self.lock() - try: - val = list.__contains__(self, attr) - finally: - self.unlock() - return val - - def __len__(self): - self.lock() - try: - val = list.__len__(self) - finally: - self.unlock() - return val - - def lock(self): - self.mutex.acquire() - - def unlock(self): - self.mutex.release() - - def __deepcopy__(self, memo): - raise Exception("deepcopy not implemented") - - -def makeThreadsafe(obj): - if type(obj) is dict: - return ThreadsafeDict(obj) - elif type(obj) is list: - return ThreadsafeList(obj) - elif type(obj) in [str, int, float, bool, tuple]: - return obj - else: - raise Exception("Not sure how to make object of type %s thread-safe" % str(type(obj))) - - -class Locker(object): - def __init__(self, lock): - self.lock = lock - self.lock.acquire() - def __del__(self): - try: - self.lock.release() - except: - pass - -class CaselessDict(OrderedDict): - """Case-insensitive dict. Values can be set and retrieved using keys of any case. - Note that when iterating, the original case is returned for each key.""" - def __init__(self, *args): - OrderedDict.__init__(self, {}) ## requirement for the empty {} here seems to be a python bug? - self.keyMap = OrderedDict([(k.lower(), k) for k in OrderedDict.keys(self)]) - if len(args) == 0: - return - elif len(args) == 1 and isinstance(args[0], dict): - for k in args[0]: - self[k] = args[0][k] - else: - raise Exception("CaselessDict may only be instantiated with a single dict.") - - #def keys(self): - #return self.keyMap.values() - - def __setitem__(self, key, val): - kl = key.lower() - if kl in self.keyMap: - OrderedDict.__setitem__(self, self.keyMap[kl], val) - else: - OrderedDict.__setitem__(self, key, val) - self.keyMap[kl] = key - - def __getitem__(self, key): - kl = key.lower() - if kl not in self.keyMap: - raise KeyError(key) - return OrderedDict.__getitem__(self, self.keyMap[kl]) - - def __contains__(self, key): - return key.lower() in self.keyMap - - def update(self, d): - for k, v in d.items(): - self[k] = v - - def copy(self): - return CaselessDict(OrderedDict.copy(self)) - - def __delitem__(self, key): - kl = key.lower() - if kl not in self.keyMap: - raise KeyError(key) - OrderedDict.__delitem__(self, self.keyMap[kl]) - del self.keyMap[kl] - - def __deepcopy__(self, memo): - raise Exception("deepcopy not implemented") - - def clear(self): - OrderedDict.clear(self) - self.keyMap.clear() - - - -class ProtectedDict(dict): - """ - A class allowing read-only 'view' of a dict. - The object can be treated like a normal dict, but will never modify the original dict it points to. - Any values accessed from the dict will also be read-only. - """ - def __init__(self, data): - self._data_ = data - - ## List of methods to directly wrap from _data_ - wrapMethods = ['_cmp_', '__contains__', '__eq__', '__format__', '__ge__', '__gt__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'has_key', 'iterkeys', 'keys', ] - - ## List of methods which wrap from _data_ but return protected results - protectMethods = ['__getitem__', '__iter__', 'get', 'items', 'values'] - - ## List of methods to disable - disableMethods = ['__delitem__', '__setitem__', 'clear', 'pop', 'popitem', 'setdefault', 'update'] - - - ## Template methods - def wrapMethod(methodName): - return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) - - def protectMethod(methodName): - return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) - - def error(self, *args, **kargs): - raise Exception("Can not modify read-only list.") - - - ## Directly (and explicitly) wrap some methods from _data_ - ## Many of these methods can not be intercepted using __getattribute__, so they - ## must be implemented explicitly - for methodName in wrapMethods: - locals()[methodName] = wrapMethod(methodName) - - ## Wrap some methods from _data_ with the results converted to protected objects - for methodName in protectMethods: - locals()[methodName] = protectMethod(methodName) - - ## Disable any methods that could change data in the list - for methodName in disableMethods: - locals()[methodName] = error - - - ## Add a few extra methods. - def copy(self): - raise Exception("It is not safe to copy protected dicts! (instead try deepcopy, but be careful.)") - - def itervalues(self): - for v in self._data_.values(): - yield protect(v) - - def iteritems(self): - for k, v in self._data_.items(): - yield (k, protect(v)) - - def deepcopy(self): - return copy.deepcopy(self._data_) - - def __deepcopy__(self, memo): - return copy.deepcopy(self._data_, memo) - - - -class ProtectedList(Sequence): - """ - A class allowing read-only 'view' of a list or dict. - The object can be treated like a normal list, but will never modify the original list it points to. - Any values accessed from the list will also be read-only. - - Note: It would be nice if we could inherit from list or tuple so that isinstance checks would work. - However, doing this causes tuple(obj) to return unprotected results (importantly, this means - unpacking into function arguments will also fail) - """ - def __init__(self, data): - self._data_ = data - #self.__mro__ = (ProtectedList, object) - - ## List of methods to directly wrap from _data_ - wrapMethods = ['__contains__', '__eq__', '__format__', '__ge__', '__gt__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'index'] - - ## List of methods which wrap from _data_ but return protected results - protectMethods = ['__getitem__', '__getslice__', '__mul__', '__reversed__', '__rmul__'] - - ## List of methods to disable - disableMethods = ['__delitem__', '__delslice__', '__iadd__', '__imul__', '__setitem__', '__setslice__', 'append', 'extend', 'insert', 'pop', 'remove', 'reverse', 'sort'] - - - ## Template methods - def wrapMethod(methodName): - return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) - - def protectMethod(methodName): - return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) - - def error(self, *args, **kargs): - raise Exception("Can not modify read-only list.") - - - ## Directly (and explicitly) wrap some methods from _data_ - ## Many of these methods can not be intercepted using __getattribute__, so they - ## must be implemented explicitly - for methodName in wrapMethods: - locals()[methodName] = wrapMethod(methodName) - - ## Wrap some methods from _data_ with the results converted to protected objects - for methodName in protectMethods: - locals()[methodName] = protectMethod(methodName) - - ## Disable any methods that could change data in the list - for methodName in disableMethods: - locals()[methodName] = error - - - ## Add a few extra methods. - def __iter__(self): - for item in self._data_: - yield protect(item) - - - def __add__(self, op): - if isinstance(op, ProtectedList): - return protect(self._data_.__add__(op._data_)) - elif isinstance(op, list): - return protect(self._data_.__add__(op)) - else: - raise TypeError("Argument must be a list.") - - def __radd__(self, op): - if isinstance(op, ProtectedList): - return protect(op._data_.__add__(self._data_)) - elif isinstance(op, list): - return protect(op.__add__(self._data_)) - else: - raise TypeError("Argument must be a list.") - - def deepcopy(self): - return copy.deepcopy(self._data_) - - def __deepcopy__(self, memo): - return copy.deepcopy(self._data_, memo) - - def poop(self): - raise Exception("This is a list. It does not poop.") - - -class ProtectedTuple(Sequence): - """ - A class allowing read-only 'view' of a tuple. - The object can be treated like a normal tuple, but its contents will be returned as protected objects. - - Note: It would be nice if we could inherit from list or tuple so that isinstance checks would work. - However, doing this causes tuple(obj) to return unprotected results (importantly, this means - unpacking into function arguments will also fail) - """ - def __init__(self, data): - self._data_ = data - - ## List of methods to directly wrap from _data_ - wrapMethods = ['__contains__', '__eq__', '__format__', '__ge__', '__getnewargs__', '__gt__', '__hash__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'index'] - - ## List of methods which wrap from _data_ but return protected results - protectMethods = ['__getitem__', '__getslice__', '__iter__', '__add__', '__mul__', '__reversed__', '__rmul__'] - - - ## Template methods - def wrapMethod(methodName): - return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) - - def protectMethod(methodName): - return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) - - - ## Directly (and explicitly) wrap some methods from _data_ - ## Many of these methods can not be intercepted using __getattribute__, so they - ## must be implemented explicitly - for methodName in wrapMethods: - locals()[methodName] = wrapMethod(methodName) - - ## Wrap some methods from _data_ with the results converted to protected objects - for methodName in protectMethods: - locals()[methodName] = protectMethod(methodName) - - - ## Add a few extra methods. - def deepcopy(self): - return copy.deepcopy(self._data_) - - def __deepcopy__(self, memo): - return copy.deepcopy(self._data_, memo) - - - -def protect(obj): - if isinstance(obj, dict): - return ProtectedDict(obj) - elif isinstance(obj, list): - return ProtectedList(obj) - elif isinstance(obj, tuple): - return ProtectedTuple(obj) - else: - return obj - - -if __name__ == '__main__': - d = {'x': 1, 'y': [1,2], 'z': ({'a': 2, 'b': [3,4], 'c': (5,6)}, 1, 2)} - dp = protect(d) - - l = [1, 'x', ['a', 'b'], ('c', 'd'), {'x': 1, 'y': 2}] - lp = protect(l) - - t = (1, 'x', ['a', 'b'], ('c', 'd'), {'x': 1, 'y': 2}) - tp = protect(t) diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py deleted file mode 100644 index a806349b14..0000000000 --- a/pyqtgraph/ptime.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -ptime.py - Precision time function made os-independent (should have been taken care of by python) -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more information. -""" - - -import sys -import warnings -from time import perf_counter as clock -from time import time as system_time - -START_TIME = None -time = None - -def winTime(): - """Return the current time in seconds with high precision (windows version, use Manager.time() to stay platform independent).""" - warnings.warn( - "'pg.time' will be removed from the library in the first release following January, 2022.", - DeprecationWarning, stacklevel=2 - ) - return clock() + START_TIME - -def unixTime(): - """Return the current time in seconds with high precision (unix version, use Manager.time() to stay platform independent).""" - warnings.warn( - "'pg.time' will be removed from the library in the first release following January, 2022.", - DeprecationWarning, stacklevel=2 - ) - return system_time() - - -if sys.platform.startswith('win'): - cstart = clock() ### Required to start the clock in windows - START_TIME = system_time() - cstart - - time = winTime -else: - time = unixTime diff --git a/tests/image_testing.py b/tests/image_testing.py index 3a07e6077c..6b62fea2a6 100644 --- a/tests/image_testing.py +++ b/tests/image_testing.py @@ -387,16 +387,6 @@ def failTest(self): self.lastKey = 'f' -def getTestDataRepo(): - warnings.warn( - "Test data data repo has been merged with the main repo" - "use getTestDataDirectory() instead, this method will be removed" - "in a future version of pyqtgraph", - DeprecationWarning, stacklevel=2 - ) - return getTestDataDirectory() - - def getTestDataDirectory(): dataPath = Path(__file__).absolute().parent / "images" return dataPath.as_posix() From fbb48a3f9f1b217b8e26ca6604b95c681c971110 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Tue, 30 Aug 2022 22:22:40 -0700 Subject: [PATCH 013/306] Have pyqtgraph emit a warning with non-supported Qt version, remove more deprecated things --- pyqtgraph/Qt/__init__.py | 74 +++++++++++++--------------------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/pyqtgraph/Qt/__init__.py b/pyqtgraph/Qt/__init__.py index 744a246fdd..222d604c0d 100644 --- a/pyqtgraph/Qt/__init__.py +++ b/pyqtgraph/Qt/__init__.py @@ -105,7 +105,7 @@ def _loadUiType(uiFile): if pyside2uic is None: pyside2version = tuple(map(int, PySide2.__version__.split("."))) if (5, 14) <= pyside2version < (5, 14, 2, 2): - warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15') + warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15', RuntimeWarning, stacklevel=2) # get class names from ui file import xml.etree.ElementTree as xml @@ -251,47 +251,6 @@ def _copy_attrs(src, dst): raise ValueError("Invalid Qt lib '%s'" % QT_LIB) -# common to PyQt5, PyQt6, PySide2 and PySide6 -if QT_LIB in [PYQT5, PYQT6, PYSIDE2, PYSIDE6]: - # We're using Qt5 which has a different structure so we're going to use a shim to - # recreate the Qt4 structure - - if QT_LIB in [PYQT5, PYSIDE2]: - __QGraphicsItem_scale = QtWidgets.QGraphicsItem.scale - - # Import all QtWidgets objects into QtGui - _fallbacks = dir(QtWidgets) - - def lazyGetattr(name): - if not (name in _fallbacks and name.startswith('Q')): - raise AttributeError(f"module 'QtGui' has no attribute '{name}'") - # This whitelist is attrs which are not shared between PyQt6.QtGui and PyQt5.QtGui, but which can be found on - # one of the QtWidgets. - whitelist = [ - "QAction", - "QActionGroup", - "QFileSystemModel", - "QPagedPaintDevice", - "QPaintEvent", - "QShortcut", - "QUndoCommand", - "QUndoGroup", - "QUndoStack", - ] - if name not in whitelist: - warnings.warn( - "Accessing pyqtgraph.QtWidgets through QtGui is deprecated and will be removed sometime" - f" after May 2022. Use QtWidgets.{name} instead.", - DeprecationWarning, stacklevel=2 - ) - attr = getattr(QtWidgets, name) - setattr(QtGui, name, attr) - return attr - - QtGui.__getattr__ = lazyGetattr - - QtWidgets.QApplication.setGraphicsSystem = None - if QT_LIB in [PYQT6, PYSIDE6]: # We're using Qt6 which has a different structure so we're going to use a shim to @@ -299,6 +258,20 @@ def lazyGetattr(name): if not isinstance(QtOpenGLWidgets, FailedImport): QtWidgets.QOpenGLWidget = QtOpenGLWidgets.QOpenGLWidget +else: + # Shim Qt5 namespace to match Qt6 + module_whitelist = [ + "QAction", + "QActionGroup", + "QFileSystemModel", + "QShortcut", + "QUndoCommand", + "QUndoGroup", + "QUndoStack", + ] + for module in module_whitelist: + attr = getattr(QtWidgets, module) + setattr(QtGui, module, attr) # Common to PySide2 and PySide6 @@ -348,17 +321,16 @@ def isQObjectAlive(obj): from . import internals -# USE_XXX variables are deprecated -USE_PYSIDE = QT_LIB == PYSIDE -USE_PYQT4 = QT_LIB == PYQT4 -USE_PYQT5 = QT_LIB == PYQT5 - -## Make sure we have Qt >= 5.12 -versionReq = [5, 12] +# Alert user if using Qt < 5.15, but do not raise exception +versionReq = [5, 15] 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)) + warnings.warn( + f"PyQtGraph supports Qt version >= {versionReq[0]}.{versionReq[1]}," + f" but {QtVersion} detected.", + RuntimeWarning, + stacklevel=2 + ) App = QtWidgets.QApplication # subclassing QApplication causes segfaults on PySide{2, 6} / Python 3.8.7+ From 4365d4f4cf3f1e64aa45027f05e410becf77cda0 Mon Sep 17 00:00:00 2001 From: ntjess Date: Thu, 1 Sep 2022 00:00:47 -0400 Subject: [PATCH 014/306] Bugfix: Handle example search failure due to bad regex (#2121) * Bugfix: Handle example search failure due to bad regex * Implement recommendations for easier color-setting logic * Handle switching to title search after invalid regex --- pyqtgraph/examples/ExampleApp.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/examples/ExampleApp.py b/pyqtgraph/examples/ExampleApp.py index badd703311..1e6e393e63 100644 --- a/pyqtgraph/examples/ExampleApp.py +++ b/pyqtgraph/examples/ExampleApp.py @@ -325,6 +325,9 @@ def onComboChanged(searchType): if self.curListener is not None: self.curListener.disconnect() self.curListener = textFil.textChanged + # In case the regex was invalid before switching to title search, + # ensure the "invalid" color is reset + self.ui.exampleFilter.setStyleSheet('') if searchType == 'Content Search': self.curListener.connect(self.filterByContent) else: @@ -367,7 +370,21 @@ def filterByTitle(self, text): self.hl.setDocument(self.ui.codeView.document()) def filterByContent(self, text=None): - # Don't filter very short strings + # If the new text isn't valid regex, fail early and highlight the search filter red to indicate a problem + # to the user + validRegex = True + try: + re.compile(text) + self.ui.exampleFilter.setStyleSheet('') + except re.error: + colors = DarkThemeColors if app.property('darkMode') else LightThemeColors + errorColor = pg.mkColor(colors.Red) + validRegex = False + errorColor.setAlpha(100) + # Tuple prints nicely :) + self.ui.exampleFilter.setStyleSheet(f'background: rgba{errorColor.getRgb()}') + if not validRegex: + return checkDict = unnestedDict(utils.examples_) self.hl.searchText = text # Need to reapply to current document From 9e775a8f9515b471b0f2273cb4d5cb1c8bb159da Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 31 Aug 2022 21:08:23 -0700 Subject: [PATCH 015/306] Remove unused imports --- pyqtgraph/GraphicsScene/GraphicsScene.py | 1 - pyqtgraph/SRTTransform.py | 1 - pyqtgraph/colormap.py | 1 - pyqtgraph/examples/GraphicsScene.py | 1 - pyqtgraph/flowchart/Node.py | 1 - pyqtgraph/graphicsItems/AxisItem.py | 1 - pyqtgraph/graphicsItems/ColorBarItem.py | 1 - pyqtgraph/graphicsItems/GraphicsItem.py | 1 - pyqtgraph/graphicsItems/ROI.py | 1 - pyqtgraph/graphicsItems/ScatterPlotItem.py | 1 - pyqtgraph/graphicsItems/TargetItem.py | 3 +-- pyqtgraph/opengl/GLViewWidget.py | 1 - pyqtgraph/parametertree/Parameter.py | 1 - tests/image_testing.py | 1 - 14 files changed, 1 insertion(+), 15 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 6e41bf3170..ed8bd6e6e3 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,4 +1,3 @@ -import warnings import weakref from time import perf_counter, perf_counter_ns diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index ea8bc9f3ca..07e3c14cde 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -1,4 +1,3 @@ -import warnings from math import atan2, degrees import numpy as np diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 3160ea5111..68f7185ff1 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -1,4 +1,3 @@ -import warnings from collections.abc import Callable, Sequence from os import listdir, path diff --git a/pyqtgraph/examples/GraphicsScene.py b/pyqtgraph/examples/GraphicsScene.py index a33965b0e3..17330f8ae8 100644 --- a/pyqtgraph/examples/GraphicsScene.py +++ b/pyqtgraph/examples/GraphicsScene.py @@ -1,5 +1,4 @@ import pyqtgraph as pg -from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.Qt import QtCore, QtWidgets app = pg.mkQApp("GraphicsScene Example") diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index 00503da0fe..7fdfbd1626 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -1,7 +1,6 @@ __all__ = ["Node", "NodeGraphicsItem"] import sys -import warnings from collections import OrderedDict from .. import functions as fn diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index a29d622a3b..357c2bcc9a 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1,4 +1,3 @@ -import warnings import weakref from math import ceil, floor, isfinite, log, log10 diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py index 371073a550..2e31501ebd 100644 --- a/pyqtgraph/graphicsItems/ColorBarItem.py +++ b/pyqtgraph/graphicsItems/ColorBarItem.py @@ -1,5 +1,4 @@ import math -import warnings import weakref import numpy as np diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index c81987adb3..af3478ca21 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -1,5 +1,4 @@ import operator -import warnings import weakref from collections import OrderedDict from functools import reduce diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 36c4c7f21c..53ab90806e 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -12,7 +12,6 @@ """ import sys -import warnings from math import atan2, cos, degrees, hypot, sin import numpy as np diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 2436f3e4f8..fd94132f00 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -1,6 +1,5 @@ import itertools import math -import warnings import weakref from collections import OrderedDict diff --git a/pyqtgraph/graphicsItems/TargetItem.py b/pyqtgraph/graphicsItems/TargetItem.py index 010bdbb581..d1e6eeeaaa 100644 --- a/pyqtgraph/graphicsItems/TargetItem.py +++ b/pyqtgraph/graphicsItems/TargetItem.py @@ -1,12 +1,11 @@ import string -import warnings from math import atan2 from .. import functions as fn from ..Point import Point from ..Qt import QtCore, QtGui from .GraphicsObject import GraphicsObject -from .ScatterPlotItem import Symbols, makeCrosshair +from .ScatterPlotItem import Symbols from .TextItem import TextItem from .UIGraphicsItem import UIGraphicsItem from .ViewBox import ViewBox diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 56cd90e6bc..2a078be5c8 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -1,6 +1,5 @@ from OpenGL.GL import * # noqa import OpenGL.GL.framebufferobjects as glfbo # noqa -import warnings from math import cos, radians, sin, tan import numpy as np diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 28e0dcffbd..6b7581780e 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -1,5 +1,4 @@ import re -import warnings import weakref from collections import OrderedDict diff --git a/tests/image_testing.py b/tests/image_testing.py index 6b62fea2a6..2db9b4b67f 100644 --- a/tests/image_testing.py +++ b/tests/image_testing.py @@ -20,7 +20,6 @@ import os import sys import time -import warnings from pathlib import Path import numpy as np From a097845c79b7b3ac0530d2fc4bbab69f63c1ca7b Mon Sep 17 00:00:00 2001 From: jonmatthis Date: Thu, 1 Sep 2022 00:46:41 -0400 Subject: [PATCH 016/306] Added Jupyter console widget and Example (#2353) * modied git ignore * Added `JupyterConsoleWidget` (and associated `example`) * add file to git * black format files * swiched to 'QTInProcessKernelManager' so I could add 'push_variables' method Also added method to input 'namespace' on init (like old Console) * remove unused 'time' import * remove 'JupyterConsoleWidget' and update Examples list to point at `jupyter_console_example.py` * add doc string and format with `black` * dare I engage in such hubris as to list the `jupyter_console_example.py` example as `recommended=True`? * pretty up the formatting a bit * add newline * remove old file * newline AFTER `.venv` lol * specify `ImportError`or `NameError` * fix exception, use - `except (ImportErroror,NameError):` * Skip testing console example if qtconsole is not available Co-authored-by: Jon Co-authored-by: Ogi Moore --- .gitignore | 2 + pyqtgraph/examples/jupyter_console_example.py | 97 +++++++++++++++++++ pyqtgraph/examples/test_examples.py | 4 + pyqtgraph/examples/utils.py | 1 + 4 files changed, 104 insertions(+) create mode 100644 pyqtgraph/examples/jupyter_console_example.py diff --git a/.gitignore b/.gitignore index e3bd193185..f8de805b91 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,5 @@ asv.conf.json # jupyter notebooks .ipynb_checkpoints + +.venv diff --git a/pyqtgraph/examples/jupyter_console_example.py b/pyqtgraph/examples/jupyter_console_example.py new file mode 100644 index 0000000000..3fe5639a6e --- /dev/null +++ b/pyqtgraph/examples/jupyter_console_example.py @@ -0,0 +1,97 @@ +""" +This example show how to create a Rich Jupyter Widget, and places it in a MainWindow alongside a PlotWidget. + +The widgets are implemented as `Docks` so they may be moved around within the Main Window + +The `__main__` function shows an example that inputs the commands to plot simple `sine` and cosine` waves, equivalent to creating such plots by entering the commands manually in the console + +Also shows the use of `whos`, which returns a list of the variables defined within the `ipython` kernel + +This method for creating a Jupyter console is based on the example(s) here: +https://github.com/jupyter/qtconsole/tree/b4e08f763ef1334d3560d8dac1d7f9095859545a/examples +especially- +https://github.com/jupyter/qtconsole/blob/b4e08f763ef1334d3560d8dac1d7f9095859545a/examples/embed_qtconsole.py#L19 + +""" + + +import numpy as np +import pyqtgraph as pg +from pyqtgraph.Qt import QtWidgets +from pyqtgraph.dockarea.Dock import Dock +from pyqtgraph.dockarea.DockArea import DockArea + +try: + from qtconsole import inprocess +except (ImportError, NameError): + print( + "The example in `jupyter_console_example.py` requires `qtconsole` to run. Install with `pip install qtconsole` or equivalent." + ) + + +class JupyterConsoleWidget(inprocess.QtInProcessRichJupyterWidget): + def __init__(self): + super().__init__() + + self.kernel_manager = inprocess.QtInProcessKernelManager() + self.kernel_manager.start_kernel() + self.kernel_client = self.kernel_manager.client() + self.kernel_client.start_channels() + + def shutdown_kernel(self): + self.kernel_client.stop_channels() + self.kernel_manager.shutdown_kernel() + + +class MainWindow(QtWidgets.QMainWindow): + def __init__(self, dark_mode=True): + super().__init__() + central_dock_area = DockArea() + + # create plot widget (and dock) + self.plot_widget = pg.PlotWidget() + plot_dock = Dock(name="Plot Widget Dock", closable=True) + plot_dock.addWidget(self.plot_widget) + central_dock_area.addDock(plot_dock) + + # create jupyter console widget (and dock) + self.jupyter_console_widget = JupyterConsoleWidget() + jupyter_console_dock = Dock("Jupyter Console Dock") + jupyter_console_dock.addWidget(self.jupyter_console_widget) + central_dock_area.addDock(jupyter_console_dock) + self.setCentralWidget(central_dock_area) + + app = QtWidgets.QApplication.instance() + app.aboutToQuit.connect(self.jupyter_console_widget.shutdown_kernel) + + kernel = self.jupyter_console_widget.kernel_manager.kernel + kernel.shell.push(dict(np=np, pw=self.plot_widget)) + + # set dark mode + if dark_mode: + # Set Dark bg color via this relatively roundabout method + self.jupyter_console_widget.set_default_style( + "linux" + ) + +if __name__ == "__main__": + pg.mkQApp() + main = MainWindow(dark_mode=True) + main.show() + main.jupyter_console_widget.execute('print("hello world :D ")') + + # plot a sine/cosine waves by printing to console + # this is equivalent to typing the commands into the console manually + main.jupyter_console_widget.execute("x = np.arange(0, 3 * np.pi, .1)") + main.jupyter_console_widget.execute("pw.plotItem.plot(np.sin(x), pen='r')") + main.jupyter_console_widget.execute( + "pw.plotItem.plot(np.cos(x),\ + pen='cyan',\ + symbol='o',\ + symbolPen='m',\ + symbolBrush=(0,0,255))" + ) + main.jupyter_console_widget.execute("whos") + main.jupyter_console_widget.execute("") + + pg.exec() diff --git a/pyqtgraph/examples/test_examples.py b/pyqtgraph/examples/test_examples.py index ad0c029705..53bbefdc70 100644 --- a/pyqtgraph/examples/test_examples.py +++ b/pyqtgraph/examples/test_examples.py @@ -70,6 +70,10 @@ def buildFileList(examples, files=None): False, reason="Example requires user interaction" ), + "jupyter_console_example.py": exceptionCondition( + importlib.util.find_spec("qtconsole") is not None, + reason="No need to test with qtconsole not being installed" + ), "RemoteSpeedTest.py": exceptionCondition( False, reason="Test is being problematic on CI machines" diff --git a/pyqtgraph/examples/utils.py b/pyqtgraph/examples/utils.py index 3badf69e48..2fa4e3ed6c 100644 --- a/pyqtgraph/examples/utils.py +++ b/pyqtgraph/examples/utils.py @@ -16,6 +16,7 @@ ('Matrix Display', 'MatrixDisplayExample.py'), ('ViewBox Features', Namespace(filename='ViewBoxFeatures.py', recommended=True)), ('Dock widgets', 'dockarea.py'), + ('Rich Jupyter Console', Namespace(filename='jupyter_console_example.py', recommended=True)), ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), ('Beeswarm plot', 'beeswarm.py'), From 18e1721f3fa741c5912897b294812a9c0b7babde Mon Sep 17 00:00:00 2001 From: bbc131 <36670201+bbc131@users.noreply.github.com> Date: Fri, 2 Sep 2022 07:08:11 +0200 Subject: [PATCH 017/306] Add style option 'hideOverlappingLabels' (#2385) * Add style option 'hideOverlappingLabels' * With 'hideOverlappingLabels==False' nothing changes. Tick labels might be drawn overlapping with tick labels from neighboring plots or overlap with other items or might be simply clipped. * With 'hideOverlappingLabels==True', only tick labels, which are fully contained within the items geometry rectangle are drawn. * Fix docstring --- pyqtgraph/graphicsItems/AxisItem.py | 95 ++++++++++++++++------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 357c2bcc9a..88bac3570f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -59,6 +59,7 @@ def __init__(self, orientation, pen=None, textPen=None, tickPen = None, linkView 'tickTextHeight': 18, 'autoExpandTextSpace': True, ## automatically expand text space if needed 'autoReduceTextSpace': True, + 'hideOverlappingLabels': True, 'tickFont': None, 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick 'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally. @@ -125,46 +126,49 @@ def setStyle(self, **kwds): """ Set various style options. - =================== ======================================================= + ===================== ======================================================= Keyword Arguments: - tickLength (int) The maximum length of ticks in pixels. - Positive values point toward the text; negative - values point away. - tickTextOffset (int) reserved spacing between text and axis in px - tickTextWidth (int) Horizontal space reserved for tick text in px - tickTextHeight (int) Vertical space reserved for tick text in px - autoExpandTextSpace (bool) Automatically expand text space if the tick - strings become too long. - autoReduceTextSpace (bool) Automatically shrink the axis if necessary - tickFont (QFont or None) Determines the font used for tick - values. Use None for the default font. - stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis - line is drawn only as far as the last tick. - Otherwise, the line is drawn to the edge of the - AxisItem boundary. - textFillLimits (list of (tick #, % fill) tuples). This structure - determines how the AxisItem decides how many ticks - should have text appear next to them. Each tuple in - the list specifies what fraction of the axis length - may be occupied by text, given the number of ticks - that already have text displayed. For example:: - - [(0, 0.8), # Never fill more than 80% of the axis - (2, 0.6), # If we already have 2 ticks with text, - # fill no more than 60% of the axis - (4, 0.4), # If we already have 4 ticks with text, - # fill no more than 40% of the axis - (6, 0.2)] # If we already have 6 ticks with text, - # fill no more than 20% of the axis - - showValues (bool) indicates whether text is displayed adjacent - to ticks. - tickAlpha (float or int or None) If None, pyqtgraph will draw the - ticks with the alpha it deems appropriate. Otherwise, - the alpha will be fixed at the value passed. With int, - accepted values are [0..255]. With value of type - float, accepted values are from [0..1]. - =================== ======================================================= + tickLength (int) The maximum length of ticks in pixels. + Positive values point toward the text; negative + values point away. + tickTextOffset (int) reserved spacing between text and axis in px + tickTextWidth (int) Horizontal space reserved for tick text in px + tickTextHeight (int) Vertical space reserved for tick text in px + autoExpandTextSpace (bool) Automatically expand text space if the tick + strings become too long. + autoReduceTextSpace (bool) Automatically shrink the axis if necessary + hideOverlappingLabels (bool) Hide tick labels which overlap the AxisItems' + geometry rectangle. If False, labels might be drawn + overlapping with tick labels from neighboring plots. + tickFont (QFont or None) Determines the font used for tick + values. Use None for the default font. + stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis + line is drawn only as far as the last tick. + Otherwise, the line is drawn to the edge of the + AxisItem boundary. + textFillLimits (list of (tick #, % fill) tuples). This structure + determines how the AxisItem decides how many ticks + should have text appear next to them. Each tuple in + the list specifies what fraction of the axis length + may be occupied by text, given the number of ticks + that already have text displayed. For example:: + + [(0, 0.8), # Never fill more than 80% of the axis + (2, 0.6), # If we already have 2 ticks with text, + # fill no more than 60% of the axis + (4, 0.4), # If we already have 4 ticks with text, + # fill no more than 40% of the axis + (6, 0.2)] # If we already have 6 ticks with text, + # fill no more than 20% of the axis + + showValues (bool) indicates whether text is displayed adjacent + to ticks. + tickAlpha (float or int or None) If None, pyqtgraph will draw the + ticks with the alpha it deems appropriate. Otherwise, + the alpha will be fixed at the value passed. With int, + accepted values are [0..255]. With value of type + float, accepted values are from [0..1]. + ===================== ======================================================= Added in version 0.9.9 """ @@ -620,6 +624,7 @@ def linkedViewChanged(self, view, newRange=None): self.setRange(*newRange) def boundingRect(self): + m = 0 if self.style['hideOverlappingLabels'] else 15 linkedView = self.linkedView() if linkedView is None or self.grid is False: rect = self.mapRectFromParent(self.geometry()) @@ -627,13 +632,13 @@ def boundingRect(self): ## also extend to account for text that flows past the edges tl = self.style['tickLength'] if self.orientation == 'left': - rect = rect.adjusted(0, -15, -min(0,tl), 15) + rect = rect.adjusted(0, -m, -min(0,tl), m) elif self.orientation == 'right': - rect = rect.adjusted(min(0,tl), -15, 0, 15) + rect = rect.adjusted(min(0,tl), -m, 0, m) elif self.orientation == 'top': rect = rect.adjusted(-15, 0, 15, -min(0,tl)) elif self.orientation == 'bottom': - rect = rect.adjusted(-15, min(0,tl), 15, 0) + rect = rect.adjusted(-m, min(0,tl), m, 0) return rect else: return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) @@ -1156,6 +1161,7 @@ def generateDrawSpecs(self, p): #self.textHeight = height offset = max(0,self.style['tickLength']) + textOffset + rect = QtCore.QRectF() if self.orientation == 'left': alignFlags = QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignVCenter rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) @@ -1172,6 +1178,11 @@ def generateDrawSpecs(self, p): textFlags = alignFlags | QtCore.Qt.TextFlag.TextDontClip #p.setPen(self.pen()) #p.drawText(rect, textFlags, vstr) + + br = self.boundingRect() + if not br.contains(rect): + continue + textSpecs.append((rect, textFlags, vstr)) profiler('compute text') From 13142f8532320250a1d67fd9bbec44027f990d5d Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 2 Sep 2022 09:17:46 -0400 Subject: [PATCH 018/306] Clean up `InteractiveFunction` wrapper assignments --- pyqtgraph/parametertree/interactive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 64cc33febb..f87aaaf7c9 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -61,9 +61,9 @@ def __init__(self, function, *, closures=None, **extra): self.parametersNeedRunKwargs = False self.parameterCache = {} - self.__name__ = function.__name__ - self.__doc__ = function.__doc__ - functools.update_wrapper(self, function) + # No need for wrapper __dict__ to function as function.__dict__, since + # Only __doc__, __name__, etc. attributes are required + functools.update_wrapper(self, function, updated=()) def __call__(self, **kwargs): """ From 642b9dbf1f3b28f22234644e803ed7fd231a7c62 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 2 Sep 2022 09:24:50 -0400 Subject: [PATCH 019/306] `Interactor.title` -> `InteractiveFunction.titleFormat` The name `title` implies a string rather than a formatting callable. The intentionality behind `titleFormat` is much more explicit and doesn't clash with existing conventions for `Parameter.title()` --- .../parametertree/interactiveparameters.rst | 4 +-- pyqtgraph/examples/InteractiveParameter.py | 2 +- pyqtgraph/examples/ScatterPlotSpeedTest.py | 2 +- pyqtgraph/parametertree/interactive.py | 30 +++++++++---------- tests/parametertree/test_Parameter.py | 4 +-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/source/parametertree/interactiveparameters.rst b/doc/source/parametertree/interactiveparameters.rst index 9a5d2bda77..1d010117df 100644 --- a/doc/source/parametertree/interactiveparameters.rst +++ b/doc/source/parametertree/interactiveparameters.rst @@ -340,7 +340,7 @@ Title Formatting ---------------- If functions should have formatted titles, specify this in the -``title`` parameter: +``titleFormat`` parameter: .. code:: python @@ -351,7 +351,7 @@ If functions should have formatted titles, specify this in the return name.replace('_', ' ').title() # The title in the parameter tree will be "My Snake Case Function" - params = interact(my_snake_case_function, title=titleFormat) + params = interact(my_snake_case_function, titleFormat=titleFormat) Using ``InteractiveFunction`` ----------------------------- diff --git a/pyqtgraph/examples/InteractiveParameter.py b/pyqtgraph/examples/InteractiveParameter.py index d5caf70f90..58f55c8916 100644 --- a/pyqtgraph/examples/InteractiveParameter.py +++ b/pyqtgraph/examples/InteractiveParameter.py @@ -82,7 +82,7 @@ def accessVarInDifferentScope(x, y=10): interactor(func_interactive) -with interactor.optsContext(title=str.upper): +with interactor.optsContext(titleFormat=str.upper): @interactor.decorate() @printResult diff --git a/pyqtgraph/examples/ScatterPlotSpeedTest.py b/pyqtgraph/examples/ScatterPlotSpeedTest.py index 8b11ff4916..a6063a433f 100644 --- a/pyqtgraph/examples/ScatterPlotSpeedTest.py +++ b/pyqtgraph/examples/ScatterPlotSpeedTest.py @@ -42,7 +42,7 @@ def fmt(name): return translate("ScatterPlot", name.title().strip() + ": ") -interactor = ptree.Interactor(title=fmt, nest=False, parent=param) +interactor = ptree.Interactor(titleFormat=fmt, nest=False, parent=param) @interactor.decorate( diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index f87aaaf7c9..8151cf8d43 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -199,12 +199,12 @@ def __repr__(self): class Interactor: runOpts = RunOpts.ON_CHANGED parent = None - title = None + titleFormat = None nest = True existOk = True runActionTemplate = dict(type="action", defaultName="Run") - _optNames = ["runOpts", "parent", "title", "nest", "existOk", "runActionTemplate"] + _optNames = ["runOpts", "parent", "titleFormat", "nest", "existOk", "runActionTemplate"] def __init__(self, **kwargs): """ @@ -267,7 +267,7 @@ def interact( ignores=None, runOpts=RunOpts.PARAM_UNSET, parent=RunOpts.PARAM_UNSET, - title=RunOpts.PARAM_UNSET, + titleFormat=RunOpts.PARAM_UNSET, nest=RunOpts.PARAM_UNSET, existOk=RunOpts.PARAM_UNSET, **overrides, @@ -295,8 +295,8 @@ def interact( parent: GroupParameter Parent in which to add argument Parameters. If *None*, a new group parameter is created. - title: str or Callable - Title of the group sub-parameter if one must be created (see ``nest`` + titleFormat: str or Callable + title of the group sub-parameter if one must be created (see ``nest`` behavior). If a function is supplied, it must be of the form (str) -> str and will be passed the function name as an input nest: bool @@ -324,7 +324,7 @@ def interact( } oldOpts = self.setOpts(**opts) # Delete explicitly since correct values are now ``self`` attributes - del runOpts, title, nest, existOk, parent + del runOpts, titleFormat, nest, existOk, parent funcDict = self.functionToParameterDict(function, **overrides) children = funcDict.pop("children", []) # type: list[dict] @@ -404,20 +404,20 @@ def decorator(function): def _nameToTitle(self, name, forwardStrTitle=False): """ - Converts a function name to a title based on ``self.title``. + Converts a function name to a title based on ``self.titleFormat``. Parameters ---------- name: str Name of the function forwardStrTitle: bool - If ``self.title`` is a string and ``forwardStrTitle`` is True, - ``self.title`` will be used as the title. Otherwise, if ``self.title`` is - *None*, the name will be returned unchanged. Finally, if ``self.title`` is - a callable, it will be called with the name as an input and the output will - be returned + If ``self.titleFormat`` is a string and ``forwardStrTitle`` is True, + ``self.titleFormat`` will be used as the title. Otherwise, if + ``self.titleFormat`` is *None*, the name will be returned unchanged. + Finally, if ``self.titleFormat`` is a callable, it will be called with + the name as an input and the output will be returned """ - titleFormat = self.title + titleFormat = self.titleFormat isString = isinstance(titleFormat, str) if titleFormat is None or (isString and not forwardStrTitle): return name @@ -485,7 +485,7 @@ def functionToParameterDict(self, function, **overrides): """ children = [] out = dict(name=function.__name__, type="group", children=children) - if self.title is not None: + if self.titleFormat is not None: out["title"] = self._nameToTitle(function.__name__, forwardStrTitle=True) funcParams = inspect.signature(function).parameters @@ -559,7 +559,7 @@ def createFunctionParameter(self, name, signatureParameter, overridesInfo): pgDict.setdefault("value", RunOpts.PARAM_UNSET) # Anywhere a title is specified should take precedence over the default factory - if self.title is not None: + if self.titleFormat is not None: pgDict.setdefault("title", self._nameToTitle(name)) pgDict.setdefault("type", type(pgDict["value"]).__name__) return pgDict diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 28ac14d09a..3c48fa24e4 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -116,13 +116,13 @@ def a(x, y=5): host.child("y").sigValueChanging.emit(host.child("y"), 100) assert value == (10, 100) - with interactor.optsContext(title=str.upper): + with interactor.optsContext(titleFormat=str.upper): host = interactor(a, x={"title": "different", "value": 5}) titles = [p.title() for p in host] for ch in "different", "Y": assert ch in titles - with interactor.optsContext(title="Group only"): + with interactor.optsContext(titleFormat="Group only"): host = interactor(a, x=1) assert host.title() == "Group only" assert [p.title() is None for p in host] From 63e0f7d653d9b177284a51aba292ae4901207b63 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 3 Sep 2022 12:31:13 -0700 Subject: [PATCH 020/306] QFileSystemModel was incorrectly placed in the QtWidgets module --- pyqtgraph/Qt/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyqtgraph/Qt/__init__.py b/pyqtgraph/Qt/__init__.py index 222d604c0d..65f84c98af 100644 --- a/pyqtgraph/Qt/__init__.py +++ b/pyqtgraph/Qt/__init__.py @@ -258,6 +258,12 @@ def _copy_attrs(src, dst): if not isinstance(QtOpenGLWidgets, FailedImport): QtWidgets.QOpenGLWidget = QtOpenGLWidgets.QOpenGLWidget + + # PySide6 incorrectly placed QFileSystemModel inside QtWidgets + if QT_LIB == PYSIDE6 and hasattr(QtWidgets, 'QFileSystemModel'): + module = getattr(QtWidgets, "QFileSystemModel") + setattr(QtGui, "QFileSystemModel", module) + else: # Shim Qt5 namespace to match Qt6 module_whitelist = [ From ab5ec85a4164fa350a6640810acc6716f73d631f Mon Sep 17 00:00:00 2001 From: bbc131 <36670201+bbc131@users.noreply.github.com> Date: Sun, 4 Sep 2022 05:00:47 +0200 Subject: [PATCH 021/306] Fix GraphicsScene.itemsNearEvent and setClickRadius (#2383) * Fix GraphicsScene.itemsNearEvent and setClickRadius * Correctly consider the click radius when mapping position to items * Remove workaround for InfiniteLine * Adjust test * Try to fix test_InfiniteLine --- pyqtgraph/GraphicsScene/GraphicsScene.py | 99 +++++++++++++----------- pyqtgraph/graphicsItems/InfiniteLine.py | 5 +- tests/graphicsItems/test_InfiniteLine.py | 4 +- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index ed8bd6e6e3..3c9e30e6ee 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -388,66 +388,77 @@ def removeItem(self, item): ret = QtWidgets.QGraphicsScene.removeItem(self, item) self.sigItemRemoved.emit(item) return ret - - def itemsNearEvent(self, event, selMode=QtCore.Qt.ItemSelectionMode.IntersectsItemShape, sortOrder=QtCore.Qt.SortOrder.DescendingOrder, hoverable=False): + + def itemsNearEvent( + self, + event, + selMode=QtCore.Qt.ItemSelectionMode.IntersectsItemShape, + sortOrder=QtCore.Qt.SortOrder.DescendingOrder, + hoverable=False, + ): """ Return an iterator that iterates first through the items that directly intersect point (in Z order) followed by any other items that are within the scene's click radius. """ - #tr = self.getViewWidget(event.widget()).transform() view = self.views()[0] tr = view.viewportTransform() - - if hasattr(event, 'buttonDownScenePos'): + + if hasattr(event, "buttonDownScenePos"): point = event.buttonDownScenePos() else: point = event.scenePos() - items = self.items(point, selMode, sortOrder, tr) - - ## remove items whose shape does not contain point (scene.items() apparently sucks at this) - items2 = [] - for item in items: - if hoverable and not hasattr(item, 'hoverEvent'): - continue - if item.scene() is not self: - continue - shape = item.shape() # Note: default shape() returns boundingRect() - if shape is None: - continue - if shape.contains(item.mapFromScene(point)): - items2.append(item) - ## Sort by descending Z-order (don't trust scene.itms() to do this either) ## use 'absolute' z value, which is the sum of all item/parent ZValues def absZValue(item): if item is None: return 0 return item.zValue() + absZValue(item.parentItem()) - - items2.sort(key=absZValue, reverse=True) - - return items2 - - #seen = set() - #r = self._clickRadius - #rect = view.mapToScene(QtCore.QRect(0, 0, 2*r, 2*r)).boundingRect() - #w = rect.width() - #h = rect.height() - #rgn = QtCore.QRectF(point.x()-w, point.y()-h, 2*w, 2*h) - #self.searchRect.setRect(rgn) - - #for item in items: - ##seen.add(item) - - #shape = item.mapToScene(item.shape()) - #if not shape.contains(point): - #continue - #yield item - #for item in self.items(rgn, selMode, sortOrder, tr): - ##if item not in seen: - #yield item - + + ## Get items, which directly are at the given point (sorted by z-value) + items_at_point = self.items(point, selMode, sortOrder, tr) + items_at_point.sort(key=absZValue, reverse=True) + + ## Get items, which are within the click radius around the given point (sorted by z-value) + r = self._clickRadius + items_within_radius = [] + rgn = None + if r > 0.0: + rect = view.mapToScene(QtCore.QRect(0, 0, 2 * r, 2 * r)).boundingRect() + w = rect.width() + h = rect.height() + rgn = QtCore.QRectF(point.x() - w / 2, point.y() - h / 2, w, h) + items_within_radius = self.items(rgn, selMode, sortOrder, tr) + items_within_radius.sort(key=absZValue, reverse=True) + # Remove items, which are already in the other list + for item in items_at_point: + if item in items_within_radius: + items_within_radius.remove(item) + + ## Put both groups of items together, but in the correct order + ## The items directly at the given point shall have higher priority + all_items = items_at_point + items_within_radius + + ## Remove items, which we don't want, due to several reasons + selected_items = [] + for item in all_items: + if hoverable and not hasattr(item, "hoverEvent"): + continue + if item.scene() is not self: + continue + shape = item.shape() # Note: default shape() returns boundingRect() + if shape is None: + continue + # Remove items whose shape does not contain point or region + # (scene.items() apparently sucks at this) + if ( + rgn is not None + and shape.intersects(item.mapFromScene(rgn).boundingRect()) + ) or shape.contains(item.mapFromScene(point)): + selected_items.append(item) + + return selected_items + def getViewWidget(self): return self.views()[0] diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 75098c44cb..ba543ea328 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -301,14 +301,11 @@ def _computeBoundingRect(self): if vr is None: return QtCore.QRectF() - ## add a 4-pixel radius around the line for mouse interaction. - px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line if px is None: px = 0 pw = max(self.pen.width() / 2, self.hoverPen.width() / 2) - w = max(4, self._maxMarkerSize + pw) + 1 - w = w * px + w = (self._maxMarkerSize + pw + 1) * px br = QtCore.QRectF(vr) br.setBottom(-w) br.setTop(w) diff --git a/tests/graphicsItems/test_InfiniteLine.py b/tests/graphicsItems/test_InfiniteLine.py index 584552e6e4..991dedad53 100644 --- a/tests/graphicsItems/test_InfiniteLine.py +++ b/tests/graphicsItems/test_InfiniteLine.py @@ -46,8 +46,8 @@ def test_InfiniteLine(): pos = oline.mapToScene(pg.Point(2, 0)) assert br.containsPoint(pos, QtCore.Qt.FillRule.OddEvenFill) px = pg.Point(-0.5, -1.0 / 3**0.5) - assert br.containsPoint(pos + 5 * px, QtCore.Qt.FillRule.OddEvenFill) - assert not br.containsPoint(pos + 7 * px, QtCore.Qt.FillRule.OddEvenFill) + assert br.containsPoint(pos + 1 * px, QtCore.Qt.FillRule.OddEvenFill) + assert not br.containsPoint(pos + 3 * px, QtCore.Qt.FillRule.OddEvenFill) plt.close() def test_mouseInteraction(): From c9645929e96e8104f80bb8ce93a064a3ef14eab1 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 4 Sep 2022 20:04:00 -0700 Subject: [PATCH 022/306] Have CSV exporter export error bar information (#2405) * Have CSV exporter export error bar information * Restructure CSV exporter to handle different plot types with greater ease --- pyqtgraph/exporters/CSVExporter.py | 157 ++++++++++++++++++----------- tests/exporters/test_csv.py | 31 +++++- 2 files changed, 128 insertions(+), 60 deletions(-) diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index 32b5047ebc..d918b52dd8 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -1,4 +1,9 @@ -from .. import PlotItem +import csv +import itertools + +import numpy as np + +from .. import ErrorBarItem, PlotItem from ..parametertree import Parameter from ..Qt import QtCore from .Exporter import Exporter @@ -16,71 +21,107 @@ def __init__(self, item): self.params = Parameter(name='params', type='group', children=[ {'name': 'separator', 'title': translate("Exporter", 'separator'), 'type': 'list', 'value': 'comma', 'limits': ['comma', 'tab']}, {'name': 'precision', 'title': translate("Exporter", 'precision'), 'type': 'int', 'value': 10, 'limits': [0, None]}, - {'name': 'columnMode', 'title': translate("Exporter", 'columnMode'), 'type': 'list', 'limits': ['(x,y) per plot', '(x,y,y,y) for all plots']} + { + 'name': 'columnMode', + 'title': translate("Exporter", 'columnMode'), + 'type': 'list', + 'limits': ['(x,y) per plot', '(x,y,y,y) for all plots'] + } ]) - + + self.index_counter = itertools.count(start=0) + self.header = [] + self.data = [] + def parameters(self): return self.params - + + def _exportErrorBarItem(self, errorBarItem: ErrorBarItem) -> None: + error_data = [] + index = next(self.index_counter) + + # make sure the plot actually has data: + if errorBarItem.opts['x'] is None or errorBarItem.opts['y'] is None: + return None + + header_naming_map = { + "left": "x_min_error", + "right": "x_max_error", + "bottom": "y_min_error", + "top": "y_max_error" + } + + # grab the base-points + self.header.extend([f'x{index:04}_error', f'y{index:04}_error']) + error_data.extend([errorBarItem.opts['x'], errorBarItem.opts['y']]) + + # grab the error bars + for error_direction, header_label in header_naming_map.items(): + if (error := errorBarItem.opts[error_direction]) is not None: + self.header.extend([f'{header_label}_{index:04}']) + error_data.append(error) + + self.data.append(tuple(error_data)) + return None + + def _exportPlotDataItem(self, plotDataItem) -> None: + if hasattr(plotDataItem, 'getOriginalDataset'): + # try to access unmapped, unprocessed data + cd = plotDataItem.getOriginalDataset() + else: + # fall back to earlier access method + cd = plotDataItem.getData() + if cd[0] is None: + # no data found, break out... + return None + self.data.append(cd) + + index = next(self.index_counter) + if plotDataItem.name() is not None: + name = plotDataItem.name().replace('"', '""') + '_' + xName = f"{name}x" + yName = f"{name}y" + else: + xName = f'x{index:04}' + yName = f'y{index:04}' + appendAllX = self.params['columnMode'] == '(x,y) per plot' + if appendAllX or index == 0: + self.header.extend([xName, yName]) + else: + self.header.extend([yName]) + return None + def export(self, fileName=None): - if not isinstance(self.item, PlotItem): - raise Exception("Must have a PlotItem selected for CSV export.") - + raise TypeError("Must have a PlotItem selected for CSV export.") + if fileName is None: self.fileSaveDialog(filter=["*.csv", "*.tsv"]) return - data = [] - header = [] + for item in self.item.items: + if isinstance(item, ErrorBarItem): + self._exportErrorBarItem(item) + elif hasattr(item, 'implements') and item.implements('plotData'): + self._exportPlotDataItem(item) - appendAllX = self.params['columnMode'] == '(x,y) per plot' + sep = "," if self.params['separator'] == 'comma' else "\t" + # we want to flatten the nested arrays of data into columns + columns = [column for dataset in self.data for column in dataset] + with open(fileName, 'w', newline='') as csvfile: + writer = csv.writer(csvfile, delimiter=sep, quoting=csv.QUOTE_MINIMAL) + writer.writerow(self.header) + for row in itertools.zip_longest(*columns, fillvalue=""): + row_to_write = [ + item if isinstance(item, str) + else np.format_float_positional( + item, precision=self.params['precision'] + ) + for item in row + ] + writer.writerow(row_to_write) - for i, c in enumerate(self.item.curves): - if hasattr(c, 'getOriginalDataset'): # try to access unmapped, unprocessed data - cd = c.getOriginalDataset() - else: - cd = c.getData() # fall back to earlier access method - if cd[0] is None: - continue - data.append(cd) - if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None: - name = c.name().replace('"', '""') + '_' - xName, yName = '"'+name+'x"', '"'+name+'y"' - else: - xName = 'x%04d' % i - yName = 'y%04d' % i - if appendAllX or i == 0: - header.extend([xName, yName]) - else: - header.extend([yName]) - - if self.params['separator'] == 'comma': - sep = ',' - else: - sep = '\t' - - with open(fileName, 'w') as fd: - fd.write(sep.join(map(str, header)) + '\n') - i = 0 - numFormat = '%%0.%dg' % self.params['precision'] - numRows = max([len(d[0]) for d in data]) - for i in range(numRows): - for j, d in enumerate(data): - # write x value if this is the first column, or if we want - # x for all rows - if appendAllX or j == 0: - if d is not None and i < len(d[0]): - fd.write(numFormat % d[0][i] + sep) - else: - fd.write(' %s' % sep) - - # write y value - if d is not None and i < len(d[1]): - fd.write(numFormat % d[1][i] + sep) - else: - fd.write(' %s' % sep) - fd.write('\n') - - -CSVExporter.register() + self.header.clear() + self.data.clear() + +CSVExporter.register() diff --git a/tests/exporters/test_csv.py b/tests/exporters/test_csv.py index 44bf580eef..75410a4322 100644 --- a/tests/exporters/test_csv.py +++ b/tests/exporters/test_csv.py @@ -1,12 +1,11 @@ """ CSV export test """ -from __future__ import absolute_import, division, print_function - import csv import tempfile import numpy as np +import pytest import pyqtgraph as pg @@ -54,3 +53,31 @@ def test_CSVExporter(): assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i]) assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i]) + +def test_CSVExporter_with_ErrorBarItem(): + plt = pg.plot() + x=np.arange(5) + y=np.array([1, 2, 3, 2, 1]) + top_error = np.array([2, 3, 3, 3, 2]) + bottom_error = np.array([-2.5, -2.5, -2.5, -2.5, -1.5]) + + err = pg.ErrorBarItem( + x=x, + y=y, + top=top_error, + bottom=bottom_error + ) + plt.addItem(err) + ex = pg.exporters.CSVExporter(plt.plotItem) + with tempfile.NamedTemporaryFile(mode="w+t", suffix='.csv', encoding="utf-8", delete=False) as tf: + ex.export(fileName=tf.name) + lines = [line for line in csv.reader(tf)] + + header = lines.pop(0) + + assert header == ['x0000_error', 'y0000_error', 'y_min_error_0000', 'y_max_error_0000'] + for i, values in enumerate(lines): + assert pytest.approx(float(values[0])) == x[i] + assert pytest.approx(float(values[1])) == y[i] + assert pytest.approx(float(values[2])) == bottom_error[i] + assert pytest.approx(float(values[3])) == top_error[i] From 6a57f14f2a4991ccbe306462855db9e5f22443c0 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 24 Jan 2021 17:58:40 -0800 Subject: [PATCH 023/306] Update paths to listmissing --- doc/listmissing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/listmissing.py b/doc/listmissing.py index 6268d81ee8..4acb1f52dd 100644 --- a/doc/listmissing.py +++ b/doc/listmissing.py @@ -7,8 +7,8 @@ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') for a, b in dirs: - rst = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, 'documentation', 'source', a))] - py = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, b))] + rst = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, 'doc', 'source', a))] + py = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, "pyqtgraph", b))] print(a) for x in set(py) - set(rst): print( " ", x) From 7d956fe93148fb5438269b5045b27a71e40fca49 Mon Sep 17 00:00:00 2001 From: Martin Chase Date: Wed, 7 Sep 2022 11:49:40 -0700 Subject: [PATCH 024/306] Add a few ImageView improvements (#1828) NEW features in ImageView: * timeline mouse interactions can be configured as discrete * histograms can be given a title * pause/unpause maintains playback speed Also, some style improvements. Squashed history: * +corrected wrong .opts key which caused example to crash +corrected buggy play() * removes 'pass' * ignorePlaying replaced with ignoreTimeLine * no thanks, import star * use tVals if present; use SignalBlock * small improvements * whitespace * remove discreteTimeLine from opts; remove opts * space toggles playitude * show off new features in example * nframes can be zero; no need to normalize first * bugs in setHistogramPrintView, so just remove it for now * docstring the new methods; lint * INCOMPLETE: test the discrete timeline * fix ui_template * numpy-style docstrings * numpy-style docstrings * fix test fails: imageItem might be None * a linspace across 31 points gives the values I expect * playback should resume at the previous play speed * style: no property decorators unless necessary * int, float; comments to track play state Co-authored-by: Karl Bedrich Co-authored-by: Ogi Moore --- pyqtgraph/examples/ImageView.py | 6 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 3 +- pyqtgraph/imageview/ImageView.py | 291 ++++++++++++-------- tests/imageview/test_imageview.py | 26 ++ 4 files changed, 215 insertions(+), 111 deletions(-) diff --git a/pyqtgraph/examples/ImageView.py b/pyqtgraph/examples/ImageView.py index c9fc49f01d..f8f4d85c90 100644 --- a/pyqtgraph/examples/ImageView.py +++ b/pyqtgraph/examples/ImageView.py @@ -24,10 +24,11 @@ ## Create window with ImageView widget win = QtWidgets.QMainWindow() win.resize(800,800) -imv = pg.ImageView() +imv = pg.ImageView(discreteTimeLine=True) win.setCentralWidget(imv) win.show() win.setWindowTitle('pyqtgraph example: ImageView') +imv.setHistogramLabel("Histogram label goes here") ## Create random 3D data set with time varying signals dataRed = np.ones((100, 200, 200)) * np.linspace(90, 150, 100)[:, np.newaxis, np.newaxis] @@ -42,8 +43,9 @@ ) -## Display the data and assign each frame a time value from 1.0 to 3.0 +# Display the data and assign each frame a time value from 1.0 to 3.0 imv.setImage(data, xvals=np.linspace(1., 3., data.shape[0])) +imv.play(10) ## Set a custom color map colors = [ diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 58c8f72262..fbf37e043e 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -437,7 +437,8 @@ def setLevelMode(self, mode): # force this because calling self.setLevels might not set the imageItem # levels if there was no change to the region item - self.imageItem().setLevels(self.getLevels()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.getLevels()) self.imageChanged() self.update() diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 9d559fbcb0..193c5dadbc 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -17,12 +17,9 @@ import numpy as np -from .. import functions as fn -from ..Qt import QtCore, QtGui, QtWidgets - -from . import ImageViewTemplate_generic as ui_template - +from .. import ImageItem, ViewBox from .. import debug as debug +from .. import functions as fn from .. import getConfigOption from ..graphicsItems.GradientEditorItem import addGradientListToDocstring from ..graphicsItems.ImageItem import * @@ -31,7 +28,9 @@ from ..graphicsItems.ROI import * from ..graphicsItems.ViewBox import * from ..graphicsItems.VTickGroup import VTickGroup +from ..Qt import QtCore, QtGui, QtWidgets from ..SignalProxy import SignalProxy +from . import ImageViewTemplate_generic as ui_template try: from bottleneck import nanmax, nanmin @@ -40,6 +39,7 @@ translate = QtCore.QCoreApplication.translate + class PlotROI(ROI): def __init__(self, size): ROI.__init__(self, pos=[0,0], size=size) #, scaleSnap=True, translateSnap=True) @@ -81,44 +81,48 @@ class ImageView(QtWidgets.QWidget): sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) - def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, - levelMode='mono', *args): + def __init__( + self, + parent=None, + name="ImageView", + view=None, + imageItem=None, + levelMode='mono', + discreteTimeLine=False, + *args, + ): """ By default, this class creates an :class:`ImageItem ` to display image data - and a :class:`ViewBox ` to contain the ImageItem. - - ============= ========================================================= - **Arguments** - parent (QWidget) Specifies the parent widget to which - this ImageView will belong. If None, then the ImageView - is created with no parent. - name (str) The name used to register both the internal ViewBox - and the PlotItem used to display ROI data. See the *name* - argument to :func:`ViewBox.__init__() - `. - view (ViewBox or PlotItem) If specified, this will be used - as the display area that contains the displayed image. - Any :class:`ViewBox `, - :class:`PlotItem `, or other - compatible object is acceptable. - imageItem (ImageItem) If specified, this object will be used to - display the image. Must be an instance of ImageItem - or other compatible object. - levelMode See the *levelMode* argument to - :func:`HistogramLUTItem.__init__() - ` - ============= ========================================================= - - Note: to display axis ticks inside the ImageView, instantiate it - with a PlotItem instance as its view:: - - pg.ImageView(view=pg.PlotItem()) + and a :class:`ViewBox ` to contain the ImageItem. + + Parameters + ---------- + parent : QWidget + Specifies the parent widget to which this ImageView will belong. If None, then the ImageView is created with + no parent. + name : str + The name used to register both the internal ViewBox and the PlotItem used to display ROI data. See the + *name* argument to :func:`ViewBox.__init__() `. + view : ViewBox or PlotItem + If specified, this will be used as the display area that contains the displayed image. Any + :class:`ViewBox `, :class:`PlotItem `, or other compatible object is + acceptable. Note: to display axis ticks inside the ImageView, instantiate it with a PlotItem instance as its + view:: + + pg.ImageView(view=pg.PlotItem()) + imageItem : ImageItem + If specified, this object will be used to display the image. Must be an instance of ImageItem or other + compatible object. + levelMode : str + See the *levelMode* argument to :func:`HistogramLUTItem.__init__() ` + discreteTimeLine : bool + Whether to snap to xvals / frame numbers when interacting with the timeline position. """ QtWidgets.QWidget.__init__(self, parent, *args) self._imageLevels = None # [(min, max), ...] per channel image metrics self.levelMin = None # min / max levels across all channels self.levelMax = None - + self.name = name self.image = None self.axes = {} @@ -126,9 +130,10 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, self.ui = ui_template.Ui_Form() self.ui.setupUi(self) self.scene = self.ui.graphicsView.scene() - - self.ignorePlaying = False - + self.discreteTimeLine = discreteTimeLine + self.ui.histogram.setLevelMode(levelMode) + self.ignoreTimeLine = False + if view is None: self.view = ViewBox() else: @@ -185,7 +190,8 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, self.keysPressed = {} self.playTimer = QtCore.QTimer() self.playRate = 0 - self.fps = 1 # 1 Hz by default + self._pausedPlayRate = None + self.fps = 1 # 1 Hz by default self.lastPlayTime = 0 self.normRgn = LinearRegionItem() @@ -220,43 +226,67 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, self.ui.roiPlot.registerPlot(self.name + '_ROI') self.view.register(self.name) - self.noRepeatKeys = [QtCore.Qt.Key.Key_Right, QtCore.Qt.Key.Key_Left, QtCore.Qt.Key.Key_Up, QtCore.Qt.Key.Key_Down, QtCore.Qt.Key.Key_PageUp, QtCore.Qt.Key.Key_PageDown] + self.noRepeatKeys = [ + QtCore.Qt.Key.Key_Right, + QtCore.Qt.Key.Key_Left, + QtCore.Qt.Key.Key_Up, + QtCore.Qt.Key.Key_Down, + QtCore.Qt.Key.Key_PageUp, + QtCore.Qt.Key.Key_PageDown, + ] self.roiClicked() ## initialize roi plot to correct shape / visibility - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True, levelMode=None): + def setImage( + self, + img, + autoRange=True, + autoLevels=True, + levels=None, + axes=None, + xvals=None, + pos=None, + scale=None, + transform=None, + autoHistogramRange=True, + levelMode=None, + ): """ Set the image to be displayed in the widget. - - ================== =========================================================================== - **Arguments:** - img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and - *notes* below. - xvals (numpy array) 1D array of z-axis values corresponding to the first axis - in a 3D image. For video, this array should contain the time of each - frame. - autoRange (bool) whether to scale/pan the view to fit the image. - autoLevels (bool) whether to update the white/black levels to fit the image. - levels (min, max); the white and black level values to use. - axes Dictionary indicating the interpretation for each axis. - This is only needed to override the default guess. Format is:: - - {'t':0, 'x':1, 'y':2, 'c':3}; - - pos Change the position of the displayed image - scale Change the scale of the displayed image - transform Set the transform of the displayed image. This option overrides *pos* - and *scale*. - autoHistogramRange If True, the histogram y-range is automatically scaled to fit the - image data. - levelMode If specified, this sets the user interaction mode for setting image - levels. Options are 'mono', which provides a single level control for - all image channels, and 'rgb' or 'rgba', which provide individual - controls for each channel. - ================== =========================================================================== - - **Notes:** - + + Parameters + ---------- + img : ndarray + The image to be displayed. See :func:`ImageItem.setImage` and *notes* below. + autoRange : bool + Whether to scale/pan the view to fit the image. + autoLevels : bool + Whether to update the white/black levels to fit the image. + levels : (min, max) + The white and black level values to use. + axes : dict + Dictionary indicating the interpretation for each axis. This is only needed to override the default guess. + Format is:: + + {'t':0, 'x':1, 'y':2, 'c':3}; + xvals : ndarray + 1D array of values corresponding to the first axis in a 3D image. For video, this array should contain + the time of each frame. + pos + Change the position of the displayed image + scale + Change the scale of the displayed image + transform + Set the transform of the displayed image. This option overrides *pos* and *scale*. + autoHistogramRange : bool + If True, the histogram y-range is automatically scaled to fit the image data. + levelMode : str + If specified, this sets the user interaction mode for setting image levels. Options are 'mono', + which provides a single level control for all image channels, and 'rgb' or 'rgba', which provide + individual controls for each channel. + + Notes + ----- For backward compatibility, image data is assumed to be in column-major order (column, row). However, most image data is stored in row-major order (row, column) and will need to be transposed before calling setImage():: @@ -265,30 +295,29 @@ def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, This requirement can be changed by the ``imageAxisOrder`` :ref:`global configuration option `. - """ profiler = debug.Profiler() - + if hasattr(img, 'implements') and img.implements('MetaArray'): img = img.asarray() - + if not isinstance(img, np.ndarray): required = ['dtype', 'max', 'min', 'ndim', 'shape', 'size'] if not all(hasattr(img, attr) for attr in required): raise TypeError("Image must be NumPy array or any object " "that provides compatible attributes/methods:\n" " %s" % str(required)) - + self.image = img self.imageDisp = None if levelMode is not None: self.ui.histogram.setLevelMode(levelMode) - + profiler() - + if axes is None: x,y = (0, 1) if self.imageItem.axisOrder == 'col-major' else (1, 0) - + if img.ndim == 2: self.axes = {'t': None, 'x': x, 'y': y, 'c': None} elif img.ndim == 3: @@ -383,9 +412,10 @@ def clear(self): def play(self, rate=None): """Begin automatically stepping frames forward at the given rate (in fps). This can also be accessed by pressing the spacebar.""" - #print "play:", rate - if rate is None: - rate = self.fps + if rate is None: + rate = self._pausedPlayRate or self.fps + if rate == 0 and self.playRate not in (None, 0): + self._pausedPlayRate = self.playRate self.playRate = rate if rate == 0: @@ -394,8 +424,43 @@ def play(self, rate=None): self.lastPlayTime = perf_counter() if not self.playTimer.isActive(): - self.playTimer.start(16) - + self.playTimer.start(abs(int(1000/rate))) + + def togglePause(self): + if self.playTimer.isActive(): + self.play(0) + elif self.playRate == 0: + if self._pausedPlayRate is not None: + fps = self._pausedPlayRate + else: + fps = (self.nframes() - 1) / (self.tVals[-1] - self.tVals[0]) + self.play(fps) + else: + self.play(self.playRate) + + def setHistogramLabel(self, text=None, **kwargs): + """ + Set the label text of the histogram axis similar to + :func:`AxisItem.setLabel() ` + """ + a = self.ui.histogram.axis + a.setLabel(text, **kwargs) + if text == '': + a.showLabel(False) + self.ui.histogram.setMinimumWidth(135) + + def nframes(self): + """ + Returns + ------- + int + The number of frames in the image data. + """ + if self.image is None: + return 0 + else: + return self.image.shape[0] + def autoLevels(self): """Set the min/max intensity levels automatically to match the image data.""" self.setLevels(rgba=self._imageLevels) @@ -438,17 +503,14 @@ def keyPressEvent(self, ev): return if ev.key() == QtCore.Qt.Key.Key_Space: - if self.playRate == 0: - self.play() - else: - self.play(0) + self.togglePause() ev.accept() elif ev.key() == QtCore.Qt.Key.Key_Home: self.setCurrentIndex(0) self.play(0) ev.accept() elif ev.key() == QtCore.Qt.Key.Key_End: - self.setCurrentIndex(self.getProcessedImage().shape[0]-1) + self.setCurrentIndex(self.nframes()-1) self.play(0) ev.accept() elif ev.key() in self.noRepeatKeys: @@ -516,11 +578,13 @@ def timeout(self): def setCurrentIndex(self, ind): """Set the currently displayed frame index.""" - index = fn.clip_scalar(ind, 0, self.getProcessedImage().shape[self.axes['t']]-1) - self.ignorePlaying = True + index = fn.clip_scalar(ind, 0, self.nframes()-1) + self.currentIndex = index + self.updateImage() + self.ignoreTimeLine = True # Implicitly call timeLineChanged self.timeLine.setValue(self.tVals[index]) - self.ignorePlaying = False + self.ignoreTimeLine = False def jumpFrames(self, n): """Move video frame ahead n frames (may be negative)""" @@ -728,22 +792,28 @@ def normalize(self, image): return norm def timeLineChanged(self): - if not self.ignorePlaying: + if not self.ignoreTimeLine: self.play(0) (ind, time) = self.timeIndex(self.timeLine) if ind != self.currentIndex: self.currentIndex = ind self.updateImage() + if self.discreteTimeLine: + with fn.SignalBlock(self.timeLine.sigPositionChanged, self.timeLineChanged): + if self.tVals is not None: + self.timeLine.setPos(self.tVals[ind]) + else: + self.timeLine.setPos(ind) + self.sigTimeChanged.emit(ind, time) def updateImage(self, autoHistogramRange=True): ## Redraw image on screen if self.image is None: return - + image = self.getProcessedImage() - if autoHistogramRange: self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) @@ -761,13 +831,19 @@ def updateImage(self, autoHistogramRange=True): image = image[self.currentIndex] self.imageItem.updateImage(image) - - + def timeIndex(self, slider): - ## Return the time and frame index indicated by a slider + """ + Returns + ------- + int + The index of the frame closest to the timeline slider. + float + The time value of the slider. + """ if not self.hasTimeAxis(): - return (0,0) - + return 0, 0.0 + t = slider.value() xv = self.tVals @@ -775,11 +851,11 @@ def timeIndex(self, slider): ind = int(t) else: if len(xv) < 2: - return (0,0) + return 0, 0.0 inds = np.argwhere(xv <= t) if len(inds) < 1: - return (0,t) - ind = inds[-1,0] + return 0, t + ind = inds[-1, 0] return ind, t def getView(self): @@ -840,11 +916,10 @@ def menuClicked(self): def setColorMap(self, colormap): """Set the color map. - ============= ========================================================= - **Arguments** - colormap (A ColorMap() instance) The ColorMap to use for coloring - images. - ============= ========================================================= + Parameters + ---------- + colormap : ColorMap + The ColorMap to use for coloring images. """ self.ui.histogram.gradient.setColorMap(colormap) diff --git a/tests/imageview/test_imageview.py b/tests/imageview/test_imageview.py index 885993e778..73de91599f 100644 --- a/tests/imageview/test_imageview.py +++ b/tests/imageview/test_imageview.py @@ -4,6 +4,7 @@ app = pg.mkQApp() + def test_nan_image(): img = np.ones((10,10)) img[0,0] = np.nan @@ -12,6 +13,31 @@ def test_nan_image(): app.processEvents() v.window().close() + +def test_timeslide_snap(): + count = 31 + frames = np.ones((count, 10, 10)) + iv = pg.ImageView(discreteTimeLine=True) + assert iv.nframes() == 0 + iv.setImage(frames, xvals=(np.linspace(0., 1., count))) + iv.show() + assert iv.nframes() == count + speed = count / 2 + iv.play(speed) + assert iv.playRate == speed + iv.timeLine.setPos(0.51) # side effect: also pauses playback + assert iv.playRate == 0 + ind, val = iv.timeIndex(iv.timeLine) + assert ind == count // 2 + assert val == 0.5 + iv.togglePause() # restarts playback + assert iv.playRate == speed + iv.togglePause() # pauses playback + assert iv.playRate == 0 + iv.play() + assert iv.playRate == speed + + def test_init_with_mode_and_imageitem(): data = np.random.randint(256, size=(256, 256, 3)) imgitem = pg.ImageItem(data) From 20b63f757e93a5ecaad99a77c34029f365335ce7 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 7 Sep 2022 14:55:03 -0400 Subject: [PATCH 025/306] Bugfix: `interact` on decorated method that uses `self`. The new example used to fail before this PR. A result of #2404 was that `inspect.signature` no longer realized that `self` was a bound variable in class-defined, outside-decorated functions. Fix this simply by calling `functionToParameterDict` on the original function, rather than the interactive version. --- pyqtgraph/parametertree/interactive.py | 4 ++-- tests/parametertree/test_Parameter.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 8151cf8d43..7be1d2f822 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -326,11 +326,11 @@ def interact( # Delete explicitly since correct values are now ``self`` attributes del runOpts, titleFormat, nest, existOk, parent - funcDict = self.functionToParameterDict(function, **overrides) + function = self._toInteractiveFunction(function) + funcDict = self.functionToParameterDict(function.function, **overrides) children = funcDict.pop("children", []) # type: list[dict] chNames = [ch["name"] for ch in children] funcGroup = self._resolveFunctionGroup(funcDict) - function = self._toInteractiveFunction(function) # Values can't come both from closures and overrides/params, so ensure they don't # get created diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 3c48fa24e4..08ec66ee64 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -388,6 +388,13 @@ def test_class_interact(): parent = Parameter.create(name="parent", type="group") interactor = Interactor(parent=parent, nest=False) + def outside_class_deco(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + class A: def a(self, x=5): return x @@ -396,9 +403,16 @@ def a(self, x=5): def b(cls, y=5): return y + @outside_class_deco + def c(self, z=5): + return z + a = A() ai = interactor.decorate()(a.a) assert ai() == a.a() bi = interactor.decorate()(A.b) assert bi() == A.b() + + ci = interactor.decorate()(a.c) + assert ci() == a.c() From 74aa92030b570f166f4a1b59ee4430acfabcf4b5 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 7 Sep 2022 15:17:29 -0400 Subject: [PATCH 026/306] Clean up `RunOpts` - `runOpts` -> `runOptions` - Don't pollute `RunOptions` namespace with `UNSET` sentinel indicator, since it isn't affiliated with the run button behavior --- .../parametertree/interactiveparameters.rst | 34 ++++++------- pyqtgraph/examples/InteractiveParameter.py | 6 +-- pyqtgraph/parametertree/__init__.py | 2 +- pyqtgraph/parametertree/interactive.py | 49 +++++++++++-------- tests/parametertree/test_Parameter.py | 33 +++++++++---- 5 files changed, 72 insertions(+), 52 deletions(-) diff --git a/doc/source/parametertree/interactiveparameters.rst b/doc/source/parametertree/interactiveparameters.rst index 1d010117df..1b08bd145b 100644 --- a/doc/source/parametertree/interactiveparameters.rst +++ b/doc/source/parametertree/interactiveparameters.rst @@ -73,24 +73,24 @@ code below is functionally equivalent to above): There are several caveats, but this is one of the most common scenarios for function interaction. -``runOpts`` -^^^^^^^^^^^ +``runOptions`` +^^^^^^^^^^^^^^ Often, an ``interact``-ed function shouldn't run until multiple parameter values are changed. Or, the function should be run every time a value is *changing*, not just changed. In these cases, modify the -``runOpts`` parameter. +``runOptions`` parameter. .. code:: python - from pyqtgraph.parametertree import interact, RunOpts + from pyqtgraph.parametertree import interact, RunOptions # Will add a button named "Run". When clicked, the function will run - params = interact(a, runOpts=RunOpts.ON_ACTION) + params = interact(a, runOptions=RunOptions.ON_ACTION) # Will run on any `sigValueChanging` signal - params = interact(a, runOpts=RunOpts.ON_CHANGING) + params = interact(a, runOptions=RunOptions.ON_CHANGING) # Runs on `sigValueChanged` or when "Run" is pressed - params = interact(a, runOpts=[RunOpts.ON_CHANGED, RunOpts.ON_ACTION]) + params = interact(a, runOptions=[RunOptions.ON_CHANGED, RunOptions.ON_ACTION]) # Any combination of RUN_* options can be used The default run behavior can also be modified. If several functions are @@ -100,13 +100,13 @@ use the provided context manager: .. code:: python from pyqtgraph.parametertree import interact - # `runOpts` can be set to any combination of options as demonstrated above, too - with interact.optsContext(runOpts=RunOpts.ON_ACTION): - # All will have `runOpts` set to ON_ACTION + # `runOptions` can be set to any combination of options as demonstrated above, too + with interact.optsContext(runOptions=RunOptions.ON_ACTION): + # All will have `runOptions` set to ON_ACTION p1 = interact(aFunc) p2 = interact(bFunc) p3 = interact(cFunc) - # After the context, `runOpts` is back to the previous default + # After the context, `runOptions` is back to the previous default If the default for all interaction should be changed, you can directly call ``interactDefaults.setOpts`` (but be warned - anyone who imports your @@ -119,9 +119,9 @@ resetting afterward: from pyqtgraph.parametertree import Interactor myInteractor = Interactor() - oldOpts = myInteractor.setOpts(runOpts=RunOpts.ON_ACTION) + oldOpts = myInteractor.setOpts(runOptions=RunOptions.ON_ACTION) # Can also directly create interactor with these opts: - # myInteractor = Interactor(runOpts=RunOpts.ON_ACTION) + # myInteractor = Interactor(runOptions=RunOptions.ON_ACTION) # ... do some things... # Unset option @@ -367,13 +367,13 @@ and ``reconnect()`` methods, and object accessors to ``closures`` arguments. .. code:: python - from pyqtgraph.parametertree import InteractiveFunction, interact, Parameter, RunOpts + from pyqtgraph.parametertree import InteractiveFunction, interact, Parameter, RunOptions def myfunc(a=5): print(a) useFunc = InteractiveFunction(myfunc) - param = interact(useFunc, runOpts=RunOpts.ON_CHANGED) + param = interact(useFunc, runOptions=RunOptions.ON_CHANGED) param['a'] = 6 # Will print 6 useFunc.disconnect() @@ -388,7 +388,7 @@ can use ``InteractiveFunction`` like a decorator: .. code:: python - from pyqtgraph.parametertree import InteractiveFunction, interact, Parameter, RunOpts + from pyqtgraph.parametertree import InteractiveFunction, interact, Parameter, RunOptions @InteractiveFunction def myfunc(a=5): @@ -396,7 +396,7 @@ can use ``InteractiveFunction`` like a decorator: # myfunc is now an InteractiveFunction that can be used as above # Also, calling `myfunc` will preserve parameter arguments - param = interact(myfunc, RunOpts.ON_ACTION) + param = interact(myfunc, RunOptions.ON_ACTION) param['a'] = 6 myfunc() diff --git a/pyqtgraph/examples/InteractiveParameter.py b/pyqtgraph/examples/InteractiveParameter.py index 58f55c8916..139669ec7f 100644 --- a/pyqtgraph/examples/InteractiveParameter.py +++ b/pyqtgraph/examples/InteractiveParameter.py @@ -5,7 +5,7 @@ from pyqtgraph.parametertree import ( Parameter, ParameterTree, - RunOpts, + RunOptions, InteractiveFunction, Interactor, ) @@ -60,7 +60,7 @@ def ignoredAParam(a=10, b=20): return a * b -@interactor.decorate(runOpts=RunOpts.ON_ACTION) +@interactor.decorate(runOptions=RunOptions.ON_ACTION) @printResult def runOnButton(a=10, b=20): return a + b @@ -91,7 +91,7 @@ def capslocknames(a=5): @interactor.decorate( - runOpts=(RunOpts.ON_CHANGED, RunOpts.ON_ACTION), + runOptions=(RunOptions.ON_CHANGED, RunOptions.ON_ACTION), a={"type": "list", "limits": [5, 10, 20]}, ) @printResult diff --git a/pyqtgraph/parametertree/__init__.py b/pyqtgraph/parametertree/__init__.py index 417637925b..44b758d04b 100644 --- a/pyqtgraph/parametertree/__init__.py +++ b/pyqtgraph/parametertree/__init__.py @@ -3,4 +3,4 @@ from .ParameterItem import ParameterItem from .ParameterSystem import ParameterSystem, SystemSolver from .ParameterTree import ParameterTree -from .interactive import RunOpts, interact, InteractiveFunction, Interactor +from .interactive import RunOptions, interact, InteractiveFunction, Interactor diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 7be1d2f822..7897179362 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -7,9 +7,11 @@ from .. import functions as fn -class RunOpts: - class PARAM_UNSET: - """Sentinel value for detecting parameters with unset values""" +class PARAM_UNSET: + """Sentinel value for detecting parameters with unset values""" + + +class RunOptions: ON_ACTION = "action" """ @@ -197,14 +199,21 @@ def __repr__(self): class Interactor: - runOpts = RunOpts.ON_CHANGED + runOptions = RunOptions.ON_CHANGED parent = None titleFormat = None nest = True existOk = True runActionTemplate = dict(type="action", defaultName="Run") - _optNames = ["runOpts", "parent", "titleFormat", "nest", "existOk", "runActionTemplate"] + _optNames = [ + "runOptions", + "parent", + "titleFormat", + "nest", + "existOk", + "runActionTemplate", + ] def __init__(self, **kwargs): """ @@ -265,11 +274,11 @@ def interact( function, *, ignores=None, - runOpts=RunOpts.PARAM_UNSET, - parent=RunOpts.PARAM_UNSET, - titleFormat=RunOpts.PARAM_UNSET, - nest=RunOpts.PARAM_UNSET, - existOk=RunOpts.PARAM_UNSET, + runOptions=PARAM_UNSET, + parent=PARAM_UNSET, + titleFormat=PARAM_UNSET, + nest=PARAM_UNSET, + existOk=PARAM_UNSET, **overrides, ): """ @@ -287,7 +296,7 @@ def interact( function: Callable function with which to interact. Can also be a :class:`InteractiveFunction`, if a reference to the bound signals is required. - runOpts: ``GroupParameter.`` value + runOptions: ``GroupParameter.`` value How the function should be run, i.e. when pressing an action, on sigValueChanged, and/or on sigValueChanging ignores: Sequence @@ -318,13 +327,11 @@ def interact( locs = locals() # Everything until action template opts = { - kk: locs[kk] - for kk in self._optNames[:-1] - if locs[kk] is not RunOpts.PARAM_UNSET + kk: locs[kk] for kk in self._optNames[:-1] if locs[kk] is not PARAM_UNSET } oldOpts = self.setOpts(**opts) # Delete explicitly since correct values are now ``self`` attributes - del runOpts, titleFormat, nest, existOk, parent + del runOptions, titleFormat, nest, existOk, parent function = self._toInteractiveFunction(function) funcDict = self.functionToParameterDict(function.function, **overrides) @@ -342,13 +349,13 @@ def interact( recycleNames = set(ignores) & set(chNames) for name in recycleNames: value = children[chNames.index(name)]["value"] - if name not in function.extra and value is not RunOpts.PARAM_UNSET: + if name not in function.extra and value is not PARAM_UNSET: function.extra[name] = value missingChildren = [ ch["name"] for ch in children - if ch["value"] is RunOpts.PARAM_UNSET + if ch["value"] is PARAM_UNSET and ch["name"] not in function.closures and ch["name"] not in function.extra ] @@ -368,7 +375,7 @@ def interact( function.hookupParameters(useParams) # If no top-level parent and no nesting, return the list of child parameters ret = funcGroup or useParams - if RunOpts.ON_ACTION in self.runOpts: + if RunOptions.ON_ACTION in self.runOptions: # Add an extra action child which can activate the function action = self._makeRunAction(self.nest, funcDict.get("tip"), function) # Return just the action if no other params were allowed @@ -460,9 +467,9 @@ def resolveAndHookupParameterChild(self, funcGroup, childOpts, interactiveFunc): child = Parameter.create(**childOpts) else: child = funcGroup.addChild(childOpts, existOk=self.existOk) - if RunOpts.ON_CHANGED in self.runOpts: + if RunOptions.ON_CHANGED in self.runOptions: child.sigValueChanged.connect(interactiveFunc.runFromChangedOrChanging) - if RunOpts.ON_CHANGING in self.runOpts: + if RunOptions.ON_CHANGING in self.runOptions: child.sigValueChanging.connect(interactiveFunc.runFromChangedOrChanging) return child @@ -556,7 +563,7 @@ def createFunctionParameter(self, name, signatureParameter, overridesInfo): pgDict["name"] = name # Required function arguments with any override specifications can still be # unfilled at this point - pgDict.setdefault("value", RunOpts.PARAM_UNSET) + pgDict.setdefault("value", PARAM_UNSET) # Anywhere a title is specified should take precedence over the default factory if self.titleFormat is not None: diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 08ec66ee64..1e270d752b 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -2,7 +2,12 @@ from functools import wraps from pyqtgraph.parametertree import Parameter from pyqtgraph.parametertree.parameterTypes import GroupParameter as GP -from pyqtgraph.parametertree import RunOpts, InteractiveFunction, Interactor, interact +from pyqtgraph.parametertree import ( + RunOptions, + InteractiveFunction, + Interactor, + interact, +) def test_parameter_hasdefault(): @@ -65,7 +70,7 @@ def test_unpack_parameter(): def test_interact(): - interactor = Interactor(runOpts=RunOpts.ON_ACTION) + interactor = Interactor(runOptions=RunOptions.ON_ACTION) value = None def retain(func): @@ -106,7 +111,11 @@ def a(x, y=5): assert value == (10, 5) host = interactor( - a, x=10, y=50, ignores=["x"], runOpts=(RunOpts.ON_CHANGED, RunOpts.ON_CHANGING) + a, + x=10, + y=50, + ignores=["x"], + runOptions=(RunOptions.ON_CHANGED, RunOptions.ON_CHANGING), ) for child in "x", "Run": assert child not in host.names @@ -127,7 +136,7 @@ def a(x, y=5): assert host.title() == "Group only" assert [p.title() is None for p in host] - with interactor.optsContext(runOpts=RunOpts.ON_CHANGED): + with interactor.optsContext(runOptions=RunOptions.ON_CHANGED): host = interactor(a, x=5) host["y"] = 20 assert value == (5, 20) @@ -156,7 +165,7 @@ def a(x=5): host.child("a", "Run").activate() assert value == 5 - @interactor.decorate(nest=False, runOpts=RunOpts.ON_CHANGED) + @interactor.decorate(nest=False, runOptions=RunOptions.ON_CHANGED) @retain def b(y=6): return y @@ -173,7 +182,7 @@ def raw(x=5): def override(**kwargs): return raw(**kwargs) - host = interactor(wraps(raw)(override), runOpts=RunOpts.ON_CHANGED) + host = interactor(wraps(raw)(override), runOptions=RunOptions.ON_CHANGED) assert "x" in host.names host["x"] = 100 assert value == 100 @@ -183,7 +192,7 @@ def test_run(): def a(): """""" - interactor = Interactor(runOpts=RunOpts.ON_ACTION) + interactor = Interactor(runOptions=RunOptions.ON_ACTION) defaultRunBtn = Parameter.create(**interactor.runActionTemplate, name="Run") btn = interactor(a) @@ -201,6 +210,7 @@ def a(): test2 = interactor(a, nest=False) assert not test2.parent() + def test_no_func_group(): def inner(a=5, b=6): return a + b @@ -215,7 +225,7 @@ def a(): interactor = Interactor() - btn = interactor(a, runOpts=RunOpts.ON_ACTION) + btn = interactor(a, runOptions=RunOptions.ON_ACTION) assert btn.opts["tip"] == a.__doc__ def a2(x=5): @@ -227,7 +237,7 @@ def a3(x=5): followed by more text won't result in a tooltip """ - param = interactor(a2, runOpts=RunOpts.ON_ACTION) + param = interactor(a2, runOptions=RunOptions.ON_ACTION) assert param.opts["tip"] == a2.__doc__ and param.type() == "group" param = interactor(a3) @@ -243,7 +253,7 @@ def myfunc(a=5): return a interactive = InteractiveFunction(myfunc) - host = interact(interactive, runOpts=[]) + host = interact(interactive, runOptions=[]) host["a"] = 7 assert interactive.runFromAction() == 7 @@ -354,6 +364,7 @@ class RetainVal: def a(x=3, **kwargs): RetainVal.a = sum(kwargs.values()) + x return RetainVal.a + a.parametersNeedRunKwargs = True host = interact(a) @@ -372,10 +383,12 @@ def a(x=3, **kwargs): # But the cache should still be up-to-date assert a() == 5 + def test_hookup_extra_params(): @InteractiveFunction def a(x=5, **kwargs): return x + sum(kwargs.values()) + interact(a) p2 = Parameter.create(name="p2", type="int", value=3) From d2b4b0a61274a749e1fe8f72f5fb4219a7683d78 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sat, 1 May 2021 09:01:05 -0700 Subject: [PATCH 027/306] Fix colormapwidget sphinx links --- doc/source/widgets/colormapwidget.rst | 8 +++++--- pyqtgraph/widgets/ColorMapWidget.py | 2 +- pyqtgraph/widgets/ScatterPlotWidget.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/source/widgets/colormapwidget.rst b/doc/source/widgets/colormapwidget.rst index e6b3bb0bcb..deebec5978 100644 --- a/doc/source/widgets/colormapwidget.rst +++ b/doc/source/widgets/colormapwidget.rst @@ -6,7 +6,9 @@ ColorMapWidget .. automethod:: pyqtgraph.ColorMapWidget.__init__ - .. automethod:: pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields + .. automethod:: pyqtgraph.ColorMapParameter.setFields - .. automethod:: pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.map - + .. automethod:: pyqtgraph.ColorMapParameter.map + +.. autoclass:: pyqtgraph.widgets.ColorMapWidget.ColorMapParameter + :members: diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 8fbcb78fe6..e31bdf4261 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -6,7 +6,7 @@ from .. import parametertree as ptree from ..Qt import QtCore -__all__ = ['ColorMapWidget'] +__all__ = ['ColorMapWidget', 'ColorMapParameter'] class ColorMapWidget(ptree.ParameterTree): """ diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index 5411e4fdb9..5dc7ab68bd 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -82,7 +82,7 @@ def setFields(self, fields, mouseOverField=None): Set the list of field names/units to be processed. The format of *fields* is the same as used by - :func:`ColorMapWidget.setFields ` + :meth:`~pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields` """ self.fields = OrderedDict(fields) self.mouseOverField = mouseOverField From a8ef156955e12c073ae52a463184b25f00176209 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 8 Sep 2022 22:50:47 -0700 Subject: [PATCH 028/306] Fix variety of errors that came up in nit-picky mode checks --- doc/Makefile | 2 +- doc/requirements.txt | 3 ++- doc/source/apireference.rst | 1 + doc/source/conf.py | 20 +++++++++++++++--- doc/source/parametertree/parametertypes.rst | 3 +++ doc/source/plotting.rst | 6 +++--- doc/source/point.rst | 17 +++++++++++++++ doc/source/prototyping.rst | 2 ++ doc/source/widgets/index.rst | 1 + doc/source/widgets/rawimagewidget.rst | 9 ++++++++ pyqtgraph/GraphicsScene/GraphicsScene.py | 2 +- pyqtgraph/__init__.py | 1 + pyqtgraph/graphicsItems/ColorBarItem.py | 7 ++++--- pyqtgraph/graphicsItems/GraphicsLayout.py | 21 ++++++++++++++++++- pyqtgraph/graphicsItems/PColorMeshItem.py | 13 +++++------- pyqtgraph/graphicsItems/PlotDataItem.py | 2 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 19 ++++++++++------- pyqtgraph/graphicsItems/TargetItem.py | 11 +++++----- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- pyqtgraph/imageview/ImageView.py | 10 ++++----- pyqtgraph/opengl/items/GLGraphItem.py | 9 ++++---- .../parametertree/parameterTypes/basetypes.py | 10 ++++----- 22 files changed, 120 insertions(+), 51 deletions(-) create mode 100644 doc/source/point.rst create mode 100644 doc/source/widgets/rawimagewidget.rst diff --git a/doc/Makefile b/doc/Makefile index 15b77d380e..0f1e4ba258 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -10,7 +10,7 @@ BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest diff --git a/doc/requirements.txt b/doc/requirements.txt index 3571388ed0..cb82cf797d 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,6 @@ sphinx==5.1.1 PyQt6==6.3.1 -sphinx_rtd_theme +sphinx-rtd-theme +sphinx-qt-documentation numpy pyopengl diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index 27a8ae8848..6a3d028323 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -16,3 +16,4 @@ Contents: dockarea graphicsscene/index flowchart/index + point diff --git a/doc/source/conf.py b/doc/source/conf.py index d241c81516..f660ee3ec4 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -10,9 +10,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import time -import sys import os +import sys +import time from datetime import datetime # If extensions (or modules to document with autodoc) are in another directory, @@ -30,7 +30,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx_qt_documentation'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -38,6 +38,20 @@ # The suffix of source filenames. source_suffix = '.rst' +# Set Qt Documentation Variable +qt_documentation = "Qt6" + +extensions += ["sphinx.ext.intersphinx"] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'numpy': ('https://numpy.org/doc/stable/', None) +} + +napoleon_preprocess_types = True +napoleon_type_aliases = { + "callable": ":class:`collections.abc.Callable`" +} + # The encoding of source files. #source_encoding = 'utf-8-sig' diff --git a/doc/source/parametertree/parametertypes.rst b/doc/source/parametertree/parametertypes.rst index ef524440ae..f77b4187cc 100644 --- a/doc/source/parametertree/parametertypes.rst +++ b/doc/source/parametertree/parametertypes.rst @@ -102,3 +102,6 @@ ParameterItems .. autoclass:: TextParameterItem :members: + +.. autoclass:: WidgetParameterItem + :members: diff --git a/doc/source/plotting.rst b/doc/source/plotting.rst index 956f5b97ee..6fd5c27702 100644 --- a/doc/source/plotting.rst +++ b/doc/source/plotting.rst @@ -3,12 +3,12 @@ Plotting in pyqtgraph There are a few basic ways to plot data in pyqtgraph: -=================================================================== ================================================== +=================================================================== ===================================================== :func:`pyqtgraph.plot` Create a new plot window showing your data -:func:`PlotWidget.plot() ` Add a new set of data to an existing plot widget :func:`PlotItem.plot() ` Add a new set of data to an existing plot widget +``PlotWidget.plot()`` Calls :func:`PlotItem.plot ` :func:`GraphicsLayout.addPlot() ` Add a new plot to a grid of plots -=================================================================== ================================================== +=================================================================== ===================================================== All of these will accept the same basic arguments which control how the plot data is interpreted and displayed: diff --git a/doc/source/point.rst b/doc/source/point.rst new file mode 100644 index 0000000000..26e5e24616 --- /dev/null +++ b/doc/source/point.rst @@ -0,0 +1,17 @@ +Point +===== + +.. automodule:: pyqtgraph.Point + :members: + + .. automethod:: pyqtgraph.Point.length + + .. automethod:: pyqtgraph.Point.norm + + .. automethod:: pyqtgraph.Point.angle + + .. automethod:: pyqtgraph.Point.dot + + .. automethod:: pyqtgraph.Point.cross + + .. automethod:: pyqtgraph.Point.proj diff --git a/doc/source/prototyping.rst b/doc/source/prototyping.rst index a392f20e6c..3d2688d112 100644 --- a/doc/source/prototyping.rst +++ b/doc/source/prototyping.rst @@ -21,6 +21,8 @@ PyQtGraph's flowcharts provide a visual programming environment similar in conce See the `flowchart documentation `_ and the flowchart examples for more information. +.. _Canvas: + Graphical Canvas ---------------- diff --git a/doc/source/widgets/index.rst b/doc/source/widgets/index.rst index e5acb7f05d..06d87759eb 100644 --- a/doc/source/widgets/index.rst +++ b/doc/source/widgets/index.rst @@ -38,3 +38,4 @@ Contents: pathbutton valuelabel busycursor + rawimagewidget diff --git a/doc/source/widgets/rawimagewidget.rst b/doc/source/widgets/rawimagewidget.rst new file mode 100644 index 0000000000..db6dc7f031 --- /dev/null +++ b/doc/source/widgets/rawimagewidget.rst @@ -0,0 +1,9 @@ +RawImageWidget +============== + +.. autoclass:: pyqtgraph.RawImageWidget + :members: + + .. automethod:: pyqtgraph.widgets.RawImageWidget.RawImageWidget.__init__ + + .. automethod:: pyqtgraph.widgets.RawImageWidget.RawImageWidget.setImage diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 3c9e30e6ee..dd7f757472 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -34,7 +34,7 @@ class GraphicsScene(QtWidgets.QGraphicsScene): 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`. + See :class:`pyqtgraph.GraphicsScene.mouseEvents.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 diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index ead6a1eb48..ae932f2553 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -278,6 +278,7 @@ def renamePyc(startDir): from .widgets.PathButton import * from .widgets.PlotWidget import * from .widgets.ProgressDialog import * +from .widgets.RawImageWidget import * from .widgets.RemoteGraphicsView import RemoteGraphicsView from .widgets.ScatterPlotWidget import * from .widgets.SpinBox import * diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py index 2e31501ebd..a226ebfe9e 100644 --- a/pyqtgraph/graphicsItems/ColorBarItem.py +++ b/pyqtgraph/graphicsItems/ColorBarItem.py @@ -25,9 +25,10 @@ class ColorBarItem(PlotItem): A labeled axis is displayed directly next to the gradient to help identify values. Handles included in the color bar allow for interactive adjustment. - A ColorBarItem can be assigned one or more :class:`~pyqtgraph.ImageItem`s that will be displayed - according to the selected color map and levels. The ColorBarItem can be used as a separate - element in a :class:`~pyqtgraph.GraphicsLayout` or added to the layout of a + A ColorBarItem can be assigned one or more :class:`~pyqtgraph.ImageItem` s + that will be displayed according to the selected color map and levels. The + ColorBarItem can be used as a separate element in a + :class:`~pyqtgraph.GraphicsLayout` or added to the layout of a :class:`~pyqtgraph.PlotItem` used to display image data with coordinate axes. ============================= ============================================= diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index 2e90b41ec8..6d2bddb224 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -149,10 +149,27 @@ def boundingRect(self): return self.rect() def itemIndex(self, item): + """Return the numerical index of GraphicsItem object passed in + + Parameters + ---------- + item : pyqtgraph.Qt.QtWidgets.QGraphicsLayoutItem + Item to query the index position of + + Returns + ------- + int + Index of the item within the graphics layout + + Raises + ------ + ValueError + Raised if item could not be found inside the GraphicsLayout instance. + """ for i in range(self.layout.count()): if self.layout.itemAt(i).graphicsItem() is item: return i - raise Exception("Could not determine index of item " + str(item)) + raise ValueError(f"Could not determine index of item {item}") def removeItem(self, item): """Remove *item* from the layout.""" @@ -171,6 +188,8 @@ def removeItem(self, item): self.update() def clear(self): + """Remove all items from the layout and set the current row and column to 0 + """ for i in list(self.items.keys()): self.removeItem(i) self.currentRow = 0 diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index 0be9c7b342..5bfc0b8990 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -84,18 +84,15 @@ def __init__(self, *args, **kwargs): +---------+ (x[i, j], y[i, j]) (x[i, j+1], y[i, j+1]) - "ASCII from: ". - colorMap : pg.ColorMap, default pg.colormap.get('viridis') + "ASCII from: ". + colorMap : pg.ColorMap Colormap used to map the z value to colors. + default ``pg.colormap.get('viridis')`` edgecolors : dict, default None The color of the edges of the polygons. Default None means no edges. The dict may contains any arguments accepted by :func:`mkColor() `. - Example: - - ``mkPen(color='w', width=2)`` - + Example: ``mkPen(color='w', width=2)`` antialiasing : bool, default False Whether to draw edgelines with antialiasing. Note that if edgecolors is None, antialiasing is always False. @@ -107,7 +104,7 @@ def __init__(self, *args, **kwargs): self.x = None self.y = None self.z = None - + self.edgecolors = kwargs.get('edgecolors', None) self.antialiasing = kwargs.get('antialiasing', False) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index e3a8fea00d..8faace03af 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1080,7 +1080,7 @@ def getData(self): # compatbility method for access to dataRect for full dataset: def dataRect(self): """ - Returns a bounding rectangle (as :class:`QtGui.QRectF`) for the full set of data. + Returns a bounding rectangle (as :class:`QtCore.QRectF`) for the full set of data. Will return `None` if there is no data or if all values (x or y) are NaN. """ if self._dataset is None: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index d511688aba..1f4cc37bc9 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -35,7 +35,7 @@ class PlotItem(GraphicsWidget): This class provides the ViewBox-plus-axes that appear when using :func:`pg.plot() `, :class:`PlotWidget `, - and :func:`GraphicsLayoutWidget.addPlot() `. + and :func:`GraphicsLayout.addPlot() `. It's main functionality is: @@ -556,8 +556,8 @@ def addItem(self, item, *args, **kargs): self.legend.addItem(item, name=name) def listDataItems(self): - """Return a list of all data items (:class:`~pyqtgrpah.PlotDataItem`, - :class:`~pyqtgraph.PlotCurveItem`, :class:`~pyqtgraph.ScatterPlotItem`, etc) + """Return a list of all data items (:class:`~pyqtgrpah.PlotDataItem` , + :class:`~pyqtgraph.PlotCurveItem` , :class:`~pyqtgraph.ScatterPlotItem` , etc) contained in this PlotItem.""" return self.dataItems[:] @@ -645,7 +645,7 @@ def addLegend(self, offset=(30, 30), **kwargs): :class:`~pyqtgraph.ViewBox`. Plots added after this will be automatically displayed in the legend if they are created with a 'name' argument. - If a :class:`~pyqtGraph.LegendItem` has already been created using this method, + If a :class:`~pyqtgraph.LegendItem` has already been created using this method, that item will be returned rather than creating a new one. Accepts the same arguments as :func:`~pyqtgraph.LegendItem.__init__`. @@ -986,7 +986,7 @@ def downsampleMode(self): return ds, auto, method def setClipToView(self, clip): - """Set the default clip-to-view mode for all :class:`~pyqtgraph.PlotDataItem`s managed by this plot. + """Set the default clip-to-view mode for all :class:`~pyqtgraph.PlotDataItem` s managed by this plot. If *clip* is `True`, then PlotDataItems will attempt to draw only points within the visible range of the ViewBox.""" self.ctrl.clipToViewCheck.setChecked(clip) @@ -1186,21 +1186,24 @@ def showAxes(self, selection, showValues=True, size=False): Parameters ---------- - selection: boolean or tuple of booleans (left, top, right, bottom) + selection: bool or tuple of bool Determines which AxisItems will be displayed. + If in tuple form, order is (left, top, right, bottom) A single boolean value will set all axes, so that ``showAxes(True)`` configures the axes to draw a frame. - showValues: optional, boolean or tuple of booleans (left, top, right, bottom) + showValues: bool or tuple of bool, optional Determines if values will be displayed for the ticks of each axis. True value shows values for left and bottom axis (default). False shows no values. + If in tuple form, order is (left, top, right, bottom) None leaves settings unchanged. If not specified, left and bottom axes will be drawn with values. - size: optional, float or tuple of floats (width, height) + size: float or tuple of float, optional Reserves as fixed amount of space (width for vertical axis, height for horizontal axis) for each axis where tick values are enabled. If only a single float value is given, it will be applied for both width and height. If `None` is given instead of a float value, the axis reverts to automatic allocation of space. + If in tuple form, order is (width, height) """ if selection is True: # shortcut: enable all axes, creating a frame selection = (True, True, True, True) diff --git a/pyqtgraph/graphicsItems/TargetItem.py b/pyqtgraph/graphicsItems/TargetItem.py index d1e6eeeaaa..3f74e2eb0e 100644 --- a/pyqtgraph/graphicsItems/TargetItem.py +++ b/pyqtgraph/graphicsItems/TargetItem.py @@ -125,14 +125,14 @@ def setPos(self, *args): Parameters ---------- - args : tuple, list, QPointF, QPoint, pg.Point, or two floats + args : tuple or list or QtCore.QPointF or QtCore.QPoint or Point or float Two float values or a container that specifies ``(x, y)`` position where the TargetItem should be placed Raises ------ TypeError - If args cannot be used to instantiate a pg.Point + If args cannot be used to instantiate a Point """ try: newPos = Point(*args) @@ -303,7 +303,7 @@ def setLabel(self, text=None, labelOpts=None): displayed If a non-formatted string, then the text label will display ``text``, by default None - labelOpts : dictionary, optional + labelOpts : dict, optional These arguments are passed on to :class:`~pyqtgraph.TextItem` """ if not text: @@ -342,10 +342,11 @@ class TargetLabel(TextItem): offset : tuple or list or QPointF or QPoint Position to set the anchor of the TargetLabel away from the center of the target in pixels, by default it is (20, 0). - anchor : tuple, list, QPointF or QPoint + anchor : tuple or list or QPointF or QPoint Position to rotate the TargetLabel about, and position to set the offset value to see :class:`~pyqtgraph.TextItem` for more information. - kwargs : dict of arguments that are passed on to + kwargs : dict + kwargs contains arguments that are passed onto :class:`~pyqtgraph.TextItem` constructor, excluding text parameter """ diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 7ec7808462..0b6961bf4b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -76,7 +76,7 @@ class ViewBox(GraphicsWidget): **Bases:** :class:`GraphicsWidget ` Box that allows internal scaling/panning of children by mouse drag. - This class is usually created automatically as part of a :class:`PlotItem ` or :class:`Canvas ` or with :func:`GraphicsLayout.addViewBox() `. + This class is usually created automatically as part of a :class:`PlotItem ` or :ref:`Canvas ` or with :func:`GraphicsLayout.addViewBox() `. Features: diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 9d559fbcb0..6fead81833 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -17,12 +17,8 @@ import numpy as np -from .. import functions as fn -from ..Qt import QtCore, QtGui, QtWidgets - -from . import ImageViewTemplate_generic as ui_template - from .. import debug as debug +from .. import functions as fn from .. import getConfigOption from ..graphicsItems.GradientEditorItem import addGradientListToDocstring from ..graphicsItems.ImageItem import * @@ -31,7 +27,9 @@ from ..graphicsItems.ROI import * from ..graphicsItems.ViewBox import * from ..graphicsItems.VTickGroup import VTickGroup +from ..Qt import QtCore, QtGui, QtWidgets from ..SignalProxy import SignalProxy +from . import ImageViewTemplate_generic as ui_template try: from bottleneck import nanmax, nanmin @@ -850,7 +848,7 @@ def setColorMap(self, colormap): @addGradientListToDocstring() def setPredefinedGradient(self, name): - """Set one of the gradients defined in :class:`GradientEditorItem `. + """Set one of the gradients defined in :class:`GradientEditorItem`. Currently available gradients are: """ self.ui.histogram.gradient.loadPreset(name) diff --git a/pyqtgraph/opengl/items/GLGraphItem.py b/pyqtgraph/opengl/items/GLGraphItem.py index 9738620cf7..b09f28422a 100644 --- a/pyqtgraph/opengl/items/GLGraphItem.py +++ b/pyqtgraph/opengl/items/GLGraphItem.py @@ -1,5 +1,6 @@ from OpenGL.GL import * # noqa import numpy as np + from ... import functions as fn from ...Qt import QtCore, QtGui from ..GLGraphicsItem import GLGraphicsItem @@ -34,21 +35,21 @@ def setData(self, **kwds): 2D array of shape (M, 2) of connection data, each row contains indexes of two nodes that are connected. Dtype must be integer or unsigned. - edgeColor: QColor, array-like, optional. + edgeColor: QtGui.QColor | array-like, optional The color to draw edges. Accepts the same arguments as :func:`~pyqtgraph.mkColor()`. If None, no edges will be drawn. Default is (1.0, 1.0, 1.0, 0.5). - edgeWidth: float, optional. + edgeWidth: float, optional Value specifying edge width. Default is 1.0 nodePositions : np.ndarray 2D array of shape (N, 3), where each row represents the x, y, z coordinates for each node - nodeColor : np.ndarray, QColor, str or array like + nodeColor : np.ndarray | QColor | str | array-like 2D array of shape (N, 4) of dtype float32, where each row represents the R, G, B, A vakues in range of 0-1, or for the same color for all nodes, provide either QColor type or input for :func:`~pyqtgraph.mkColor()` - nodeSize : np.ndarray, float or int + nodeSize : np.ndarray | float | int Either 2D numpy array of shape (N, 1) where each row represents the size of each node, or if a scalar, apply the same size to all nodes **kwds diff --git a/pyqtgraph/parametertree/parameterTypes/basetypes.py b/pyqtgraph/parametertree/parameterTypes/basetypes.py index 7992f015f2..02af608f99 100644 --- a/pyqtgraph/parametertree/parameterTypes/basetypes.py +++ b/pyqtgraph/parametertree/parameterTypes/basetypes.py @@ -1,10 +1,10 @@ import builtins -from ..Parameter import Parameter -from ..ParameterItem import ParameterItem from ... import functions as fn from ... import icons from ...Qt import QtCore, QtGui, QtWidgets, mkQApp +from ..Parameter import Parameter +from ..ParameterItem import ParameterItem class WidgetParameterItem(ParameterItem): @@ -258,8 +258,8 @@ class SimpleParameter(Parameter): """ Parameter representing a single value. - This parameter is backed by :class:`WidgetParameterItem` to represent the - following parameter names through various subclasses: + This parameter is backed by :class:`~pyqtgraph.parametertree.parameterTypes.basetypes.WidgetParameterItem` + to represent the following parameter names through various subclasses: - 'int' - 'float' @@ -274,7 +274,7 @@ def __init__(self, *args, **kargs): Initialize the parameter. This is normally called implicitly through :meth:`Parameter.create`. - The keyword arguments avaialble to :meth:`Parameter.__init__` are + The keyword arguments available to :meth:`Parameter.__init__` are applicable. """ Parameter.__init__(self, *args, **kargs) From d7044c4c7c9f88705a58f99c0c3d33534d6e4f3a Mon Sep 17 00:00:00 2001 From: Etienne Dumur Date: Fri, 9 Sep 2022 09:47:46 +0200 Subject: [PATCH 029/306] Add pyplotter to the list of pyqtgraph user and sort the list --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ac978e0d36..096b09c65d 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Support ------- * Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues) -* Post questions to +* Post questions to * [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) * [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph) * [GitHub Discussions](https://github.com/pyqtgraph/pyqtgraph/discussions) @@ -121,20 +121,21 @@ Used By Here is a partial listing of some of the applications that make use of PyQtGraph! * [ACQ4](https://github.com/acq4/acq4) -* [Orange3](https://orangedatamining.com/) -* [neurotic](https://neurotic.readthedocs.io) -* [ephyviewer](https://ephyviewer.readthedocs.io) -* [Joulescope](https://www.joulescope.com/) -* [rapidtide](https://rapidtide.readthedocs.io/en/latest/) * [argos](https://github.com/titusjan/argos) -* [PySpectra](http://hasyweb.desy.de/services/computing/Spock/node138.html) -* [Semi-Supervised Semantic Annotator](https://gitlab.com/ficsresearch/s3ah) -* [PyMeasure](https://github.com/pymeasure/pymeasure) -* [Exo-Striker](https://github.com/3fon3fonov/exostriker) -* [HussariX](https://github.com/sem-geologist/HussariX) +* [Atomize](https://github.com/Anatoly1010/Atomize) * [EnMAP-Box](https://enmap-box.readthedocs.io) * [EO Time Series Viewer](https://eo-time-series-viewer.readthedocs.io) -* [Atomize](https://github.com/Anatoly1010/Atomize) +* [ephyviewer](https://ephyviewer.readthedocs.io) +* [Exo-Striker](https://github.com/3fon3fonov/exostriker) * [GraPhysio](https://github.com/jaj42/GraPhysio) +* [HussariX](https://github.com/sem-geologist/HussariX) +* [Joulescope](https://www.joulescope.com/) +* [neurotic](https://neurotic.readthedocs.io) +* [Orange3](https://orangedatamining.com/) +* [pyplotter](https://github.com/pyplotter/pyplotter) +* [PyMeasure](https://github.com/pymeasure/pymeasure) +* [PySpectra](http://hasyweb.desy.de/services/computing/Spock/node138.html) +* [rapidtide](https://rapidtide.readthedocs.io/en/latest/) +* [Semi-Supervised Semantic Annotator](https://gitlab.com/ficsresearch/s3ah) Do you use PyQtGraph in your own project, and want to add it to the list? Submit a pull request to update this listing! From fa42e2132e1a2ccb3bfa22ba0d1c8349f558fe5e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 9 Sep 2022 20:27:31 +0800 Subject: [PATCH 030/306] no need to specially disable test_loadUiType for conda --- tests/test_qt.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_qt.py b/tests/test_qt.py index 7790c9e320..94dc6307f3 100644 --- a/tests/test_qt.py +++ b/tests/test_qt.py @@ -13,11 +13,6 @@ def test_isQObjectAlive(): del o1 assert not pg.Qt.isQObjectAlive(o2) -@pytest.mark.skipif( - pg.Qt.QT_LIB == 'PySide2' - and not pg.Qt.PySide2.__version__ .startswith(pg.Qt.QtCore.__version__), - reason='test fails on conda distributions' -) @pytest.mark.skipif( pg.Qt.QT_LIB == "PySide2" and tuple(map(int, pg.Qt.PySide2.__version__.split("."))) >= (5, 14) From 4c46982796360c219538a2888943e5c194cce2fc Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 9 Sep 2022 21:49:42 -0700 Subject: [PATCH 031/306] Fixup more nit-pick errors --- doc/source/3dgraphics/index.rst | 2 +- doc/source/conf.py | 5 +- doc/source/config_options.rst | 2 + doc/source/flowchart/index.rst | 134 ++++++++++++++----- doc/source/graphicsItems/infiniteline.rst | 5 + doc/source/widgets/consolewidget.rst | 1 + pyqtgraph/__init__.py | 2 +- pyqtgraph/colormap.py | 57 ++++---- pyqtgraph/console/Console.py | 2 +- pyqtgraph/functions.py | 21 ++- pyqtgraph/graphicsItems/ColorBarItem.py | 12 +- pyqtgraph/graphicsItems/GraphicsLayout.py | 2 +- pyqtgraph/graphicsItems/GraphicsObject.py | 7 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 8 +- pyqtgraph/graphicsItems/ImageItem.py | 48 +++---- pyqtgraph/graphicsItems/InfiniteLine.py | 2 +- pyqtgraph/graphicsItems/PColorMeshItem.py | 6 +- pyqtgraph/graphicsItems/PlotDataItem.py | 10 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 6 +- pyqtgraph/opengl/GLGraphicsItem.py | 2 +- pyqtgraph/opengl/items/GLAxisItem.py | 2 +- pyqtgraph/opengl/items/GLGraphItem.py | 8 +- pyqtgraph/opengl/items/GLGridItem.py | 2 +- pyqtgraph/opengl/items/GLImageItem.py | 3 +- pyqtgraph/opengl/items/GLMeshItem.py | 2 +- pyqtgraph/opengl/items/GLVolumeItem.py | 2 +- 26 files changed, 222 insertions(+), 131 deletions(-) diff --git a/doc/source/3dgraphics/index.rst b/doc/source/3dgraphics/index.rst index 06bf048176..1678eb4bd5 100644 --- a/doc/source/3dgraphics/index.rst +++ b/doc/source/3dgraphics/index.rst @@ -2,7 +2,7 @@ PyQtGraph's 3D Graphics System ============================== The 3D graphics system in pyqtgraph is composed of a :class:`view widget ` and -several graphics items (all subclasses of :class:`GLGraphicsItem `) which +several graphics items (all subclasses of :class:`GLGraphicsItem `) which can be added to a view widget. **Note 1:** pyqtgraph.opengl is based on the deprecated OpenGL fixed-function pipeline. Although it is diff --git a/doc/source/conf.py b/doc/source/conf.py index f660ee3ec4..9b916e8bd7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -49,7 +49,10 @@ napoleon_preprocess_types = True napoleon_type_aliases = { - "callable": ":class:`collections.abc.Callable`" + "callable": ":class:`collections.abc.Callable`", + "np.ndarray": ":class:`numpy.ndarray`", + 'array_like': ':term:`array_like`', + 'color_like': ':func:`pyqtgraph.mkColor`' } # The encoding of source files. diff --git a/doc/source/config_options.rst b/doc/source/config_options.rst index c14743ffc0..6893995848 100644 --- a/doc/source/config_options.rst +++ b/doc/source/config_options.rst @@ -49,4 +49,6 @@ segmentedLineMode str 'auto' For 'on', lines are al .. autofunction:: pyqtgraph.setConfigOptions +.. autofunction:: pyqtgraph.setConfigOption + .. autofunction:: pyqtgraph.getConfigOption diff --git a/doc/source/flowchart/index.rst b/doc/source/flowchart/index.rst index 98f5680b38..cb1f5d56fb 100644 --- a/doc/source/flowchart/index.rst +++ b/doc/source/flowchart/index.rst @@ -1,14 +1,32 @@ Visual Programming with Flowcharts ================================== -PyQtGraph's flowcharts provide a visual programming environment similar in concept to LabView--functional modules are added to a flowchart and connected by wires to define a more complex and arbitrarily configurable algorithm. A small number of predefined modules (called Nodes) are included with pyqtgraph, but most flowchart developers will want to define their own library of Nodes. At their core, the Nodes are little more than 1) a python function 2) a list of input/output terminals, and 3) an optional widget providing a control panel for the Node. Nodes may transmit/receive any type of Python object via their terminals. - -One major limitation of flowcharts is that there is no mechanism for looping within a flowchart. (however individual Nodes may contain loops (they may contain any Python code at all), and an entire flowchart may be executed from within a loop). +PyQtGraph's flowcharts provide a visual programming environment similar in concept to +LabView--functional modules are added to a flowchart and connected by wires to define +a more complex and arbitrarily configurable algorithm. A small number of predefined +modules (called Nodes) are included with pyqtgraph, but most flowchart developers will +want to define their own library of Nodes. At their core, the Nodes are little more +than 1) a python function 2) a list of input/output terminals, and 3) an optional +widget providing a control panel for the Node. Nodes may transmit/receive any type of +Python object via their terminals. + +One major limitation of flowcharts is that there is no mechanism for looping within a +flowchart. (however individual Nodes may contain loops (they may contain any Python +code at all), and an entire flowchart may be executed from within a loop). There are two distinct modes of executing the code in a flowchart: -1. Provide data to the input terminals of the flowchart. This method is slower and will provide a graphical representation of the data as it passes through the flowchart. This is useful for debugging as it allows the user to inspect the data at each terminal and see where exceptions occurred within the flowchart. -2. Call :func:`Flowchart.process() `. This method does not update the displayed state of the flowchart and only retains the state of each terminal as long as it is needed. Additionally, Nodes which do not contribute to the output values of the flowchart (such as plotting nodes) are ignored. This mode allows for faster processing of large data sets and avoids memory issues which can occur if too much data is present in the flowchart at once (e.g., when processing image data through several stages). +1. Provide data to the input terminals of the flowchart. This method is slower and +will provide a graphical representation of the data as it passes through the +flowchart. This is useful for debugging as it allows the user to inspect the data +at each terminal and see where exceptions occurred within the flowchart. +2. Call :func:`Flowchart.process() `. This +method does not update the displayed state of the flowchart and only retains the +state of each terminal as long as it is needed. Additionally, Nodes which do not +contribute to the output values of the flowchart (such as plotting nodes) are +ignored. This mode allows for faster processing of large data sets and avoids memory +issues which can occur if too much data is present in the flowchart at once (e.g., +when processing image data through several stages). See the flowchart example for more information. @@ -24,9 +42,15 @@ API Reference: Basic Use --------- -Flowcharts are most useful in situations where you have a processing stage in your application that you would like to be arbitrarily configurable by the user. Rather than giving a pre-defined algorithm with parameters for the user to tweak, you supply a set of pre-defined functions and allow the user to arrange and connect these functions how they like. A very common example is the use of filter networks in audio / video processing applications. +Flowcharts are most useful in situations where you have a processing stage in your +application that you would like to be arbitrarily configurable by the user. Rather +than giving a pre-defined algorithm with parameters for the user to tweak, you supply +a set of pre-defined functions and allow the user to arrange and connect these +functions how they like. A very common example is the use of filter networks in +audio / video processing applications. -To begin, you must decide what the input and output variables will be for your flowchart. Create a flowchart with one terminal defined for each variable:: +To begin, you must decide what the input and output variables will be for your +flowchart. Create a flowchart with one terminal defined for each variable:: ## This example creates just a single input and a single output. ## Flowcharts may define any number of terminals, though. @@ -36,7 +60,11 @@ To begin, you must decide what the input and output variables will be for your f 'nameOfOutputTerminal': {'io': 'out'} }) -In the example above, each terminal is defined by a dictionary of options which define the behavior of that terminal (see :func:`Terminal.__init__() ` for more information and options). Note that Terminals are not typed; any python object may be passed from one Terminal to another. +In the example above, each terminal is defined by a dictionary of options which define +the behavior of that terminal (see +:func:`Terminal.__init__() ` for more +information and options). Note that Terminals are not typed; any python object may be +passed from one Terminal to another. Once the flowchart is created, add its control widget to your application:: @@ -50,36 +78,60 @@ The control widget provides several features: * Provides access to the flowchart design window via the 'flowchart' button * Interface for saving / restoring flowcharts to disk. -At this point your user has the ability to generate flowcharts based on the built-in node library. It is recommended to provide a default set of flowcharts for your users to build from. +At this point your user has the ability to generate flowcharts based on the built-in +node library. It is recommended to provide a default set of flowcharts for your users +to build from. -All that remains is to process data through the flowchart. As noted above, there are two ways to do this: +All that remains is to process data through the flowchart. As noted above, there are +two ways to do this: .. _processing methods: -1. Set the values of input terminals with :func:`Flowchart.setInput() `, then read the values of output terminals with :func:`Flowchart.output() `:: - +1. Set the values of input terminals with + :func:`Flowchart.setInput() `, then read + the values of output terminals with + :func:`Flowchart.output() `:: + fc.setInput(nameOfInputTerminal=newValue) output = fc.output() # returns {terminalName:value} - - This method updates all of the values displayed in the flowchart design window, allowing the user to inspect values at all terminals in the flowchart and indicating the location of errors that occurred during processing. + + This method updates all of the values displayed in the flowchart design window, + allowing the user to inspect values at all terminals in the flowchart and + indicating the location of errors that occurred during processing. 2. Call :func:`Flowchart.process() `:: - + output = fc.process(nameOfInputTerminal=newValue) - - This method processes data without updating any of the displayed terminal values. Additionally, all :func:`Node.process() ` methods are called with display=False to request that they not invoke any custom display code. This allows data to be processed both more quickly and with a smaller memory footprint, but errors that occur during Flowchart.process() will be more difficult for the user to diagnose. It is thus recommended to use this method for batch processing through flowcharts that have already been tested and debugged with method 1. + + This method processes data without updating any of the displayed terminal values. + Additionally, all :func:`Node.process() ` methods + are called with display=False to request that they not invoke any custom display + code. This allows data to be processed both more quickly and with a smaller memory + footprint, but errors that occur during Flowchart.process() will be more difficult + for the user to diagnose. It is thus recommended to use this method for batch + processing through flowcharts that have already been tested and debugged with + method 1. Implementing Custom Nodes ------------------------- -PyQtGraph includes a small library of built-in flowchart nodes. This library is intended to cover some of the most commonly-used functions as well as provide examples for some more exotic Node types. Most applications that use the flowchart system will find the built-in library insufficient and will thus need to implement custom Node classes. +PyQtGraph includes a small library of built-in flowchart nodes. This library is +intended to cover some of the most commonly-used functions as well as provide examples +for some more exotic Node types. Most applications that use the flowchart system will +find the built-in library insufficient and will thus need to implement custom Node +classes. A node subclass implements at least: - + 1) A list of input / output terminals and their properties -2) A :func:`process() ` function which takes the names of input terminals as keyword arguments and returns a dict with the names of output terminals as keys. +2) A :func:`process() ` function which takes the + names of input terminals as keyword arguments and returns a dict with the names of + output terminals as keys. + +Optionally, a Node subclass can implement the +:func:`ctrlWidget() ` method, which must return +a QWidget (usually containing other widgets) that will be displayed in the flowchart +control panel. A minimal Node subclass looks like:: -Optionally, a Node subclass can implement the :func:`ctrlWidget() ` method, which must return a QWidget (usually containing other widgets) that will be displayed in the flowchart control panel. A minimal Node subclass looks like:: - class SpecialFunctionNode(Node): """SpecialFunction: short description @@ -106,16 +158,28 @@ Optionally, a Node subclass can implement the :func:`ctrlWidget() ` subclass allows you +to instead define a simple data structure that CtrlNode will use to automatically +generate the control widget. This is used in many of the built-in library nodes +(especially the filters). + +There are many other optional parameters for nodes and terminals -- whether the user +is allowed to add/remove/rename terminals, whether one terminal may be connected to +many others or just one, etc. See the documentation on the +:class:`~pyqtgraph.flowchart.Node` and :class:`~pyqtgraph.flowchart.Terminal` +classes for more details. + +After implementing a new Node subclass, you will most likely want to register the +class so that it appears in the menu of Nodes the user can select from:: import pyqtgraph.flowchart.library as fclib fclib.registerNodeType(SpecialFunctionNode, [('Category', 'Sub-Category')]) -The second argument to registerNodeType is a list of tuples, with each tuple describing a menu location in which SpecialFunctionNode should appear. +The second argument to registerNodeType is a list of tuples, with each tuple +describing a menu location in which SpecialFunctionNode should appear. See the FlowchartCustomNode example for more information. @@ -123,13 +187,23 @@ See the FlowchartCustomNode example for more information. Debugging Custom Nodes ^^^^^^^^^^^^^^^^^^^^^^ -When designing flowcharts or custom Nodes, it is important to set the input of the flowchart with data that at least has the same types and structure as the data you intend to process (see `processing methods`_ #1 above). When you use :func:`Flowchart.setInput() `, the flowchart displays visual feedback in its design window that can tell you what data is present at any terminal and whether there were errors in processing. Nodes that generated errors are displayed with a red border. If you select a Node, its input and output values will be displayed as well as the exception that occurred while the node was processing, if any. +When designing flowcharts or custom Nodes, it is important to set the input of the +flowchart with data that at least has the same types and structure as the data you +intend to process (see `processing methods`_ #1 above). When you use +:func:`Flowchart.setInput() `, the flowchart +displays visual feedback in its design window that can tell you what data is present +at any terminal and whether there were errors in processing. Nodes that generated +errors are displayed with a red border. If you select a Node, its input and output +values will be displayed as well as the exception that occurred while the node was +processing, if any. Using Nodes Without Flowcharts ------------------------------ -Flowchart Nodes implement a very useful generalization in data processing by combining a function with a GUI for configuring that function. This generalization is useful even outside the context of a flowchart. For example:: +Flowchart Nodes implement a very useful generalization in data processing by combining +a function with a GUI for configuring that function. This generalization is useful +even outside the context of a flowchart. For example:: ## We defined a useful filter Node for use in flowcharts, but would like to ## re-use its processing code and GUI without having a flowchart present. diff --git a/doc/source/graphicsItems/infiniteline.rst b/doc/source/graphicsItems/infiniteline.rst index e438d54ea1..e96ac24530 100644 --- a/doc/source/graphicsItems/infiniteline.rst +++ b/doc/source/graphicsItems/infiniteline.rst @@ -5,3 +5,8 @@ InfiniteLine :members: .. automethod:: pyqtgraph.InfiniteLine.__init__ + +.. autoclass:: pyqtgraph.InfLineLabel + :members: + + .. automethod:: pyqtgraph.InfLineLabel.__init__ diff --git a/doc/source/widgets/consolewidget.rst b/doc/source/widgets/consolewidget.rst index 580dae3eec..961a5a056f 100644 --- a/doc/source/widgets/consolewidget.rst +++ b/doc/source/widgets/consolewidget.rst @@ -4,3 +4,4 @@ ConsoleWidget .. autoclass:: pyqtgraph.console.ConsoleWidget :members: + .. automethod:: pyqtgraph.console.ConsoleWidget.__init__ diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index ae932f2553..fba4caf55f 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -393,7 +393,7 @@ def exit(): def plot(*args, **kargs): """ - Create and return a :class:`PlotWidget ` + Create and return a :class:`PlotWidget ` Accepts a *title* argument to set the title of the window. All other arguments are used to plot data. (see :func:`PlotItem.plot() `) """ diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 68f7185ff1..413c4436e1 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -378,9 +378,10 @@ def __init__(self, pos, color, mapping=CLIP, mode=None, linearize=False, name='' Parameters ---------- - pos: array_like of float in range 0 to 1, or None + pos: array_like of float, optional Assigned positions of specified colors. `None` sets equal spacing. - color: array_like of colors + Values need to be in range 0.0-1.0. + color: array_like of color_like List of colors, interpreted via :func:`mkColor() `. mapping: str or int, optional Controls how values outside the 0 to 1 range are mapped to colors. @@ -481,16 +482,18 @@ def getSubset(self, start, span): Parameters ---------- - start : float (0.0 to 1.0) + start : float Starting value that defines the 0.0 value of the new color map. - span : float (-1.0 to 1.0) - span of the extracted region. The orignal color map will be trated as cyclical - if the extracted interval exceeds the 0.0 to 1.0 range. + Possible value between 0.0 to 1.0 + span : float + Span of the extracted region. The original color map will be + treated as cyclical if the extracted interval exceeds the + 0.0 to 1.0 range. Possible values between -1.0 to 1.0. """ pos, col = self.getStops( mode=ColorMap.FLOAT ) start = clip_scalar(start, 0.0, 1.0) span = clip_scalar(span, -1.0, 1.0) - + if span == 0.0: raise ValueError("'length' needs to be non-zero") stop = (start + span) @@ -561,14 +564,14 @@ def map(self, data, mode=BYTE): Returns ------- - array of color.dtype + np.ndarray of {``ColorMap.BYTE``, ``ColorMap.FLOAT``, QColor} for `ColorMap.BYTE` or `ColorMap.FLOAT`: RGB values for each `data` value, arranged in the same shape as `data`. - list of QColor objects + list of QColor for `ColorMap.QCOLOR`: - Colors for each `data` value as Qcolor objects. + Colors for each `data` value as QColor objects. """ if isinstance(mode, str): mode = self.enumMap[mode.lower()] @@ -624,7 +627,7 @@ def getByIndex(self, idx): def getGradient(self, p1=None, p2=None): """ Returns a QtGui.QLinearGradient corresponding to this ColorMap. - The span and orientiation is given by two points in plot coordinates. + The span and orientation is given by two points in plot coordinates. When no parameters are given for `p1` and `p2`, the gradient is mapped to the `y` coordinates 0 to 1, unless the color map is defined for a more limited range. @@ -634,11 +637,11 @@ def getGradient(self, p1=None, p2=None): Parameters ---------- - p1: QtCore.QPointF, default (0,0) - Starting point (value 0) of the gradient. - p2: QtCore.QPointF, default (dy,0) + p1: QtCore.QPointF, optional + Starting point (value 0) of the gradient. Default value is QPointF(0., 0.) + p2: QtCore.QPointF, optional End point (value 1) of the gradient. Default parameter `dy` is the span of ``max(pos) - min(pos)`` - over which the color map is defined, typically `dy=1`. + over which the color map is defined, typically `dy=1`. Default is QPointF(dy, 0.) """ if p1 is None: p1 = QtCore.QPointF(0,0) @@ -669,14 +672,16 @@ def getBrush(self, span=(0.,1.), orientation='vertical'): Parameters ---------- - span : tuple (min, max), default (0.0, 1.0) + span : tuple of float, optional Span of data values covered by the gradient: - Color map value 0.0 will appear at `min`, - Color map value 1.0 will appear at `max`. + + Default value is (0., 1.) orientation : str, default 'vertical' - Orientiation of the gradient: + Orientation of the gradient: - 'vertical': `span` corresponds to the `y` coordinate. - 'horizontal': `span` corresponds to the `x` coordinate. @@ -698,17 +703,18 @@ def getPen(self, span=(0.,1.), orientation='vertical', width=1.0): Parameters ---------- - span : tuple (min, max), default (0.0, 1.0) + span : tuple of float Span of the data values covered by the gradient: - Color map value 0.0 will appear at `min`. - Color map value 1.0 will appear at `max`. + Default is (0., 1.) orientation : str, default 'vertical' - Orientiation of the gradient: + Orientation of the gradient: - 'vertical' creates a vertical gradient, where `span` corresponds to the `y` coordinate. - - 'horizontal' creates a horizontal gradient, where `span` correspnds to the `x` coordinate. + - 'horizontal' creates a horizontal gradient, where `span` corresponds to the `x` coordinate. width : int or float Width of the pen in pixels on screen. @@ -775,23 +781,24 @@ def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode=BYTE): The starting value in the lookup table stop: float, default=1.0 The final value in the lookup table - nPts: int, default is 512 + nPts: int, default=512 The number of points in the returned lookup table. - alpha: True, False, or None + alpha: bool, optional Specifies whether or not alpha values are included in the table. If alpha is None, it will be automatically determined. - mode: int or str, default is `ColorMap.BYTE` + mode: int or str, default='byte' Determines return type as described in :func:`map() `, can be either `ColorMap.BYTE` (0 to 255), `ColorMap.FLOAT` (0.0 to 1.0) or `ColorMap.QColor`. Returns ------- - array of color.dtype + np.ndarray of {``ColorMap.BYTE``, ``ColorMap.FLOAT``} for `ColorMap.BYTE` or `ColorMap.FLOAT`: RGB values for each `data` value, arranged in the same shape as `data`. If alpha values are included the array has shape (`nPts`, 4), otherwise (`nPts`, 3). - list of QColor objects + + list of QColor for `ColorMap.QCOLOR`: Colors for each `data` value as QColor objects. diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index b8f275f1ec..06f19e0e61 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -36,7 +36,7 @@ class ConsoleWidget(QtWidgets.QWidget): def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None): """ - ============== ============================================================================ + ============== ============================================================================= **Arguments:** namespace dictionary containing the initial variables present in the default namespace historyFile optional file for storing command history diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 96a45121fe..6e1b569eed 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -16,8 +16,7 @@ import numpy as np -from . import Qt, debug, reload -from . import getConfigOption +from . import Qt, debug, getConfigOption, reload from .metaarray import MetaArray from .Qt import QT_LIB, QtCore, QtGui from .util.cupy_helper import getCupy @@ -501,7 +500,7 @@ def colorCIELab(qcol): Returns ------- - NumPy array + np.ndarray Color coordinates `[L, a, b]`. """ srgb = qcol.getRgbF()[:3] # get sRGB values from QColor @@ -536,7 +535,7 @@ def colorDistance(colors, metric='CIE76'): ---------- colors: list of QColor Two or more colors to calculate the distances between. - metric: string, optional + metric: str, optional Metric used to determined the difference. Only 'CIE76' is supported at this time, where a distance of 2.3 is considered a "just noticeable difference". The default may change as more metrics become available. @@ -1329,8 +1328,8 @@ def applyLookupTable(data, lut): Parameters ---------- - data : ndarray - lut : ndarray + data : np.ndarray + lut : np.ndarray Either cupy or numpy arrays are accepted, though this function has only consistently behaved correctly on windows with cuda toolkit version >= 11.1. """ @@ -2050,17 +2049,17 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True): Parameters ---------- - x : (N,) ndarray - x-values to be plotted - y : (N,) ndarray - y-values to be plotted, must be same length as `x` + x : np.ndarray + x-values to be plotted of shape (N,) + y : np.ndarray + y-values to be plotted, must be same length as `x` of shape (N,) connect : {'all', 'pairs', 'finite', (N,) ndarray}, optional Argument detailing how to connect the points in the path. `all` will have sequential points being connected. `pairs` generates lines between every other point. `finite` only connects points that are finite. If an ndarray is passed, containing int32 values of 0 or 1, only values with 1 will connect to the previous point. Def - finiteCheck : bool, default Ture + finiteCheck : bool, default True When false, the check for finite values will be skipped, which can improve performance. If nonfinite values are present in `x` or `y`, an empty QPainterPath will be generated. diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py index a226ebfe9e..bb1fbc34a1 100644 --- a/pyqtgraph/graphicsItems/ColorBarItem.py +++ b/pyqtgraph/graphicsItems/ColorBarItem.py @@ -52,23 +52,23 @@ def __init__(self, values=(0,1), width=25, colorMap=None, label=None, Determines the color map displayed and applied to assigned ImageItem(s). values: tuple of float The range of image levels covered by the color bar, as ``(min, max)``. - width: float, default=25 + width: float, default=25.0 The width of the displayed color bar. label: str, optional Label applied to the color bar axis. interactive: bool, default=True If `True`, handles are displayed to interactively adjust the level range. - limits: `None` or `tuple of float` + limits: `tuple of float`, optional Limits the adjustment range to `(low, high)`, `None` disables the limit. rounding: float, default=1 Adjusted range values are rounded to multiples of this value. orientation: str, default 'vertical' 'horizontal' or 'h' gives a horizontal color bar instead of the default vertical bar - pen: :class:`Qpen` or argument to :func:`~pyqtgraph.mkPen` + pen: :class:`QPen` or color_like Sets the color of adjustment handles in interactive mode. - hoverPen: :class:`QPen` or argument to :func:`~pyqtgraph.mkPen` - Sets the color of adjustement handles when hovered over. - hoverBrush: :class:`QBrush` or argument to :func:`~pyqtgraph.mkBrush` + hoverPen: :class:`QPen` or color_like + Sets the color of adjustment handles when hovered over. + hoverBrush: :class:`QBrush` or color_like Sets the color of movable center region when hovered over. """ super().__init__() diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index 6d2bddb224..f76041c084 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -153,7 +153,7 @@ def itemIndex(self, item): Parameters ---------- - item : pyqtgraph.Qt.QtWidgets.QGraphicsLayoutItem + item : QGraphicsLayoutItem Item to query the index position of Returns diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index 663cd0175d..4f6f76b92c 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -1,13 +1,12 @@ -from ..Qt import QtCore, QtWidgets, QT_LIB - +from ..Qt import QT_LIB, QtCore, QtWidgets from .GraphicsItem import GraphicsItem __all__ = ['GraphicsObject'] class GraphicsObject(GraphicsItem, QtWidgets.QGraphicsObject): """ - **Bases:** :class:`GraphicsItem `, :class:`QtWidgets.QGraphicsObject` + **Bases:** :class:`GraphicsItem `, :class:`QtWidgets.QGraphicsObject` - Extension of QGraphicsObject with some useful methods (provided by :class:`GraphicsItem `) + Extension of QGraphicsObject with some useful methods (provided by :class:`GraphicsItem `) """ _qtBaseClass = QtWidgets.QGraphicsObject def __init__(self, *args): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index fbf37e043e..11d581d233 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -64,11 +64,11 @@ class HistogramLUTItem(GraphicsWidget): Attributes ---------- - sigLookupTableChanged : signal + sigLookupTableChanged : QtCore.Signal Emits the HistogramLUTItem itself when the gradient changes - sigLevelsChanged : signal + sigLevelsChanged : QtCore.Signal Emits the HistogramLUTItem itself while the movable region is changing - sigLevelChangeFinished : signal + sigLevelChangeFinished : QtCore.Signal Emits the HistogramLUTItem itself when the movable region is finished changing See Also @@ -195,7 +195,7 @@ def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): level : float, optional Set the fill level. See :meth:`PlotCurveItem.setFillLevel `. Only used if ``fill`` is True. - color : color, optional + color : color_like, optional Color to use for the fill when the histogram ``levelMode == "mono"``. See :meth:`PlotCurveItem.setBrush `. """ diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 35f2b70b82..3050bd12e0 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -3,6 +3,7 @@ import numpy +from .. import colormap from .. import debug as debug from .. import functions as fn from .. import getConfigOption @@ -10,7 +11,6 @@ from ..Qt import QtCore, QtGui, QtWidgets from ..util.cupy_helper import getCupy from .GraphicsObject import GraphicsObject -from .. import colormap translate = QtCore.QCoreApplication.translate @@ -32,7 +32,7 @@ def __init__(self, image=None, **kargs): Parameters ---------- - image: array + image: np.ndarray, optional Image data """ GraphicsObject.__init__(self) @@ -104,7 +104,7 @@ def setCompositionMode(self, mode): def setBorder(self, b): """ Defines the border drawn around the image. Accepts all arguments supported by - :func:`~pyqtgraph.functions.mkPen`. + :func:`~pyqtgraph.mkPen`. """ self.border = fn.mkPen(b) self.update() @@ -138,7 +138,7 @@ def setLevels(self, levels, update=True): Parameters ---------- - levels: list_like + levels: array_like - ``[blackLevel, whiteLevel]`` sets black and white levels for monochrome data and can be used with a lookup table. - ``[[minR, maxR], [minG, maxG], [minB, maxB]]`` @@ -253,18 +253,21 @@ def setOpts(self, update=True, **kargs): See :func:`~pyqtgraph.ImageItem.setCompositionMode` colorMap: :class:`~pyqtgraph.ColorMap` or `str` Sets a color map. A string will be passed to :func:`colormap.get() ` - lut: array + lut: array_like Sets a color lookup table to use when displaying the image. See :func:`~pyqtgraph.ImageItem.setLookupTable`. - levels: list_like, usally [`min`, `max`] - Sets minimum and maximum values to use when rescaling the image data. By default, these will be set to - the estimated minimum and maximum values in the image. If the image array has dtype uint8, no rescaling - is necessary. See :func:`~pyqtgraph.ImageItem.setLevels`. - opacity: float, 0.0-1.0 - Overall opacity for an RGB image. - rect: :class:`QRectF`, :class:`QRect` or array_like of floats (`x`,`y`,`w`,`h`) - Displays the current image within the specified rectangle in plot coordinates. - See :func:`~pyqtgraph.ImageItem.setRect`. + levels: array_like + Shape of (min, max). Sets minimum and maximum values to use when + rescaling the image data. By default, these will be set to the + estimated minimum and maximum values in the image. If the image array + has dtype uint8, no rescaling is necessary. See + :func:`~pyqtgraph.ImageItem.setLevels`. + opacity: float + Overall opacity for an RGB image. Between 0.0-1.0. + rect: :class:`QRectF`, :class:`QRect` or array_like + Displays the current image within the specified rectangle in plot + coordinates. If ``array_like``, should be of the of ``floats + (`x`,`y`,`w`,`h`)`` . See :func:`~pyqtgraph.ImageItem.setRect`. update : bool, optional Controls if image immediately updates to reflect the new options. """ @@ -351,22 +354,21 @@ def setImage(self, image=None, autoLevels=None, **kargs): imageitem.setImage(imagedata.T) or the interpretation of the data can be changed locally through the ``axisOrder`` keyword or by changing the - `imageAxisOrder` :ref:`global configuration option `. + `imageAxisOrder` :ref:`global configuration option ` All keywords supported by :func:`~pyqtgraph.ImageItem.setOpts` are also allowed here. Parameters ---------- - image: array + image: np.ndarray, optional Image data given as NumPy array with an integer or floating point dtype of any bit depth. A 2-dimensional array describes single-valued (monochromatic) data. A 3-dimensional array is used to give individual color components. The third dimension must be of length 3 (RGB) or 4 (RGBA). - - rect: QRectF, QRect or list_like of floats ``[x, y, w, h]``, optional - If given, sets translation and scaling to display the image within the specified rectangle. See - :func:`~pyqtgraph.ImageItem.setRect`. - + rect: QRectF or QRect or array_like, optional + If given, sets translation and scaling to display the image within the + specified rectangle. If ``array_like`` should be the form of floats + ``[x, y, w, h]`` See :func:`~pyqtgraph.ImageItem.setRect` autoLevels: bool, optional If `True`, ImageItem will automatically select levels based on the maximum and minimum values encountered in the data. For performance reasons, this search subsamples the images and may miss individual bright or @@ -375,12 +377,10 @@ def setImage(self, image=None, autoLevels=None, **kargs): If `False`, the search will be omitted. The default is `False` if a ``levels`` keyword argument is given, and `True` otherwise. - levelSamples: int, default 65536 When determining minimum and maximum values, ImageItem only inspects a subset of pixels no larger than this number. Setting this larger than the total number of pixels considers all values. - """ profile = debug.Profiler() @@ -898,7 +898,7 @@ def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSi dimensions approximating `targetImageSize` for each axis. The `bins` argument and any extra keyword arguments are passed to - :func:`self.xp.histogram()`. If `bins` is `auto`, a bin number is automatically + :func:`numpy.histogram()`. If `bins` is `auto`, a bin number is automatically chosen based on the image characteristics: * Integer images will have approximately `targetHistogramSize` bins, diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index ba543ea328..04cacbbb84 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -58,7 +58,7 @@ def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, None to show no label (default is None). May optionally include formatting strings to display the line value. labelOpts A dict of keyword arguments to use when constructing the - text label. See :class:`InfLineLabel`. + text label. See :class:`InfLineLabel `. span Optional tuple (min, max) giving the range over the view to draw the line. For example, with a vertical line, use span=(0.5, 1) to draw only on the top half of the view. diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index 5bfc0b8990..d1ddbb27c6 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -85,10 +85,10 @@ def __init__(self, *args, **kwargs): (x[i, j], y[i, j]) (x[i, j+1], y[i, j+1]) "ASCII from: ". - colorMap : pg.ColorMap + colorMap : pyqtgraph.ColorMap Colormap used to map the z value to colors. - default ``pg.colormap.get('viridis')`` - edgecolors : dict, default None + default ``pyqtgraph.colormap.get('viridis')`` + edgecolors : dict, optional The color of the edges of the polygons. Default None means no edges. The dict may contains any arguments accepted by :func:`mkColor() `. diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 8faace03af..5d3f5bc7f0 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -505,7 +505,7 @@ def setShadowPen(self, *args, **kargs): def setFillBrush(self, *args, **kargs): """ Sets the :class:`QtGui.QBrush` used to fill the area under the curve. - See :func:`mkBrush() `) for arguments. + See :func:`mkBrush() `) for arguments. """ if args[0] is None: brush = None @@ -518,7 +518,7 @@ def setFillBrush(self, *args, **kargs): def setBrush(self, *args, **kargs): """ - See :func:`setFillBrush() `) for arguments. + See :func:`mkPen() `) for arguments. """ pen = fn.mkPen(*args, **kargs) if self.opts['symbolPen'] == pen: @@ -558,7 +558,7 @@ def setSymbolPen(self, *args, **kargs): def setSymbolBrush(self, *args, **kargs): """ Sets the :class:`QtGui.QBrush` used to fill symbols. - See :func:`mkBrush() `) for arguments. + See :func:`mkBrush() `) for arguments. """ brush = fn.mkBrush(*args, **kargs) if self.opts['symbolBrush'] == brush: @@ -910,7 +910,7 @@ def getOriginalDataset(self): def getDisplayDataset(self): """ - Returns a :class:`PlotDataset ` object that contains data suitable for display + Returns a :class:`~.PlotDataset` object that contains data suitable for display (after mapping and data reduction) as ``dataset.x`` and ``dataset.y``. Intended for internal use. """ diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 1f4cc37bc9..49db63ef3a 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -505,8 +505,8 @@ def viewStateChanged(self): def addItem(self, item, *args, **kargs): """ Add a graphics item to the view box. - If the item has plot data (:class:`~pyqtgrpah.PlotDataItem`, - :class:`~pyqtgraph.PlotCurveItem`, :class:`~pyqtgraph.ScatterPlotItem`), + If the item has plot data (:class:`PlotDataItem ` , + :class:`~pyqtgraph.PlotCurveItem` , :class:`~pyqtgraph.ScatterPlotItem` ), it may be included in analysis performed by the PlotItem. """ if item in self.items: @@ -556,7 +556,7 @@ def addItem(self, item, *args, **kargs): self.legend.addItem(item, name=name) def listDataItems(self): - """Return a list of all data items (:class:`~pyqtgrpah.PlotDataItem` , + """Return a list of all data items (:class:`PlotDataItem `, :class:`~pyqtgraph.PlotCurveItem` , :class:`~pyqtgraph.ScatterPlotItem` , etc) contained in this PlotItem.""" return self.dataItems[:] diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index df9d4153c5..b546912b90 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -135,7 +135,7 @@ def depthValue(self): def setTransform(self, tr): """Set the local transform for this object. - Must be a :class:`Transform3D ` instance. This transform + Must be a :class:`Transform3D ` instance. This transform determines how the local coordinate system of the item is mapped to the coordinate system of its parent.""" self.__transform = Transform3D(tr) diff --git a/pyqtgraph/opengl/items/GLAxisItem.py b/pyqtgraph/opengl/items/GLAxisItem.py index 98331dfa84..44a66fc329 100644 --- a/pyqtgraph/opengl/items/GLAxisItem.py +++ b/pyqtgraph/opengl/items/GLAxisItem.py @@ -6,7 +6,7 @@ class GLAxisItem(GLGraphicsItem): """ - **Bases:** :class:`GLGraphicsItem ` + **Bases:** :class:`GLGraphicsItem ` Displays three lines indicating origin and orientation of local coordinate system. diff --git a/pyqtgraph/opengl/items/GLGraphItem.py b/pyqtgraph/opengl/items/GLGraphItem.py index b09f28422a..e5e3f875b7 100644 --- a/pyqtgraph/opengl/items/GLGraphItem.py +++ b/pyqtgraph/opengl/items/GLGraphItem.py @@ -35,7 +35,7 @@ def setData(self, **kwds): 2D array of shape (M, 2) of connection data, each row contains indexes of two nodes that are connected. Dtype must be integer or unsigned. - edgeColor: QtGui.QColor | array-like, optional + edgeColor: color_like, optional The color to draw edges. Accepts the same arguments as :func:`~pyqtgraph.mkColor()`. If None, no edges will be drawn. Default is (1.0, 1.0, 1.0, 0.5). @@ -44,12 +44,12 @@ def setData(self, **kwds): nodePositions : np.ndarray 2D array of shape (N, 3), where each row represents the x, y, z coordinates for each node - nodeColor : np.ndarray | QColor | str | array-like + nodeColor : np.ndarray or float or color_like, optional 2D array of shape (N, 4) of dtype float32, where each row represents - the R, G, B, A vakues in range of 0-1, or for the same color for all + the R, G, B, A values in range of 0-1, or for the same color for all nodes, provide either QColor type or input for :func:`~pyqtgraph.mkColor()` - nodeSize : np.ndarray | float | int + nodeSize : np.ndarray or float or int Either 2D numpy array of shape (N, 1) where each row represents the size of each node, or if a scalar, apply the same size to all nodes **kwds diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 9cde7dec71..59436ec0be 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -9,7 +9,7 @@ class GLGridItem(GLGraphicsItem): """ - **Bases:** :class:`GLGraphicsItem ` + **Bases:** :class:`GLGraphicsItem ` Displays a wire-frame grid. """ diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 19109bc41c..460055d946 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -1,12 +1,13 @@ from OpenGL.GL import * # noqa import numpy as np + from ..GLGraphicsItem import GLGraphicsItem __all__ = ['GLImageItem'] class GLImageItem(GLGraphicsItem): """ - **Bases:** :class:`GLGraphicsItem ` + **Bases:** :class:`GLGraphicsItem ` Displays image data as a textured quad. """ diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index 883b3ee5b8..9a3f4de67b 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -10,7 +10,7 @@ class GLMeshItem(GLGraphicsItem): """ - **Bases:** :class:`GLGraphicsItem ` + **Bases:** :class:`GLGraphicsItem ` Displays a 3D triangle mesh. """ diff --git a/pyqtgraph/opengl/items/GLVolumeItem.py b/pyqtgraph/opengl/items/GLVolumeItem.py index 6dfb96e589..2fd73cb0d9 100644 --- a/pyqtgraph/opengl/items/GLVolumeItem.py +++ b/pyqtgraph/opengl/items/GLVolumeItem.py @@ -8,7 +8,7 @@ class GLVolumeItem(GLGraphicsItem): """ - **Bases:** :class:`GLGraphicsItem ` + **Bases:** :class:`GLGraphicsItem ` Displays volumetric data. """ From 4c4006d511f124695a972f14dbac428bed5b756e Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 13 Sep 2022 20:09:44 -0700 Subject: [PATCH 032/306] Fix up a few more nitpick warnings --- doc/source/apireference.rst | 1 + doc/source/conf.py | 4 ++++ pyqtgraph/graphicsItems/PlotDataItem.py | 10 +++++----- pyqtgraph/imageview/ImageView.py | 8 ++++---- pyqtgraph/opengl/GLGraphicsItem.py | 9 ++++++--- tests/graphicsItems/test_PlotDataItem.py | 4 ++-- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index 6a3d028323..0c279013f1 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -17,3 +17,4 @@ Contents: graphicsscene/index flowchart/index point + transform3d diff --git a/doc/source/conf.py b/doc/source/conf.py index 9b916e8bd7..6211e59197 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -47,6 +47,10 @@ 'numpy': ('https://numpy.org/doc/stable/', None) } +nitpick_ignore_regex = [ + ("py:class", r"re\.Pattern"), # doesn't seem to be a good ref in python docs +] + napoleon_preprocess_types = True napoleon_type_aliases = { "callable": ":class:`collections.abc.Callable`", diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 5d3f5bc7f0..d9843864ac 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -811,8 +811,8 @@ def setData(self, *args, **kargs): self._dataset = None else: self._dataset = PlotDataset( xData, yData ) - self._datasetMapped = None # invalidata mapped data , will be generated in getData() / getDisplayDataset() - self._datasetDisplay = None # invalidate display data, will be generated in getData() / getDisplayDataset() + self._datasetMapped = None # invalidata mapped data , will be generated in getData() / _getDisplayDataset() + self._datasetDisplay = None # invalidate display data, will be generated in getData() / _getDisplayDataset() profiler('set data') @@ -862,7 +862,7 @@ def updateItems(self, styleUpdate=True): if k in self.opts: scatterArgs[v] = self.opts[k] - dataset = self.getDisplayDataset() + dataset = self._getDisplayDataset() if dataset is None: # then we have nothing to show self.curve.hide() self.scatter.hide() @@ -908,7 +908,7 @@ def getOriginalDataset(self): return (None, None) return dataset.x, dataset.y - def getDisplayDataset(self): + def _getDisplayDataset(self): """ Returns a :class:`~.PlotDataset` object that contains data suitable for display (after mapping and data reduction) as ``dataset.x`` and ``dataset.y``. @@ -1072,7 +1072,7 @@ def getData(self): """ Returns the displayed data as the tuple (`xData`, `yData`) after mapping and data reduction. """ - dataset = self.getDisplayDataset() + dataset = self._getDisplayDataset() if dataset is None: return (None, None) return dataset.x, dataset.y diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5022fbce5b..47b2d01ae1 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -255,20 +255,20 @@ def setImage( Parameters ---------- - img : ndarray + img : np.ndarray The image to be displayed. See :func:`ImageItem.setImage` and *notes* below. autoRange : bool Whether to scale/pan the view to fit the image. autoLevels : bool Whether to update the white/black levels to fit the image. - levels : (min, max) - The white and black level values to use. + levels : tuple + (min, max) white and black level values to use. axes : dict Dictionary indicating the interpretation for each axis. This is only needed to override the default guess. Format is:: {'t':0, 'x':1, 'y':2, 'c':3}; - xvals : ndarray + xvals : np.ndarray 1D array of values corresponding to the first axis in a 3D image. For video, this array should contain the time of each frame. pos diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index b546912b90..4a232b7ee3 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -135,9 +135,12 @@ def depthValue(self): def setTransform(self, tr): """Set the local transform for this object. - Must be a :class:`Transform3D ` instance. This transform - determines how the local coordinate system of the item is mapped to the coordinate - system of its parent.""" + + Parameters + ---------- + tr : pyqtgraph.Transform3D + Tranformation from the local coordinate system to the parent's. + """ self.__transform = Transform3D(tr) self.update() diff --git a/tests/graphicsItems/test_PlotDataItem.py b/tests/graphicsItems/test_PlotDataItem.py index 083c1d14cb..e89eb65c11 100644 --- a/tests/graphicsItems/test_PlotDataItem.py +++ b/tests/graphicsItems/test_PlotDataItem.py @@ -83,7 +83,7 @@ def _assert_equal_arrays(a1, a2): x = np.array([-np.inf, 0.0, 1.0, 2.0 , np.nan, 4.0 , np.inf]) y = np.array([ 1.0, 0.0,-1.0, np.inf, 2.0 , np.nan, 0.0 ]) pdi = pg.PlotDataItem(x, y) - dataset = pdi.getDisplayDataset() + dataset = pdi._getDisplayDataset() _assert_equal_arrays( dataset.x, x ) _assert_equal_arrays( dataset.y, y ) @@ -95,7 +95,7 @@ def _assert_equal_arrays(a1, a2): y_log[ ~np.isfinite(y_log) ] = np.nan pdi.setLogMode(True, True) - dataset = pdi.getDisplayDataset() + dataset = pdi._getDisplayDataset() _assert_equal_arrays( dataset.x, x_log ) _assert_equal_arrays( dataset.y, y_log ) From 0f53e4e42467485d59c090e80067b8f4a9f9540c Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 13 Sep 2022 20:17:09 -0700 Subject: [PATCH 033/306] Fix CtrlNode reference by removing it --- doc/source/flowchart/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/flowchart/index.rst b/doc/source/flowchart/index.rst index cb1f5d56fb..243a055cea 100644 --- a/doc/source/flowchart/index.rst +++ b/doc/source/flowchart/index.rst @@ -161,7 +161,7 @@ control panel. A minimal Node subclass looks like:: Some nodes implement fairly complex control widgets, but most nodes follow a simple form-like pattern: a list of parameter names and a single value (represented as spin box, check box, etc..) for each parameter. To make this easier, the -:class:`CtrlNode ` subclass allows you +``pyqtgraph.flowchart.library.common.CtrlNode`` subclass allows you to instead define a simple data structure that CtrlNode will use to automatically generate the control widget. This is used in many of the built-in library nodes (especially the filters). From 527e32039c2a24cd1f6908fabcd949ac16499461 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 13 Sep 2022 20:18:35 -0700 Subject: [PATCH 034/306] Move intersphinx to main extensions list --- doc/source/conf.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 6211e59197..0bcd95fe91 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -30,7 +30,13 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx_qt_documentation'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx_qt_documentation", +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -41,7 +47,6 @@ # Set Qt Documentation Variable qt_documentation = "Qt6" -extensions += ["sphinx.ext.intersphinx"] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'numpy': ('https://numpy.org/doc/stable/', None) From 482ce4914e68283a9588c8cefe4aed3ebb8365fb Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 13 Sep 2022 20:24:20 -0700 Subject: [PATCH 035/306] Add transform3d autodoc stub file --- doc/source/transform3d.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/source/transform3d.rst diff --git a/doc/source/transform3d.rst b/doc/source/transform3d.rst new file mode 100644 index 0000000000..44f0d5d52d --- /dev/null +++ b/doc/source/transform3d.rst @@ -0,0 +1,5 @@ +Transform3D +=========== + +.. automodule:: pyqtgraph.Transform3D + :members: From d0b9b7c66cb86a526c1f64000428d442a52d7a5c Mon Sep 17 00:00:00 2001 From: bbc131 <36670201+bbc131@users.noreply.github.com> Date: Wed, 14 Sep 2022 09:18:28 +0200 Subject: [PATCH 036/306] Fix doubling labels in DateAxisItem (#2413) * Fix doubling labels in DateAxisItem * Due to floating point errors, comparison for equality sometimes fails to detect doubling values. Use approximative comparison instead. Code is copied from AxisItem.tickValues(). * Cleanup: Use numpy array instead of list * Now, it's more like in AxisItem.tickValues() * Minor refactoring in AxisItem/DateAxisItem.tickValues() * Use `numpy.isclose()` instead of `filter()` with lambda * Remove redundant line * `ticks` is already a `numpy.ndarray` * Correct return types (list instead of numpy.ndarray) --- pyqtgraph/graphicsItems/AxisItem.py | 8 ++++++-- pyqtgraph/graphicsItems/DateAxisItem.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 88bac3570f..854808ce39 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -820,9 +820,13 @@ def tickValues(self, minVal, maxVal, size): ## remove any ticks that were present in higher levels ## we assume here that if the difference between a tick value and a previously seen tick value ## is less than spacing/100, then they are 'equal' and we can ignore the new tick. - values = list(filter(lambda x: np.all(np.abs(allValues-x) > spacing/self.scale*0.01), values)) + close = np.any( + np.isclose(allValues, values[:, np.newaxis], rtol=0, atol=spacing/self.scale*0.01) + , axis=-1 + ) + values = values[~close] allValues = np.concatenate([allValues, values]) - ticks.append((spacing/self.scale, values)) + ticks.append((spacing/self.scale, values.tolist())) if self.logMode: return self.logTickValues(minVal, maxVal, size, ticks) diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py index 0b74ca5dbe..f424fae9b8 100644 --- a/pyqtgraph/graphicsItems/DateAxisItem.py +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -157,7 +157,7 @@ def tickValues(self, minVal, maxVal, minSpc): # minSpc indicates the minimum spacing (in seconds) between two ticks # to fullfill the maxTicksPerPt constraint of the DateAxisItem at the # current zoom level. This is used for auto skipping ticks. - allTicks = [] + allTicks = np.array([]) valueSpecs = [] # back-project (minVal maxVal) to UTC, compute ticks then offset to # back to local time again @@ -168,9 +168,15 @@ def tickValues(self, minVal, maxVal, minSpc): # reposition tick labels to local time coordinates ticks += self.utcOffset # remove any ticks that were present in higher levels - tick_list = [x for x in ticks.tolist() if x not in allTicks] - allTicks.extend(tick_list) - valueSpecs.append((spec.spacing, tick_list)) + # we assume here that if the difference between a tick value and a previously seen tick value + # is less than min-spacing/100, then they are 'equal' and we can ignore the new tick. + close = np.any( + np.isclose(allTicks, ticks[:, np.newaxis], rtol=0, atol=minSpc * 0.01), + axis=-1, + ) + ticks = ticks[~close] + allTicks = np.concatenate([allTicks, ticks]) + valueSpecs.append((spec.spacing, ticks.tolist())) # if we're skipping ticks on the current level there's no point in # producing lower level ticks if skipFactor > 1: From fe9394b33ace545a68811b7d9bfed81546e7ee39 Mon Sep 17 00:00:00 2001 From: Joakim Guth Date: Sat, 17 Sep 2022 23:05:58 +0200 Subject: [PATCH 037/306] Add GLTextItem to docs --- doc/source/3dgraphics/gltextitem.rst | 7 +++++++ doc/source/3dgraphics/index.rst | 1 + 2 files changed, 8 insertions(+) create mode 100644 doc/source/3dgraphics/gltextitem.rst diff --git a/doc/source/3dgraphics/gltextitem.rst b/doc/source/3dgraphics/gltextitem.rst new file mode 100644 index 0000000000..2ffe6c3640 --- /dev/null +++ b/doc/source/3dgraphics/gltextitem.rst @@ -0,0 +1,7 @@ +GLTextItem +========== + +.. autoclass:: pyqtgraph.opengl.GLTextItem + :members: + + .. automethod:: pyqtgraph.opengl.GLTextItem.__init__ diff --git a/doc/source/3dgraphics/index.rst b/doc/source/3dgraphics/index.rst index 1678eb4bd5..15d23b382f 100644 --- a/doc/source/3dgraphics/index.rst +++ b/doc/source/3dgraphics/index.rst @@ -28,4 +28,5 @@ Contents: glaxisitem glgraphicsitem glscatterplotitem + gltextitem meshdata From fe9046be9dd4044b9bd1bd9eb2158a47c6c71225 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Sun, 11 Sep 2022 21:56:44 -0400 Subject: [PATCH 038/306] Rework `Interactor`'s run action. Save space by placing the button inside the nested group where possible. This has the added benefit of allowing a function to be run while parameters are collapsed --- pyqtgraph/parametertree/interactive.py | 68 ++++++++++++------- .../parametertree/parameterTypes/__init__.py | 4 +- .../parametertree/parameterTypes/action.py | 50 +++++++++----- .../parameterTypes/functiongroup.py | 56 +++++++++++++++ tests/parametertree/test_Parameter.py | 37 +++++----- 5 files changed, 154 insertions(+), 61 deletions(-) create mode 100644 pyqtgraph/parametertree/parameterTypes/functiongroup.py diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 7897179362..4cf4c6a5e8 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -4,6 +4,7 @@ import pydoc from . import Parameter +from .parameterTypes import FunctionGroupParameter from .. import functions as fn @@ -199,7 +200,7 @@ def __repr__(self): class Interactor: - runOptions = RunOptions.ON_CHANGED + runOptions = [RunOptions.ON_CHANGED, RunOptions.ON_ACTION] parent = None titleFormat = None nest = True @@ -337,7 +338,7 @@ def interact( funcDict = self.functionToParameterDict(function.function, **overrides) children = funcDict.pop("children", []) # type: list[dict] chNames = [ch["name"] for ch in children] - funcGroup = self._resolveFunctionGroup(funcDict) + funcGroup = self._resolveFunctionGroup(funcDict, function) # Values can't come both from closures and overrides/params, so ensure they don't # get created @@ -373,19 +374,18 @@ def interact( useParams.append(child) function.hookupParameters(useParams) - # If no top-level parent and no nesting, return the list of child parameters - ret = funcGroup or useParams if RunOptions.ON_ACTION in self.runOptions: # Add an extra action child which can activate the function - action = self._makeRunAction(self.nest, funcDict.get("tip"), function) - # Return just the action if no other params were allowed - if not useParams: - ret = action - if funcGroup: - funcGroup.addChild(action, existOk=self.existOk) - + action = self._resolveRunAction( + function, funcGroup, funcDict.get("tip") + ) + if action: + useParams.append(action) + retValue = funcGroup if self.nest else useParams self.setOpts(**oldOpts) - return ret + # Return either the parent which contains all added options, or the list + # of created children (if no parent was created) + return retValue @functools.wraps(interact) def __call__(self, function, **kwargs): @@ -433,16 +433,17 @@ def _nameToTitle(self, name, forwardStrTitle=False): # else: titleFormat should be callable return titleFormat(name) - def _resolveFunctionGroup(self, parentOpts): + def _resolveFunctionGroup(self, functionDict, interactiveFunction): """ Returns parent parameter that holds function children. May be ``None`` if no top parent is provided and nesting is disabled. """ funcGroup = self.parent if self.nest: - funcGroup = Parameter.create(**parentOpts) - if self.parent and self.nest: - self.parent.addChild(funcGroup, existOk=self.existOk) + funcGroup = Parameter.create(**functionDict) + funcGroup.sigActivated.connect(interactiveFunction.runFromAction) + if self.parent: + self.parent.addChild(funcGroup, existOk=self.existOk) return funcGroup @staticmethod @@ -473,27 +474,41 @@ def resolveAndHookupParameterChild(self, funcGroup, childOpts, interactiveFunc): child.sigValueChanging.connect(interactiveFunc.runFromChangedOrChanging) return child - def _makeRunAction(self, nest, tip, interactiveFunc): - # Add an extra action child which can activate the function + def _resolveRunAction(self, interactiveFunction, functionGroup, functionTip): + if isinstance(functionGroup, FunctionGroupParameter): + functionGroup.setButtonOpts(visible=True) + child = None + else: + # Add an extra action child which can activate the function + createOpts = self._makePopulatedActionTemplate( + interactiveFunction.__name__, functionTip + ) + child = Parameter.create(**createOpts) + child.sigActivated.connect(interactiveFunction.runFromAction) + if functionGroup: + functionGroup.addChild(child, existOk=self.existOk) + return child + + def _makePopulatedActionTemplate(self, functionName="", functionTip=None): createOpts = self.runActionTemplate.copy() defaultName = createOpts.get("defaultName", "Run") - name = defaultName if nest else interactiveFunc.function.__name__ + name = defaultName if self.nest else functionName createOpts.setdefault("name", name) - if tip: - createOpts["tip"] = tip - child = Parameter.create(**createOpts) - child.sigActivated.connect(interactiveFunc.runFromAction) - return child + if functionTip: + createOpts.setdefault("tip", functionTip) + return createOpts def functionToParameterDict(self, function, **overrides): """ Converts a function into a list of child parameter dicts """ children = [] - out = dict(name=function.__name__, type="group", children=children) + name = function.__name__ + btnOpts = dict(**self._makePopulatedActionTemplate(name), visible=False) + out = dict(name=name, type="functiongroup", children=children, button=btnOpts) if self.titleFormat is not None: - out["title"] = self._nameToTitle(function.__name__, forwardStrTitle=True) + out["title"] = self._nameToTitle(name, forwardStringTitle=True) funcParams = inspect.signature(function).parameters if function.__doc__: @@ -501,6 +516,7 @@ def functionToParameterDict(self, function, **overrides): synopsis, _ = pydoc.splitdoc(function.__doc__) if synopsis: out.setdefault("tip", synopsis) + out["button"].setdefault("tip", synopsis) # Make pyqtgraph parameter dicts from each parameter # Use list instead of funcParams.items() so kwargs can add to the iterable diff --git a/pyqtgraph/parametertree/parameterTypes/__init__.py b/pyqtgraph/parametertree/parameterTypes/__init__.py index 57bdd5838f..0e7696246e 100644 --- a/pyqtgraph/parametertree/parameterTypes/__init__.py +++ b/pyqtgraph/parametertree/parameterTypes/__init__.py @@ -12,6 +12,7 @@ from .color import ColorParameter, ColorParameterItem from .colormap import ColorMapParameter, ColorMapParameterItem from .file import FileParameter, FileParameterItem +from .functiongroup import FunctionGroupParameter, FunctionGroupParameterItem from .font import FontParameter, FontParameterItem from .list import ListParameter, ListParameterItem from .numeric import NumericParameterItem @@ -27,7 +28,8 @@ registerParameterItemType('int', NumericParameterItem, SimpleParameter, override=True) registerParameterItemType('str', StrParameterItem, SimpleParameter, override=True) -registerParameterType('group', GroupParameter, override=True) +registerParameterType('group', GroupParameter, override=True) +registerParameterType('functiongroup', FunctionGroupParameter, override=True) registerParameterType('action', ActionParameter, override=True) registerParameterType('calendar', CalendarParameter, override=True) diff --git a/pyqtgraph/parametertree/parameterTypes/action.py b/pyqtgraph/parametertree/parameterTypes/action.py index 58f8a1f151..4777011a0f 100644 --- a/pyqtgraph/parametertree/parameterTypes/action.py +++ b/pyqtgraph/parametertree/parameterTypes/action.py @@ -3,6 +3,39 @@ from ..ParameterItem import ParameterItem +class ParameterControlledButton(QtWidgets.QPushButton): + settableAttributes = {"title", "tip", "icon", "shortcut", "enabled", "visible"} + + def __init__(self, parameter=None, parent=None): + super().__init__(parent) + if not parameter: + return + parameter.sigNameChanged.connect(self.onNameChange) + parameter.sigOptionsChanged.connect(self.updateOpts) + self.clicked.connect(parameter.activate) + self.updateOpts(parameter, parameter.opts) + + def updateOpts(self, param, opts): + # Of the attributes that can be set on a QPushButton, only the text + # and tooltip attributes are different from standard pushbutton names + nameMap = dict(title="text", tip="tooltip") + # Special case: "title" could be none, in which case make it something + # readable by the simple copy-paste logic later + opts = opts.copy() + if "name" in opts: + opts.setdefault("title", opts["name"]) + if "title" in opts and opts["title"] is None: + opts["title"] = param.title() + + for attr in self.settableAttributes.intersection(opts): + buttonAttr = nameMap.get(attr, attr).title() + setter = getattr(self, f"set{buttonAttr}") + setter(opts[attr]) + + def onNameChange(self, param, name): + self.updateOpts(param, title=param.title()) + + class ActionParameterItem(ParameterItem): """ParameterItem displaying a clickable button.""" def __init__(self, param, depth): @@ -11,13 +44,11 @@ def __init__(self, param, depth): self.layout = QtWidgets.QHBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layoutWidget.setLayout(self.layout) - self.button = QtWidgets.QPushButton() + self.button = ParameterControlledButton(param) #self.layout.addSpacing(100) self.layout.addWidget(self.button) self.layout.addStretch() - self.button.clicked.connect(self.buttonClicked) self.titleChanged() - self.optsChanged(self.param, self.param.opts) def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) @@ -29,21 +60,8 @@ def treeWidgetChanged(self): tree.setItemWidget(self, 0, self.layoutWidget) def titleChanged(self): - self.button.setText(self.param.title()) self.setSizeHint(0, self.button.sizeHint()) - def optsChanged(self, param, opts): - ParameterItem.optsChanged(self, param, opts) - - if 'enabled' in opts: - self.button.setEnabled(opts['enabled']) - - if 'tip' in opts: - self.button.setToolTip(opts['tip']) - - def buttonClicked(self): - self.param.activate() - class ActionParameter(Parameter): """Used for displaying a button within the tree. diff --git a/pyqtgraph/parametertree/parameterTypes/functiongroup.py b/pyqtgraph/parametertree/parameterTypes/functiongroup.py new file mode 100644 index 0000000000..2b27e02333 --- /dev/null +++ b/pyqtgraph/parametertree/parameterTypes/functiongroup.py @@ -0,0 +1,56 @@ +from ...Qt import QtCore +from .action import ParameterControlledButton +from .basetypes import GroupParameter, GroupParameterItem +from ..ParameterItem import ParameterItem +from ...Qt import QtCore + + +class FunctionGroupParameterItem(GroupParameterItem): + """ + Wraps a :class:`GroupParameterItem` to manage ``bool`` parameter children. Also provides convenience buttons to + select or clear all values at once. Note these conveniences are disabled when ``exclusive`` is *True*. + """ + + def __init__(self, param, depth): + self.button = ParameterControlledButton() + super().__init__(param, depth) + self.button.clicked.connect(param.activate) + + def treeWidgetChanged(self): + ParameterItem.treeWidgetChanged(self) + tw = self.treeWidget() + if tw is None: + return + if self.button: + tw.setItemWidget(self, 1, self.button) + elif tw.itemWidget(self, 1) is not None: + tw.removeItemWidget(self, 1) + + def optsChanged(self, param, opts): + if "button" in opts: + buttonOpts = opts["button"] or dict(visible=False) + self.button.updateOpts(param, buttonOpts) + self.treeWidgetChanged() + super().optsChanged(param, opts) + + +class FunctionGroupParameter(GroupParameter): + itemClass = FunctionGroupParameterItem + + sigActivated = QtCore.Signal() + + def __init__(self, **opts): + opts.setdefault("button", {}) + super().__init__(**opts) + + def activate(self): + self.sigActivated.emit() + + def setButtonOpts(self, **opts): + """ + Update individual button options without replacing the entire + button definition. + """ + buttonOpts = self.opts.get("button", {}).copy() + buttonOpts.update(opts) + self.setOpts(button=buttonOpts) diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 1e270d752b..09bc20876f 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -104,10 +104,10 @@ def a(x, y=5): a_interact = InteractiveFunction(a, closures=dict(x=lambda: myval)) host = interactor(a_interact) assert "x" not in host.names - host.child("Run").activate() + host.activate() assert value == (5, 5) myval = 10 - host.child("Run").activate() + host.activate() assert value == (10, 5) host = interactor( @@ -149,7 +149,7 @@ def kwargTest(a, b=5, **c): host = interactor(kwargTest, a=10, test=3) for ch in "a", "b", "test": assert ch in host.names - host.child("Run").activate() + host.activate() assert value == 12 host = GP.create(name="test deco", type="group") @@ -162,7 +162,7 @@ def a(x=5): assert "a" in host.names assert "x" in host.child("a").names - host.child("a", "Run").activate() + host.child("a").activate() assert value == 5 @interactor.decorate(nest=False, runOptions=RunOptions.ON_CHANGED) @@ -195,20 +195,24 @@ def a(): interactor = Interactor(runOptions=RunOptions.ON_ACTION) defaultRunBtn = Parameter.create(**interactor.runActionTemplate, name="Run") - btn = interactor(a) - assert btn.type() == defaultRunBtn.type() + group = interactor(a) + assert group.makeTreeItem(0).button.text() == defaultRunBtn.name() template = dict(defaultName="Test", type="action") with interactor.optsContext(runActionTemplate=template): x = interactor(a) - assert x.name() == "Test" + assert x.makeTreeItem(0).button.text() == "Test" parent = Parameter.create(name="parent", type="group") test2 = interactor(a, parent=parent, nest=False) - assert test2.parent() is parent + assert ( + len(test2) == 1 + and test2[0].name() == a.__name__ + and test2[0].parent() is parent + ) test2 = interactor(a, nest=False) - assert not test2.parent() + assert len(test2) == 1 and not test2[0].parent() def test_no_func_group(): @@ -225,22 +229,19 @@ def a(): interactor = Interactor() - btn = interactor(a, runOptions=RunOptions.ON_ACTION) - assert btn.opts["tip"] == a.__doc__ + group = interactor(a, runOptions=RunOptions.ON_ACTION) + assert group.opts["tip"] == a.__doc__ and group.type() == "functiongroup" - def a2(x=5): - """a simple tip""" + params = interactor(a, runOptions=RunOptions.ON_ACTION, nest=False) + assert len(params) == 1 and params[0].opts["tip"] == a.__doc__ - def a3(x=5): + def a2(x=5): """ A long docstring with a newline followed by more text won't result in a tooltip """ - param = interactor(a2, runOptions=RunOptions.ON_ACTION) - assert param.opts["tip"] == a2.__doc__ and param.type() == "group" - - param = interactor(a3) + param = interactor(a2) assert "tip" not in param.opts From c1e553c630894a00161cb23b08aaf4c196c26825 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Sun, 11 Sep 2022 21:57:15 -0400 Subject: [PATCH 039/306] Prevent internal state modifications before raising ValueError --- pyqtgraph/parametertree/interactive.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 4cf4c6a5e8..086c767abd 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -361,6 +361,10 @@ def interact( and ch["name"] not in function.extra ] if missingChildren: + # setOpts will not be called due to the value error, so reset here. + # This only matters to restore Interactor state in an outer try-except + # block + self.setOpts(**oldOpts) raise ValueError( f"Cannot interact with `{function}` since it has required parameters " f"{missingChildren} with no default or closure values provided." From 1a2ec30982b5ea74834e0a3593c9cd83db01b6c0 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Mon, 12 Sep 2022 13:22:16 -0400 Subject: [PATCH 040/306] Convert even more abbreviations to full words --- pyqtgraph/parametertree/interactive.py | 32 +++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 086c767abd..7c961de3e7 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -39,6 +39,10 @@ class InteractiveFunction: can provide an external scope for accessing the hooked up parameter signals. """ + # Attributes below are populated by `update_wrapper` but aren't detected by linters + __name__: str + __qualname__: str + def __init__(self, function, *, closures=None, **extra): """ Wraps a callable function in a way that forwards Parameter arguments as keywords @@ -207,7 +211,7 @@ class Interactor: existOk = True runActionTemplate = dict(type="action", defaultName="Run") - _optNames = [ + _optionNames = [ "runOptions", "parent", "titleFormat", @@ -328,7 +332,7 @@ def interact( locs = locals() # Everything until action template opts = { - kk: locs[kk] for kk in self._optNames[:-1] if locs[kk] is not PARAM_UNSET + kk: locs[kk] for kk in self._optionNames[:-1] if locs[kk] is not PARAM_UNSET } oldOpts = self.setOpts(**opts) # Delete explicitly since correct values are now ``self`` attributes @@ -380,9 +384,7 @@ def interact( function.hookupParameters(useParams) if RunOptions.ON_ACTION in self.runOptions: # Add an extra action child which can activate the function - action = self._resolveRunAction( - function, funcGroup, funcDict.get("tip") - ) + action = self._resolveRunAction(function, funcGroup, funcDict.get("tip")) if action: useParams.append(action) retValue = funcGroup if self.nest else useParams @@ -413,7 +415,7 @@ def decorator(function): return decorator - def _nameToTitle(self, name, forwardStrTitle=False): + def _nameToTitle(self, name, forwardStringTitle=False): """ Converts a function name to a title based on ``self.titleFormat``. @@ -421,7 +423,7 @@ def _nameToTitle(self, name, forwardStrTitle=False): ---------- name: str Name of the function - forwardStrTitle: bool + forwardStringTitle: bool If ``self.titleFormat`` is a string and ``forwardStrTitle`` is True, ``self.titleFormat`` will be used as the title. Otherwise, if ``self.titleFormat`` is *None*, the name will be returned unchanged. @@ -430,7 +432,7 @@ def _nameToTitle(self, name, forwardStrTitle=False): """ titleFormat = self.titleFormat isString = isinstance(titleFormat, str) - if titleFormat is None or (isString and not forwardStrTitle): + if titleFormat is None or (isString and not forwardStringTitle): return name elif isString: return titleFormat @@ -467,15 +469,17 @@ def _toInteractiveFunction(function): refOwner.interactiveRefs = [interactive] return interactive - def resolveAndHookupParameterChild(self, funcGroup, childOpts, interactiveFunc): - if not funcGroup: + def resolveAndHookupParameterChild( + self, functionGroup, childOpts, interactiveFunction + ): + if not functionGroup: child = Parameter.create(**childOpts) else: - child = funcGroup.addChild(childOpts, existOk=self.existOk) + child = functionGroup.addChild(childOpts, existOk=self.existOk) if RunOptions.ON_CHANGED in self.runOptions: - child.sigValueChanged.connect(interactiveFunc.runFromChangedOrChanging) + child.sigValueChanged.connect(interactiveFunction.runFromChangedOrChanging) if RunOptions.ON_CHANGING in self.runOptions: - child.sigValueChanging.connect(interactiveFunc.runFromChangedOrChanging) + child.sigValueChanging.connect(interactiveFunction.runFromChangedOrChanging) return child def _resolveRunAction(self, interactiveFunction, functionGroup, functionTip): @@ -598,7 +602,7 @@ def __repr__(self): return str(self) def getOpts(self): - return {attr: getattr(self, attr) for attr in self._optNames} + return {attr: getattr(self, attr) for attr in self._optionNames} interact = Interactor() From 6b578fead82dbf184adf56bb242e57f7a4a4d5a6 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Mon, 12 Sep 2022 13:24:02 -0400 Subject: [PATCH 041/306] Handle interacted functions accepting positional args. For now, simply remove positional arguments from consideration --- pyqtgraph/parametertree/interactive.py | 17 ++++++++++++----- tests/parametertree/test_Parameter.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 7c961de3e7..3eb170b1a4 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -529,16 +529,23 @@ def functionToParameterDict(self, function, **overrides): # Make pyqtgraph parameter dicts from each parameter # Use list instead of funcParams.items() so kwargs can add to the iterable checkNames = list(funcParams) - isKwarg = [p.kind is p.VAR_KEYWORD for p in funcParams.values()] - if any(isKwarg): + parameterKinds = [p.kind for p in funcParams.values()] + _positional = inspect.Parameter.VAR_POSITIONAL + _keyword = inspect.Parameter.VAR_KEYWORD + if _keyword in parameterKinds: # Function accepts kwargs, so any overrides not already present as a function # parameter should be accepted # Remove the keyword parameter since it can't be parsed properly - # Only one kwarg can be in the signature, so there will be only one - # "True" index - del checkNames[isKwarg.index(True)] + # Kwargs must appear at the end of the parameter list + del checkNames[-1] notInSignature = [n for n in overrides if n not in checkNames] checkNames.extend(notInSignature) + if _positional in parameterKinds: + # *args is also difficult to handle for key-value paradigm + # and will mess with the logic for detecting whether any parameter + # is left unfilled + del checkNames[parameterKinds.index(_positional)] + for name in checkNames: # May be none if this is an override name after function accepted kwargs param = funcParams.get(name) diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 09bc20876f..9396390b32 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -430,3 +430,13 @@ def c(self, z=5): ci = interactor.decorate()(a.c) assert ci() == a.c() + + +def test_args_interact(): + + @interact.decorate() + def a(*args): + """""" + + assert not (a.parameters or a.extra) + a() \ No newline at end of file From fa68a2621e9c54de31ef3940029c826c33c33b8e Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Mon, 12 Sep 2022 18:51:58 -0400 Subject: [PATCH 042/306] Refactor action testing. - Add extra `action` arguments - Rename `FunctionParameterGroup` -> `ActionGroup` for clarity - Bugfix on setting icon in `action` arguments --- pyqtgraph/examples/_buildParamTypes.py | 6 +++- pyqtgraph/examples/_paramtreecfg.py | 13 +++++++- pyqtgraph/parametertree/interactive.py | 6 ++-- .../parametertree/parameterTypes/__init__.py | 7 +++-- .../parametertree/parameterTypes/action.py | 31 +++++++++++++++---- .../{functiongroup.py => actiongroup.py} | 6 ++-- tests/parametertree/test_Parameter.py | 2 +- 7 files changed, 53 insertions(+), 18 deletions(-) rename pyqtgraph/parametertree/parameterTypes/{functiongroup.py => actiongroup.py} (92%) diff --git a/pyqtgraph/examples/_buildParamTypes.py b/pyqtgraph/examples/_buildParamTypes.py index 8e0433c134..3744e49a39 100644 --- a/pyqtgraph/examples/_buildParamTypes.py +++ b/pyqtgraph/examples/_buildParamTypes.py @@ -88,7 +88,11 @@ def activate(action): btn = Parameter.create(name=f'{name} All', type='action') btn.sigActivated.connect(activate) params.insertChild(0, btn) - missing = set(PARAM_TYPES).difference(_encounteredTypes) + missing = [ + typ + for typ in set(PARAM_TYPES).difference(_encounteredTypes) + if not typ.startswith("_") + ] if missing: raise RuntimeError(f'{missing} parameters are not represented') return params diff --git a/pyqtgraph/examples/_paramtreecfg.py b/pyqtgraph/examples/_paramtreecfg.py index 4a178b19c4..161b24a136 100644 --- a/pyqtgraph/examples/_paramtreecfg.py +++ b/pyqtgraph/examples/_paramtreecfg.py @@ -132,6 +132,18 @@ } }, + 'action': { + 'shortcut': { + 'type': 'str', + 'value': "Ctrl+Shift+P", + }, + 'icon': { + 'type': 'file', + 'value': None, + 'nameFilter': "Images (*.png *.jpg *.bmp *.jpeg *.svg)", + }, + }, + 'calendar': { 'format': { 'type': 'str', @@ -186,7 +198,6 @@ 'bool': False, 'colormap': None, 'progress': 50, - 'action': None, 'font': 'Inter', } } diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 3eb170b1a4..3284703464 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -4,7 +4,7 @@ import pydoc from . import Parameter -from .parameterTypes import FunctionGroupParameter +from .parameterTypes import ActionGroup from .. import functions as fn @@ -483,7 +483,7 @@ def resolveAndHookupParameterChild( return child def _resolveRunAction(self, interactiveFunction, functionGroup, functionTip): - if isinstance(functionGroup, FunctionGroupParameter): + if isinstance(functionGroup, ActionGroup): functionGroup.setButtonOpts(visible=True) child = None else: @@ -514,7 +514,7 @@ def functionToParameterDict(self, function, **overrides): children = [] name = function.__name__ btnOpts = dict(**self._makePopulatedActionTemplate(name), visible=False) - out = dict(name=name, type="functiongroup", children=children, button=btnOpts) + out = dict(name=name, type="_actiongroup", children=children, button=btnOpts) if self.titleFormat is not None: out["title"] = self._nameToTitle(name, forwardStringTitle=True) diff --git a/pyqtgraph/parametertree/parameterTypes/__init__.py b/pyqtgraph/parametertree/parameterTypes/__init__.py index 0e7696246e..42e1dbd7b7 100644 --- a/pyqtgraph/parametertree/parameterTypes/__init__.py +++ b/pyqtgraph/parametertree/parameterTypes/__init__.py @@ -1,5 +1,6 @@ from ..Parameter import registerParameterItemType, registerParameterType from .action import ActionParameter, ActionParameterItem +from .actiongroup import ActionGroup, ActionGroupParameterItem from .basetypes import ( GroupParameter, GroupParameterItem, @@ -12,7 +13,6 @@ from .color import ColorParameter, ColorParameterItem from .colormap import ColorMapParameter, ColorMapParameterItem from .file import FileParameter, FileParameterItem -from .functiongroup import FunctionGroupParameter, FunctionGroupParameterItem from .font import FontParameter, FontParameterItem from .list import ListParameter, ListParameterItem from .numeric import NumericParameterItem @@ -28,8 +28,9 @@ registerParameterItemType('int', NumericParameterItem, SimpleParameter, override=True) registerParameterItemType('str', StrParameterItem, SimpleParameter, override=True) -registerParameterType('group', GroupParameter, override=True) -registerParameterType('functiongroup', FunctionGroupParameter, override=True) +registerParameterType('group', GroupParameter, override=True) +# Keep actiongroup private for now, mainly useful for Interactor but not externally +registerParameterType('_actiongroup', ActionGroup, override=True) registerParameterType('action', ActionParameter, override=True) registerParameterType('calendar', CalendarParameter, override=True) diff --git a/pyqtgraph/parametertree/parameterTypes/action.py b/pyqtgraph/parametertree/parameterTypes/action.py index 4777011a0f..7c7b2c2b9a 100644 --- a/pyqtgraph/parametertree/parameterTypes/action.py +++ b/pyqtgraph/parametertree/parameterTypes/action.py @@ -1,10 +1,12 @@ -from ...Qt import QtCore, QtWidgets +from ...Qt import QtCore, QtWidgets, QtGui from ..Parameter import Parameter from ..ParameterItem import ParameterItem class ParameterControlledButton(QtWidgets.QPushButton): - settableAttributes = {"title", "tip", "icon", "shortcut", "enabled", "visible"} + settableAttributes = { + "title", "tip", "icon", "shortcut", "enabled", "visible" + } def __init__(self, parameter=None, parent=None): super().__init__(parent) @@ -18,7 +20,7 @@ def __init__(self, parameter=None, parent=None): def updateOpts(self, param, opts): # Of the attributes that can be set on a QPushButton, only the text # and tooltip attributes are different from standard pushbutton names - nameMap = dict(title="text", tip="tooltip") + nameMap = dict(title="text", tip="toolTip") # Special case: "title" could be none, in which case make it something # readable by the simple copy-paste logic later opts = opts.copy() @@ -27,13 +29,19 @@ def updateOpts(self, param, opts): if "title" in opts and opts["title"] is None: opts["title"] = param.title() + # Another special case: icons should be loaded from data before + # being passed to the button + if "icon" in opts: + opts["icon"] = QtGui.QIcon(opts["icon"]) + for attr in self.settableAttributes.intersection(opts): - buttonAttr = nameMap.get(attr, attr).title() - setter = getattr(self, f"set{buttonAttr}") + buttonAttr = nameMap.get(attr, attr) + capitalized = buttonAttr[0].upper() + buttonAttr[1:] + setter = getattr(self, f"set{capitalized}") setter(opts[attr]) def onNameChange(self, param, name): - self.updateOpts(param, title=param.title()) + self.updateOpts(param, dict(title=param.title())) class ActionParameterItem(ParameterItem): @@ -67,6 +75,17 @@ class ActionParameter(Parameter): """Used for displaying a button within the tree. ``sigActivated(self)`` is emitted when the button is clicked. + + ============== ============================================================ + **Options:** + icon Icon to display in the button. Can be any argument accepted + by :func:`QtGui.QIcon ` + shortcut Key sequence to use as a shortcut for the button. Note that + this shortcut is associated with spawned parameters, i.e. + the shortcut will only work when this parameter has an item + in a tree that is visible. Can be set to any string accepted + by :func:`QtGui.QKeySequence ` + ============== ============================================================ """ itemClass = ActionParameterItem sigActivated = QtCore.Signal(object) diff --git a/pyqtgraph/parametertree/parameterTypes/functiongroup.py b/pyqtgraph/parametertree/parameterTypes/actiongroup.py similarity index 92% rename from pyqtgraph/parametertree/parameterTypes/functiongroup.py rename to pyqtgraph/parametertree/parameterTypes/actiongroup.py index 2b27e02333..1506899f1d 100644 --- a/pyqtgraph/parametertree/parameterTypes/functiongroup.py +++ b/pyqtgraph/parametertree/parameterTypes/actiongroup.py @@ -5,7 +5,7 @@ from ...Qt import QtCore -class FunctionGroupParameterItem(GroupParameterItem): +class ActionGroupParameterItem(GroupParameterItem): """ Wraps a :class:`GroupParameterItem` to manage ``bool`` parameter children. Also provides convenience buttons to select or clear all values at once. Note these conveniences are disabled when ``exclusive`` is *True*. @@ -34,8 +34,8 @@ def optsChanged(self, param, opts): super().optsChanged(param, opts) -class FunctionGroupParameter(GroupParameter): - itemClass = FunctionGroupParameterItem +class ActionGroup(GroupParameter): + itemClass = ActionGroupParameterItem sigActivated = QtCore.Signal() diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 9396390b32..76d41b69e9 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -230,7 +230,7 @@ def a(): interactor = Interactor() group = interactor(a, runOptions=RunOptions.ON_ACTION) - assert group.opts["tip"] == a.__doc__ and group.type() == "functiongroup" + assert group.opts["tip"] == a.__doc__ and group.type() == "_actiongroup" params = interactor(a, runOptions=RunOptions.ON_ACTION, nest=False) assert len(params) == 1 and params[0].opts["tip"] == a.__doc__ From 5c86fd6be0605f27d26875be345178d711dfc212 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 14 Sep 2022 20:23:34 -0400 Subject: [PATCH 043/306] No need to check for `None` button in `FunctionGroup` --- pyqtgraph/parametertree/parameterTypes/actiongroup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/actiongroup.py b/pyqtgraph/parametertree/parameterTypes/actiongroup.py index 1506899f1d..95b3052213 100644 --- a/pyqtgraph/parametertree/parameterTypes/actiongroup.py +++ b/pyqtgraph/parametertree/parameterTypes/actiongroup.py @@ -21,10 +21,7 @@ def treeWidgetChanged(self): tw = self.treeWidget() if tw is None: return - if self.button: - tw.setItemWidget(self, 1, self.button) - elif tw.itemWidget(self, 1) is not None: - tw.removeItemWidget(self, 1) + tw.setItemWidget(self, 1, self.button) def optsChanged(self, param, opts): if "button" in opts: From 279996bbee082418d79aba33876bd481ed7c5d9b Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 16 Sep 2022 12:25:59 -0400 Subject: [PATCH 044/306] Transition to numpy-style docs for `action` parameter --- .../parametertree/parameterTypes/action.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/action.py b/pyqtgraph/parametertree/parameterTypes/action.py index 7c7b2c2b9a..04f956969b 100644 --- a/pyqtgraph/parametertree/parameterTypes/action.py +++ b/pyqtgraph/parametertree/parameterTypes/action.py @@ -72,20 +72,21 @@ def titleChanged(self): class ActionParameter(Parameter): - """Used for displaying a button within the tree. + """ + Used for displaying a button within the tree. ``sigActivated(self)`` is emitted when the button is clicked. - ============== ============================================================ - **Options:** - icon Icon to display in the button. Can be any argument accepted - by :func:`QtGui.QIcon ` - shortcut Key sequence to use as a shortcut for the button. Note that - this shortcut is associated with spawned parameters, i.e. - the shortcut will only work when this parameter has an item - in a tree that is visible. Can be set to any string accepted - by :func:`QtGui.QKeySequence ` - ============== ============================================================ + Parameters + ---------- + icon: str + Icon to display in the button. Can be any argument accepted + by :class:`QIcon `. + shortcut: str + Key sequence to use as a shortcut for the button. Note that this shortcut is + associated with spawned parameters, i.e. the shortcut will only work when this + parameter has an item in a tree that is visible. Can be set to any string + accepted by :class:`QKeySequence `. """ itemClass = ActionParameterItem sigActivated = QtCore.Signal(object) From ac1b9fa9c12a991ac67554b2dabfa79a23a92a9f Mon Sep 17 00:00:00 2001 From: Puff Machine <13399678+Puff-Machine@users.noreply.github.com> Date: Wed, 21 Sep 2022 15:40:31 +0100 Subject: [PATCH 045/306] Fix missing import in Flowchart.py --- pyqtgraph/flowchart/Flowchart.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 53bddae8e4..542f0f366b 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -2,6 +2,7 @@ import importlib from collections import OrderedDict +import os from .. import DataTreeWidget, FileDialog from ..Qt import QT_LIB, QtCore, QtWidgets From ca85a8fc5fcb918d1a0b1e2ef8e1767897a24986 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 23 Sep 2022 10:50:36 +0800 Subject: [PATCH 046/306] use PlotCurveItem instead of QGraphicsPathItem using PlotCurveItem is apparently several times faster than a QGraphicsPathItem. --- pyqtgraph/examples/multiplePlotSpeedTest.py | 45 ++++++++------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/pyqtgraph/examples/multiplePlotSpeedTest.py b/pyqtgraph/examples/multiplePlotSpeedTest.py index 804706f790..8cbe1cf789 100644 --- a/pyqtgraph/examples/multiplePlotSpeedTest.py +++ b/pyqtgraph/examples/multiplePlotSpeedTest.py @@ -3,7 +3,6 @@ import numpy as np import pyqtgraph as pg -from pyqtgraph.Qt import QtWidgets app = pg.mkQApp() plt = pg.PlotWidget() @@ -30,19 +29,13 @@ def plot(): ## we need here. This overhead adds up quickly and makes a big ## difference in speed. - #plt.plot(x=x+i, y=y+j) plt.addItem(pg.PlotCurveItem(x=x+i, y=y+j)) - #path = pg.arrayToQPath(x+i, y+j) - #item = QtWidgets.QGraphicsPathItem(path) - #item.setPen(pg.mkPen('w')) - #plt.addItem(item) - dt = perf_counter() - start - print(f"Create plots tooks {dt * 1000:.3f} ms") + print(f"Create plots took: {dt * 1000:.3f} ms") ## Plot and clear 5 times, printing the time it took -for i in range(5): +for _ in range(5): plt.clear() plot() app.processEvents() @@ -54,21 +47,21 @@ def plot(): def fastPlot(): ## Different approach: generate a single item with all data points. - ## This runs about 20x faster. + ## This runs many times faster. start = perf_counter() n = 15 pts = 100 x = np.linspace(0, 0.8, pts) y = np.random.random(size=pts)*0.8 - xdata = np.empty((n, n, pts)) - xdata[:] = x.reshape(1,1,pts) + np.arange(n).reshape(n,1,1) - ydata = np.empty((n, n, pts)) - ydata[:] = y.reshape(1,1,pts) + np.arange(n).reshape(1,n,1) - conn = np.ones((n*n,pts)) - conn[:,-1] = False # make sure plots are disconnected - path = pg.arrayToQPath(xdata.flatten(), ydata.flatten(), conn.flatten()) - item = QtWidgets.QGraphicsPathItem(path) - item.setPen(pg.mkPen('w')) + shape = (n, n, pts) + xdata = np.empty(shape) + xdata[:] = x + np.arange(shape[1]).reshape((1,-1,1)) + ydata = np.empty(shape) + ydata[:] = y + np.arange(shape[0]).reshape((-1,1,1)) + conn = np.ones(shape, dtype=bool) + conn[...,-1] = False # make sure plots are disconnected + item = pg.PlotCurveItem() + item.setData(xdata.ravel(), ydata.ravel(), connect=conn.ravel()) plt.addItem(item) dt = perf_counter() - start @@ -76,15 +69,11 @@ def fastPlot(): ## Plot and clear 5 times, printing the time it took -if hasattr(pg, 'arrayToQPath'): - for i in range(5): - plt.clear() - fastPlot() - app.processEvents() -else: - print("Skipping fast tests--arrayToQPath function is missing.") - -plt.autoRange() +for _ in range(5): + plt.clear() + fastPlot() + app.processEvents() + plt.autoRange() if __name__ == '__main__': pg.exec() From 9b8add7b84ec9c34b01baf00d19e18f5b6cbf967 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 18 Sep 2022 20:59:16 +0800 Subject: [PATCH 047/306] deprecate GraphicsObject::parentChanged method QGraphicsObject has a signal named parentChanged --- pyqtgraph/graphicsItems/GraphicsItem.py | 7 +++++-- pyqtgraph/graphicsItems/GraphicsObject.py | 20 +++++++++++++++----- pyqtgraph/graphicsItems/ScaleBar.py | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index af3478ca21..98e73a5d68 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -452,13 +452,16 @@ def transformAngle(self, relativeItem=None): #print " --> ", ch2.scene() #self.setChildScene(ch2) - def parentChanged(self): + def changeParent(self): """Called when the item's parent has changed. This method handles connecting / disconnecting from ViewBox signals to make sure viewRangeChanged works properly. It should generally be extended, not overridden.""" self._updateView() - + + def parentChanged(self): + # deprecated version of changeParent() + GraphicsItem.changeParent(self) def _updateView(self): ## called to see whether this item has a new view to connect to diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index 4f6f76b92c..389c89143b 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -1,3 +1,4 @@ +import warnings from ..Qt import QT_LIB, QtCore, QtWidgets from .GraphicsItem import GraphicsItem @@ -18,12 +19,21 @@ def __init__(self, *args): def itemChange(self, change, value): ret = super().itemChange(change, value) if change in [self.GraphicsItemChange.ItemParentHasChanged, self.GraphicsItemChange.ItemSceneHasChanged]: - if QT_LIB == 'PySide6' and QtCore.__version_info__ == (6, 2, 2): - # workaround PySide6 6.2.2 issue https://bugreports.qt.io/browse/PYSIDE-1730 - # note that the bug exists also in PySide6 6.2.2.1 / Qt 6.2.2 - getattr(self.__class__, 'parentChanged')(self) + if self.__class__.__dict__.get('parentChanged') is not None: + # user's GraphicsObject subclass has a parentChanged() method + warnings.warn( + "parentChanged() is deprecated and will be removed in the future. " + "Use changeParent() instead.", + DeprecationWarning, stacklevel=2 + ) + if QT_LIB == 'PySide6' and QtCore.__version_info__ == (6, 2, 2): + # workaround PySide6 6.2.2 issue https://bugreports.qt.io/browse/PYSIDE-1730 + # note that the bug exists also in PySide6 6.2.2.1 / Qt 6.2.2 + getattr(self.__class__, 'parentChanged')(self) + else: + self.parentChanged() else: - self.parentChanged() + self.changeParent() try: inform_view_on_change = self.__inform_view_on_changes except AttributeError: diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py index 3aa953e6c8..7ad913eb87 100644 --- a/pyqtgraph/graphicsItems/ScaleBar.py +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -36,7 +36,7 @@ def __init__(self, size, width=5, brush=None, pen=None, suffix='m', offset=None) self.text = TextItem(text=fn.siFormat(size, suffix=suffix), anchor=(0.5,1)) self.text.setParentItem(self) - def parentChanged(self): + def changeParent(self): view = self.parentItem() if view is None: return From 29e887453371d62b798459f8216f98cf4f3ead36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 13:56:29 +0000 Subject: [PATCH 048/306] Bump sphinx from 5.1.1 to 5.2.1 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.1.1 to 5.2.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.1...v5.2.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index cb82cf797d..4f89a21a7a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -sphinx==5.1.1 +sphinx==5.2.1 PyQt6==6.3.1 sphinx-rtd-theme sphinx-qt-documentation From ff691a33524e3de96d56a41722e8a3a1e2cc5a1a Mon Sep 17 00:00:00 2001 From: Pepijn Kenter Date: Tue, 27 Sep 2022 00:04:38 +0200 Subject: [PATCH 049/306] Added UML class diagram to give overview of the most important classes (#1631) * UML class diagram of the main PyQtGraph classes. Inludes some relevant Qt classes. Ignore all PNG files in git. * Removed packages from overview UML diagram * Fix UML diagram: layout is attribute of PlotItem & GraphicsLayout. It is not an attribute of the GraphicsWig class. * Don't use a gradient for the classes in the UML diagram. Also a lighter background color to make it more readable. * Fix: the ImageView class contains a pq.GraphicsView object, not a QGraphicsView * Ported most recent UML diagram from PlantUML to YEd. PlantUML version from Ogi Moore was here: https://github.com/pyqtgraph/pyqtgraph/pull/1631#issuecomment-808792166 * Flipped UML diagram horizontally. * UML: put graphics view, scen and items classes at the same horizontal level. * Fixed the agragations between QGraphicsView, QGraphicsScene, QGraphicsItem, and their PyQtGraph equivalents. * Consistent distance between layout items and the rest of the classes. * Moved objects closer together to make the diagram more compact. * Added tooltips and URLs to the class diagram. Included SVG export of the diagram in git. * Added PlotItem.items to UML diagram. * UML diagram: added addItem() method to PlotItem and ViewBox Tweaked the lines a bit. * UML diagram: PlotItem.items now points to QGraphicsItem. Used to point ot GraphicsObject. * UML diagram: PlotItem.items line goes to GraphicsObject again. * Moved GraphicsObject below PlotItem * Revert "Moved GraphicsObject below PlotItem" This reverts commit 697e254a801f15520bf1e69fad4fb3ae41b0e12e. * Moved UML overview to doc/source/images. Used latest version of yEd. * Added UML class diagram to documentation Implemented as separate page with some explanatory text. Added hyperlink to that page from the "Organization of Plotting Classes" section * Removed reference to non-existing "graphicswindow" document. This was pointing to the "Deprecated Window Classes" page, which apparantly has been removed from the docs. --- .gitignore | 1 + doc/source/apireference.rst | 1 + doc/source/images/overview_uml.graphml | 1029 ++++++++++++++++++++ doc/source/images/overview_uml.svg | 1192 ++++++++++++++++++++++++ doc/source/plotting.rst | 3 + doc/source/uml_overview.rst | 15 + doc/uml_overview.txt | 153 +++ 7 files changed, 2394 insertions(+) create mode 100644 doc/source/images/overview_uml.graphml create mode 100644 doc/source/images/overview_uml.svg create mode 100644 doc/source/uml_overview.rst create mode 100644 doc/uml_overview.txt diff --git a/.gitignore b/.gitignore index f8de805b91..7309261d14 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ coverage.xml *.am *.tiff *.tif +*.png *.dat *.DAT diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index 0c279013f1..2d6411c133 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -18,3 +18,4 @@ Contents: flowchart/index point transform3d + uml_overview diff --git a/doc/source/images/overview_uml.graphml b/doc/source/images/overview_uml.graphml new file mode 100644 index 0000000000..d36dca5fcd --- /dev/null +++ b/doc/source/images/overview_uml.graphml @@ -0,0 +1,1029 @@ + + + + + + + + + + + + + + + + + + + + + + + + + ImageView + + graphicsView +imageItem +view + + + + + + + + + + + + + + + QGraphicsView + + + scene() + + + + + + + + + + + + + QGraphicsScene + + + items() +views() + + + + + + + + + + + + + QGraphicsItem + + + scene() + + + + + + + + + + + + + QGraphicsObject + + + + + + + + + + + + + + + + GraphicsView + + sceneObj + + + + + + + + + + + + + + GraphicsObject + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + ScatterPlotItem + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + PlotCurveItem + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + ImageItem + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + PlotDataItem + + curve +scatter + + + + + + + + + + + + + + GraphicsLayoutWidget + + graphicsLayout + + + + + + + + + + + + + + GraphicsItem + + + + + + + + + + + + + + + + QGraphicsLayoutItem + + + + + + + + + + + + + + + + QGraphicsWidget + + + + + + + + + + + + + + + + QGraphicsLayout + + + + + + + + + + + + + + + + QGraphicsGridLayout + + + + + + + + + + + + + + + + PlotWidget + + plotItem + + + + + + + + + + + + + + GraphicsScene + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + GraphicsWidget + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + ViewBox + + + addItem(item) + + + + + + + + + + + + + GraphicsLayout + + layout + + + + + + + + + + + + + + PlotItem + + items +layout +vb + addItem(item) + + + + + + + + + + + + + QPaintDevice + + + + + + + + + + + + + + + + QWidget + + + + + + + + + + + + + + + + QObject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/images/overview_uml.svg b/doc/source/images/overview_uml.svg new file mode 100644 index 0000000000..99f7d8cec9 --- /dev/null +++ b/doc/source/images/overview_uml.svg @@ -0,0 +1,1192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ImageView + + + + + graphicsView + imageItem + view + + + + + + + + + + + + + + + QGraphicsView + + + + + + scene() + + + + + + + + + + + + + + QGraphicsScene + + + + + + items() + views() + + + + + + + + + + + + + + QGraphicsItem + + + + + + scene() + + + + + + + + + + + + + + QGraphicsObject + + + + + + + + + + + + + + + GraphicsView + + + + + sceneObj + + + + + + + + + + + + + + + GraphicsObject + + + + + + + + + + + + + + + ScatterPlotItem + + + + + + + + + + + + + + + PlotCurveItem + + + + + + + + + + + + + + + ImageItem + + + + + + + + + + + + + + + PlotDataItem + + + + + curve + scatter + + + + + + + + + + + + + + + GraphicsLayoutWidget + + + + + graphicsLayout + + + + + + + + + + + + + + + GraphicsItem + + + + + + + + + + + + + + + QGraphicsLayoutItem + + + + + + + + + + + + + + + QGraphicsWidget + + + + + + + + + + + + + + + QGraphicsLayout + + + + + + + + + + + + + + + QGraphicsGridLayout + + + + + + + + + + + + + + + PlotWidget + + + + + plotItem + + + + + + + + + + + + + + + GraphicsScene + + + + + + + + + + + + + + + GraphicsWidget + + + + + + + + + + + + + + + ViewBox + + + + + + addItem(item) + + + + + + + + + + + + + + GraphicsLayout + + + + + layout + + + + + + + + + + + + + + + PlotItem + + + + + items + layout + vb + + addItem(item) + + + + + + + + + + + + + + QPaintDevice + + + + + + + + + + + + + + + QWidget + + + + + + + + + + + + + + + QObject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Widget used for display and analysis of image data. + + + + + + The QGraphicsView class provides a widget for + + displaying the contents of a QGraphicsScene. + + + + + + The QGraphicsScene class provides a surface for + + managing a large number of 2D graphical items. + + + + + + The QGraphicsItem class is the base class for all + + graphical items in a QGraphicsScene. + + + + + + The QGraphicsObject class provides a base class for all + + graphics items that require signals, slots and properties. + + + + + + Re-implementation of QGraphicsView that removes scrollbars and allows + + unambiguous control of the viewed coordinate range. + + + Also automatically creates a GraphicsScene and a central QGraphicsWidget + + that is automatically scaled to the full view geometry. + + + + + + Extension of QGraphicsObject with some useful + + methods (provided by GraphicsItem) + + + + + + Displays a set of x/y points. + + + Instances of this class are created automatically as part of + + PlotDataItem; these rarely need to be instantiated directly. + + + + + + Class representing a single plot curve. + + + Instances of this class are created automatically as part of + + PlotDataItem; these rarely need to be instantiated directly. + + + + + + GraphicsObject displaying an image. + + + + + + GraphicsItem for displaying plot curves, scatter plots, or both. + + + While it is possible to use PlotCurveItem or ScatterPlotItem + + individually, this class provides a unified interface to both. + + + + + + Convenience class consisting of a GraphicsView with a + + single GraphicsLayout as its central item. + + + Most of the methods provided by GraphicsLayout are + + also available through GraphicsLayoutWidget. + + + + + + Abstract class providing useful methods to GraphicsObject + + and GraphicsWidget. (This is required because we cannot + + have multiple inheritance with QObject subclasses.) + + + + + + The QGraphicsLayoutItem class can + + be inherited to allow your custom + + items to be managed by layouts. + + + + + + The QGraphicsWidget class is the base class for + + all widget items in a QGraphicsScene. + + + + + + The QGraphicsLayout class provides + + the base class for all layouts in + + Graphics View. + + + + + + The QGraphicsGridLayout class provides + + a grid layout for managing widgets in + + Graphics View. + + + + + + A subclass of GraphicsView with a single PlotItem displayed. + + + Most of the methods provided by PlotItem are also available + + through PlotWidget. + + + + + + Extension of QGraphicsScene that implements a + + complete, parallel mouse event system. + + + + + + Extends QGraphicsWidget with several helpful methods and + + workarounds for PyQt bugs. + + + Most of the extra functionality is inherited from GraphicsItem. + + + + + + Box that allows internal scaling/panning of children by mouse drag. + + + addItem() will add a graphics item (e.g. a PlotDataItem) to the scene. + + + This class is usually created automatically as part of a PlotItem or + + Canvas or with GraphicsLayout.addViewBox(). + + + + + + Used for laying out GraphicsWidgets + + in a grid. + + + This is usually created automatically + + as part of a GraphicsWindow or + + GraphicsLayoutWidget. + + + + + + GraphicsWidget implementing a standard 2D plotting + + area with axes. + + + addItem() will call ViewBox.addItem(), which will add + + the graphics item to the scene. + + + + + + The QPaintDevice class is the base class of objects that + + can be painted on with QPainter. + + + + + + The QWidget class is the base class of all user interface objects. + + + + + + The QObject class is the base class of all Qt objects. + + + + diff --git a/doc/source/plotting.rst b/doc/source/plotting.rst index 6fd5c27702..0c9abb97ef 100644 --- a/doc/source/plotting.rst +++ b/doc/source/plotting.rst @@ -46,6 +46,9 @@ There are several classes invloved in displaying plot data. Most of these classe .. image:: images/plottingClasses.png +See the :ref:`UML class diagram ` page for a more detailed figure of the most important classes and their relations. + + Examples -------- diff --git a/doc/source/uml_overview.rst b/doc/source/uml_overview.rst new file mode 100644 index 0000000000..5e71849892 --- /dev/null +++ b/doc/source/uml_overview.rst @@ -0,0 +1,15 @@ +UML class diagram +================= + +.. _uml_diagram: + +The UML class diagram below gives an overview of the most important classes and their relations. + +The green boxes represent Qt classes, the purple boxes are PyQtGraph classes. + +The black arrows indicate inheritance between two classes (with the parent class always above the child classes.) + +The gray lines with the diamonds indicate an aggregation relation. For example the :class:`PlotDataItem ` class has a ``curve`` attribute that is a reference to a :class:`PlotCurveItem ` object. + +.. image:: images/overview_uml.svg + diff --git a/doc/uml_overview.txt b/doc/uml_overview.txt new file mode 100644 index 0000000000..b3e01caadd --- /dev/null +++ b/doc/uml_overview.txt @@ -0,0 +1,153 @@ +' Made in PlantUml. You can try it out here: http://plantuml.com/plantuml +' Based on commit eb7a60fcf83cd4e7a41ae5955e57935e39928fbd + +@startuml +hide empty members +hide circles +hide stereotypes + +skinparam class { + BorderColor Black + ArrowColor Black + + ' Using stereotypes to define the background colors + BackgroundColor<> #e5f2da + BackgroundColor<> #e5e5f4 +} +skinparam shadowing false + + +'----------- Qt package ----------` + + +class QGraphicsGridLayout <> + +class QGraphicsItem <> + +class QGraphicsLayout <> + +class QGraphicsLayoutItem <> + +class QGraphicsObject <> + +class QGraphicsScene <> { + items +} + +class QGraphicsWidget <> + +class QGraphicsView <> { + scene +} + +class QObject <> + +class QPaintDevice <> + +class QWidget <> + + +'----------- PyQtGraph package ----------` + + +class GraphicsItem <> + +class GraphicsLayout <> { + layout +} + +class GraphicsLayoutWidget <> { + graphicsLayout +} + +class GraphicsObject <> + +class GraphicsView <> + +class GraphicsWidget <> + +class ImageItem <> + +class ImageView <> { + graphicsView + imageItem + scene + view +} + +class PlotCurveItem <> + +class PlotDataItem <> { + curve + scatter +} + +class PlotItem <> { + layout + vb +} + +class PlotWidget <> { + plotItem +} + +class ScatterPlotItem <> + +class ViewBox <> + + +'---------- Inheritance within Qt ----------' +QObject <|-- QGraphicsObject +QGraphicsItem <|-- QGraphicsObject +QGraphicsObject <|-- QGraphicsWidget +QGraphicsLayoutItem <|-- QGraphicsWidget +QGraphicsLayoutItem <|-- QGraphicsLayout +QGraphicsLayout <|-- QGraphicsGridLayout +QPaintDevice <|-- QWidget +QObject <|-- QWidget +QObject <|-- QGraphicsScene +QWidget <|-- QGraphicsView + + +'---------- Inheritance from Qt to PyQtGraph ----------' +QGraphicsWidget <|-- GraphicsWidget +QGraphicsObject <|-- GraphicsObject +QGraphicsView <|-- GraphicsView +QWidget <|-- ImageView + + +'---------- Inheritance within PyQtGraph ----------' +GraphicsItem <|-- GraphicsObject +GraphicsItem <|-- GraphicsWidget +GraphicsWidget <|-- GraphicsLayout +GraphicsWidget <|-- PlotItem +GraphicsWidget <|-- ViewBox +GraphicsObject <|-- ScatterPlotItem +GraphicsObject <|-- PlotCurveItem +GraphicsObject <|-- ImageItem +GraphicsObject <|-- PlotDataItem +GraphicsView <|-- PlotWidget +GraphicsView <|-- GraphicsLayoutWidget + + +'---------- Aggregation ----------' + +' Shorter arrow so at same height in the diagram +QGraphicsScene::items o- QGraphicsItem #b8b8b8 +QGraphicsView::scene o- QGraphicsScene #b8b8b8 + +' Longer (regular) arrows +PlotWidget::plotItem o-- PlotItem #b8b8b8 +GraphicsLayoutWidget::graphicsLayout o-- GraphicsLayout #b8b8b8 +PlotDataItem::curve o-- PlotCurveItem #b8b8b8 +PlotDataItem::scatter o-- ScatterPlotItem #b8b8b8 +PlotItem::vb o-- ViewBox #b8b8b8 +PlotItem::layout o-- QGraphicsGridLayout #b8b8b8 +GraphicsLayout::layout o-- QGraphicsGridLayout #b8b8b8 +ImageView::graphicsView o-- GraphicsView #b8b8b8 +ImageView::imageItem o-- ImageItem #b8b8b8 +ImageView::scene o-- QGraphicsScene #b8b8b8 +ImageView::view o-- ViewBox #b8b8b8 + + +@enduml From c9c89db0db465144fb5707934a7147c7e20b9a58 Mon Sep 17 00:00:00 2001 From: Inigo Montoya <5187375+Wubbzi@users.noreply.github.com> Date: Mon, 26 Sep 2022 23:55:24 -0500 Subject: [PATCH 050/306] change the libOrder to favor Qt6 (#2157) * change the libOrder to favor Qt6 * remove default option from binding combo in the UI file and regen template files * set binding combobox item with text matching QT_LIB to be the selected item when launched * remove the font weights QtCreator forced into the UI file. * add option for Qt.GlobalColor to mkColor * find installed bindings. If a binding is not installed, disable, change the color, and set a tooltip on that item in the combobox. * simplify loadFile method * set codeView tabStopDistance to 4 spaces. * create method to set codeView tabStopDistance to 4 spaces. set at runtime and when font size is changed. * switch pkg_resources for pkgutil and remove GlobalColor option from mkColor * workaround for bug that happens when PyQt6 is uninstalled. * remove bindings from combobox in ui files. * add items to combobox in ExampleApp.py * Undo changes to functions.py * Do not specify the font width * Make sure QT_LIB is available Co-authored-by: Ognyan Moore --- pyqtgraph/Qt/__init__.py | 6 +-- pyqtgraph/examples/ExampleApp.py | 46 ++++++++++++++----- pyqtgraph/examples/exampleLoaderTemplate.ui | 28 +---------- .../examples/exampleLoaderTemplate_generic.py | 14 +----- 4 files changed, 40 insertions(+), 54 deletions(-) diff --git a/pyqtgraph/Qt/__init__.py b/pyqtgraph/Qt/__init__.py index 65f84c98af..ebc60d969f 100644 --- a/pyqtgraph/Qt/__init__.py +++ b/pyqtgraph/Qt/__init__.py @@ -5,7 +5,6 @@ * Allow you to import QtCore/QtGui from pyqtgraph.Qt without specifying which Qt wrapper you want to use. """ - import os import re import subprocess @@ -34,7 +33,7 @@ ## is already imported. If not, then attempt to import in the order ## specified in libOrder. if QT_LIB is None: - libOrder = [PYQT5, PYSIDE2, PYSIDE6, PYQT6] + libOrder = [PYQT6, PYSIDE6, PYQT5, PYSIDE2] for lib in libOrder: if lib in sys.modules: @@ -43,8 +42,9 @@ if QT_LIB is None: for lib in libOrder: + qt = lib + '.QtCore' try: - __import__(lib) + __import__(qt) QT_LIB = lib break except ImportError: diff --git a/pyqtgraph/examples/ExampleApp.py b/pyqtgraph/examples/ExampleApp.py index 1e6e393e63..0385ea911a 100644 --- a/pyqtgraph/examples/ExampleApp.py +++ b/pyqtgraph/examples/ExampleApp.py @@ -1,5 +1,6 @@ import keyword import os +import pkgutil import re import subprocess import sys @@ -8,7 +9,7 @@ from functools import lru_cache import pyqtgraph as pg -from pyqtgraph.Qt import QtCore, QtGui, QtWidgets +from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets app = pg.mkQApp() @@ -16,10 +17,8 @@ path = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, path) -import utils - import exampleLoaderTemplate_generic as ui_template - +import utils # based on https://github.com/art1415926535/PyQt5-syntax-highlighting @@ -299,6 +298,10 @@ def unnestedDict(exDict): class ExampleLoader(QtWidgets.QMainWindow): + # update qtLibCombo item order to match bindings in the UI file and recreate + # the templates files if you change bindings. + bindings = {'PyQt6': 0, 'PySide6': 1, 'PyQt5': 2, 'PySide2': 3} + modules = tuple(m.name for m in pkgutil.iter_modules()) def __init__(self): QtWidgets.QMainWindow.__init__(self) self.ui = ui_template.Ui_Form() @@ -320,6 +323,9 @@ def __init__(self): textFil = self.ui.exampleFilter self.curListener = None self.ui.exampleFilter.setFocus() + self.ui.qtLibCombo.addItems(self.bindings.keys()) + self.ui.qtLibCombo.setCurrentIndex(self.bindings[QT_LIB]) + def onComboChanged(searchType): if self.curListener is not None: @@ -353,6 +359,27 @@ def onComboChanged(searchType): self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile) self.ui.codeView.textChanged.connect(self.onTextChange) self.codeBtn.clicked.connect(self.runEditedCode) + self.updateCodeViewTabWidth(self.ui.codeView.font()) + + def updateCodeViewTabWidth(self,font): + """ + Change the codeView tabStopDistance to 4 spaces based on the size of the current font + """ + fm = QtGui.QFontMetrics(font) + tabWidth = fm.horizontalAdvance(' ' * 4) + # the default value is 80 pixels! that's more than 2x what we want. + self.ui.codeView.setTabStopDistance(tabWidth) + + def showEvent(self, event) -> None: + super(ExampleLoader, self).showEvent(event) + disabledColor = QColor(QtCore.Qt.GlobalColor.red) + for name, idx in self.bindings.items(): + disableBinding = name not in self.modules + if disableBinding: + item = self.ui.qtLibCombo.model().item(idx) + item.setData(disabledColor, QtCore.Qt.ItemDataRole.ForegroundRole) + item.setEnabled(False) + item.setToolTip(f'{item.text()} is not installed') def onTextChange(self): """ @@ -484,14 +511,8 @@ def currentFile(self): return None def loadFile(self, edited=False): - - qtLib = str(self.ui.qtLibCombo.currentText()) - - env = None - if qtLib != 'default': - env = dict(os.environ, PYQTGRAPH_QT_LIB=qtLib) - else: - env = dict(os.environ) + qtLib = self.ui.qtLibCombo.currentText() + env = dict(os.environ, PYQTGRAPH_QT_LIB=qtLib) example_path = os.path.abspath(os.path.dirname(__file__)) path = os.path.dirname(os.path.dirname(example_path)) env['PYTHONPATH'] = f'{path}' @@ -556,6 +577,7 @@ def keyPressEvent(self, event): # Reset to original size font.setPointSize(10) self.ui.codeView.setFont(font) + self.updateCodeViewTabWidth(font) event.accept() def main(): diff --git a/pyqtgraph/examples/exampleLoaderTemplate.ui b/pyqtgraph/examples/exampleLoaderTemplate.ui index 22a213e680..a462b633e4 100644 --- a/pyqtgraph/examples/exampleLoaderTemplate.ui +++ b/pyqtgraph/examples/exampleLoaderTemplate.ui @@ -22,33 +22,7 @@ - - - - default - - - - - PyQt5 - - - - - PySide2 - - - - - PySide6 - - - - - PyQt6 - - - + diff --git a/pyqtgraph/examples/exampleLoaderTemplate_generic.py b/pyqtgraph/examples/exampleLoaderTemplate_generic.py index bf87b31f94..5219ba2b63 100644 --- a/pyqtgraph/examples/exampleLoaderTemplate_generic.py +++ b/pyqtgraph/examples/exampleLoaderTemplate_generic.py @@ -1,6 +1,6 @@ -# Form implementation generated from reading ui file 'examples/exampleLoaderTemplate.ui' +# Form implementation generated from reading ui file '../pyqtgraph/examples/exampleLoaderTemplate.ui' # -# Created by: PyQt6 UI code generator 6.1.1 +# Created by: PyQt6 UI code generator 6.2.2 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. @@ -25,11 +25,6 @@ def setupUi(self, Form): self.gridLayout.setObjectName("gridLayout") self.qtLibCombo = QtWidgets.QComboBox(self.layoutWidget) self.qtLibCombo.setObjectName("qtLibCombo") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") self.gridLayout.addWidget(self.qtLibCombo, 4, 1, 1, 1) self.loadBtn = QtWidgets.QPushButton(self.layoutWidget) self.loadBtn.setObjectName("loadBtn") @@ -77,11 +72,6 @@ def setupUi(self, Form): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "PyQtGraph")) - self.qtLibCombo.setItemText(0, _translate("Form", "default")) - self.qtLibCombo.setItemText(1, _translate("Form", "PyQt5")) - self.qtLibCombo.setItemText(2, _translate("Form", "PySide2")) - self.qtLibCombo.setItemText(3, _translate("Form", "PySide6")) - self.qtLibCombo.setItemText(4, _translate("Form", "PyQt6")) self.loadBtn.setText(_translate("Form", "Run Example")) self.label.setText(_translate("Form", "Qt Library:")) self.exampleFilter.setPlaceholderText(_translate("Form", "Type to filter...")) From 759c13f12b05b9a28e49349bb8121590e44dfb40 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 27 Sep 2022 13:14:37 -0400 Subject: [PATCH 051/306] More robust action template handling in `interact` --- pyqtgraph/parametertree/interactive.py | 17 +++++++++-- tests/parametertree/test_Parameter.py | 40 +++++++++++++++++++++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 3284703464..b46d4264fc 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -204,7 +204,7 @@ def __repr__(self): class Interactor: - runOptions = [RunOptions.ON_CHANGED, RunOptions.ON_ACTION] + runOptions = RunOptions.ON_ACTION parent = None titleFormat = None nest = True @@ -283,6 +283,7 @@ def interact( parent=PARAM_UNSET, titleFormat=PARAM_UNSET, nest=PARAM_UNSET, + runActionTemplate=PARAM_UNSET, existOk=PARAM_UNSET, **overrides, ): @@ -318,6 +319,13 @@ def interact( and arguments to that function are 'nested' inside as its children. If *False*, function arguments are directly added to this parameter instead of being placed inside a child GroupParameter + runActionTemplate: dict + Template for the action parameter which runs the function, used + if ``runOptions`` is set to ``GroupParameter.RUN_ACTION``. Note that + if keys like "name" or "type" are not included, they are inferred + from the previous / default ``runActionTemplate``. This allows + items that should only be set per-function to exist here, like + a ``shortcut`` or ``icon``. existOk: bool Whether it is OK for existing parameter names to bind to this function. See behavior during 'Parameter.insertChild' @@ -328,15 +336,18 @@ def interact( override can be a value (e.g. 5) or a dict specification of a parameter (e.g. dict(type='list', limits=[0, 10, 20])) """ + # Special case: runActionTemplate can be overridden to specify action + if runActionTemplate is not PARAM_UNSET: + runActionTemplate = {**self.runActionTemplate, **runActionTemplate} # Get every overridden default locs = locals() # Everything until action template opts = { - kk: locs[kk] for kk in self._optionNames[:-1] if locs[kk] is not PARAM_UNSET + kk: locs[kk] for kk in self._optionNames if locs[kk] is not PARAM_UNSET } oldOpts = self.setOpts(**opts) # Delete explicitly since correct values are now ``self`` attributes - del runOptions, titleFormat, nest, existOk, parent + del runOptions, titleFormat, nest, existOk, parent, runActionTemplate function = self._toInteractiveFunction(function) funcDict = self.functionToParameterDict(function.function, **overrides) diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 76d41b69e9..dd8f1bbd90 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -1,13 +1,18 @@ -import pytest from functools import wraps -from pyqtgraph.parametertree import Parameter -from pyqtgraph.parametertree.parameterTypes import GroupParameter as GP + +import numpy as np +import pytest + +from pyqtgraph import functions as fn from pyqtgraph.parametertree import ( - RunOptions, InteractiveFunction, Interactor, interact, + RunOptions, ) +from pyqtgraph.parametertree import Parameter +from pyqtgraph.parametertree.parameterTypes import GroupParameter as GP +from pyqtgraph.Qt import QtGui def test_parameter_hasdefault(): @@ -307,7 +312,7 @@ class RetainVal: def inner(a=4): RetainVal.a = a - host = interact(inner) + host = interact(inner, runOptions=RunOptions.ON_CHANGED) host["a"] = 5 assert RetainVal.a == 5 @@ -433,10 +438,31 @@ def c(self, z=5): def test_args_interact(): - @interact.decorate() def a(*args): """""" assert not (a.parameters or a.extra) - a() \ No newline at end of file + a() + + +def test_interact_with_icon(): + randomPixmap = QtGui.QPixmap(64, 64) + randomPixmap.fill(QtGui.QColor("red")) + + parent = Parameter.create(name="parent", type="group") + + @interact.decorate( + runActionTemplate=dict(icon=randomPixmap), + parent=parent, + runOptions=RunOptions.ON_ACTION, + ) + def a(): + """""" + + groupItem = parent.child("a").itemClass(parent.child("a"), 1) + buttonPixmap = groupItem.button.icon().pixmap(randomPixmap.size()) + imageBytes = [ + fn.ndarray_from_qimage(pix.toImage()) for pix in (randomPixmap, buttonPixmap) + ] + assert np.array_equal(*imageBytes) From 3329c840f9fe46d49f926527e2779e43238a79c8 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 27 Sep 2022 13:14:50 -0400 Subject: [PATCH 052/306] Add missing line of test coverage --- tests/parametertree/test_Parameter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index dd8f1bbd90..fe478ec684 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -275,6 +275,11 @@ def myfunc(a=5): assert not interactive.setDisconnected(True) assert interactive.setDisconnected(False) + host = interact(interactive, runOptions=RunOptions.ON_CHANGED) + interactive.disconnect() + host["a"] = 20 + assert value == 10 + def test_badOptsContext(): with pytest.raises(KeyError): From ba48c82c1382805a246638e6f11ab6e9c551c786 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 27 Sep 2022 13:29:41 -0400 Subject: [PATCH 053/306] Add documentation for `runActionTemplate` --- .../parametertree/interactiveparameters.rst | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/source/parametertree/interactiveparameters.rst b/doc/source/parametertree/interactiveparameters.rst index 1b08bd145b..b338cf1fed 100644 --- a/doc/source/parametertree/interactiveparameters.rst +++ b/doc/source/parametertree/interactiveparameters.rst @@ -220,6 +220,30 @@ should be directly inside the parent, use ``nest=False``: # directly as children of `parent` params = interact(a, nest=False) +``runActionTemplate`` +^^^^^^^^^^^^^^^^^^^^^ +When the ``runOptions`` argument is set to (or contains) ``RunOptions.ON_ACTION``, a +button will be added next to the parameter group which can be clicked to run the +function with the current parameter values. The button's options can be customized +through passing a dictionary to ``runActionTemplate``. The dictionary can contain +any key accepted as an ``action`` parameter option. For instance, to run a function +either by pressing the button or a shortcut, you can interact like so: + +.. code:: python + + def a(x=5, y=6): + return x + y + + # The button will be labeled "Run" and will run the function when clicked or when + # the shortcut "Ctrl+R" is pressed + params = interact(a, runActionTemplate={'shortcut': 'Ctrl+R'}) + + # Alternatively, add an icon to the button + params = interact(a, runActionTemplate={'icon': 'run.png'}) + + # Why not both? + params = interact(a, runActionTemplate={'icon': 'run.png', 'shortcut': 'Ctrl+R'}) + ``existOk`` ^^^^^^^^^^^ From dd57006b2c5e13ff548f83735daeb822f398b671 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Tue, 27 Sep 2022 11:25:26 -0700 Subject: [PATCH 054/306] Prep for 0.13.0 release (#2431) * Update changelog * Bump version --- CHANGELOG | 131 ++++++++++++++++++++++++++++++++++++++++++ pyqtgraph/__init__.py | 2 +- 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 356f5e6c6d..e30ec95bb1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,134 @@ +pyqtgraph-0.13.0 + +## What's Changed + +Highlights + +* With PyQt6 6.3.2+ PyQtGraph uses sip.array, which leads to significantly faster draw performance by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2314 +* Introducing "interactive" parameter trees by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2318 +* Minimum Qt version now 5.15 for Qt5 and 6.2+ for Qt6 by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2403 +* with `enableExperimental` pyqtgraph accesses QPainterPathPrivate for faster QPainterPath generation by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2324 + +New Features + +* Interactive params fixup by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2318 +* Added possibility to use custom dock labels by @ardiloot in https://github.com/pyqtgraph/pyqtgraph/pull/2274 +* Introduce API option to control whether lines are drawn as segmented lines by @swvanbuuren in https://github.com/pyqtgraph/pyqtgraph/pull/2185 +* access QPainterPathPrivate for faster arrayToQPath by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2324 +* Update LabelItem to allow transparency in the text by @ElpadoCan in https://github.com/pyqtgraph/pyqtgraph/pull/2300 +* Make parameter tree read-only values selectable and copiable by @ardiloot in https://github.com/pyqtgraph/pyqtgraph/pull/2311 +* Have CSV exporter export error bar information by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2405 +* map pyqtgraph symbols to a matplotlib equivalent by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2395 + +Performance Improvements + +* Improve performance of PlotCurveItem with QOpenGLWidget by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2264 +* ScatterPlotItem: use Format_ARGB32_Premultiplied by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2317 +* access QPainterPathPrivate for faster arrayToQPath by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2324 +* make use of PyQt sip.array by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2314 + +Bug Fixes + +* Fix GLImageItem regression by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2232 +* Fixed the app crash on right clicked by @Cosmicoppai in https://github.com/pyqtgraph/pyqtgraph/pull/2236 +* Fix Regression in in ViewBox.updateScaleBox Caused by #2034 by @campagnola in https://github.com/pyqtgraph/pyqtgraph/pull/2241 +* Fix UFuncTypeError when plotting integer data on windows by @campagnola in https://github.com/pyqtgraph/pyqtgraph/pull/2249 +* Fixed division by zero when no pixmap is loaded by @StSav012 in https://github.com/pyqtgraph/pyqtgraph/pull/2275 +* Ensure in PlotCurveItem lookup occurs in tuple, not str by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2294 +* Fixed a crash when `step` option is missing by @StSav012 in https://github.com/pyqtgraph/pyqtgraph/pull/2261 +* Invalidate cached properties on geometryChanged signal by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2357 +* Bugfix: Handle example search failure due to bad regex by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2121 +* Address #2303 unapplied pen parameter constructor options by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2305 +* Issue #2203 Potential Fix: Disabled FlowchartCtrlWidget.nodeRenamed o… by @HallowedDust5 in https://github.com/pyqtgraph/pyqtgraph/pull/2301 +* Fix #2289 unwanted growing in scene context menu by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2306 +* #2283 delete limitation by rectangle width ROI by @sasha-sem in https://github.com/pyqtgraph/pyqtgraph/pull/2285 +* Update exception handling to catch exceptions in threads (py3 change) by @campagnola in https://github.com/pyqtgraph/pyqtgraph/pull/2309 +* Fix PlotCurveItem errors when pen=None by @campagnola in https://github.com/pyqtgraph/pyqtgraph/pull/2315 +* ScatterPlotItem point masking fix by @ardiloot in https://github.com/pyqtgraph/pyqtgraph/pull/2310 +* Use property to lazily declare rectangle by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2356 +* Fix missing import in Flowchart.py by @Puff-Machine in https://github.com/pyqtgraph/pyqtgraph/pull/2421 +* Fix doubling labels in DateAxisItem by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2413 +* GridItem: Fix pen for usage of dash-pattern by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2304 +* Update PColorMeshItem.py by @LarsVoxen in https://github.com/pyqtgraph/pyqtgraph/pull/2327 +* Fix infinite loop within DateAxisItem by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2365 +* Fix GraphicsScene.itemsNearEvent and setClickRadius by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2383 +* Modify MatplotlibWidget to accept QWidget super constructor parameters. by @Dolphindalt in https://github.com/pyqtgraph/pyqtgraph/pull/2366 +* Fix flickering, when panning/scrolling in a fully zoomed-out view by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2387 +* Make auto downsample factor calculation more robust by @StSav012 in https://github.com/pyqtgraph/pyqtgraph/pull/2253 +* Fix : QPoint() no longer accepts float arguments by @campagnola in https://github.com/pyqtgraph/pyqtgraph/pull/2260 +* avoid double __init__ of DockDrop by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2286 +* Add a few ImageView improvements by @outofculture in https://github.com/pyqtgraph/pyqtgraph/pull/1828 + +API/Behavior Changes + +* remove border QGraphicsRectItem from scene by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2225 +* Introduce API option to control whether lines are drawn as segmented lines by @swvanbuuren in https://github.com/pyqtgraph/pyqtgraph/pull/2185 +* Modify CSV exporter to output original data without log mapping by @NilsNemitz in https://github.com/pyqtgraph/pyqtgraph/pull/2297 +* Expose useCache ScatterPlotItem option from plot method by @ibrewster in https://github.com/pyqtgraph/pyqtgraph/pull/2258 +* remove legend items manually from scene by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2368 +* `getHistogramRange` for `HistogramLUTItem` by @kremeyer in https://github.com/pyqtgraph/pyqtgraph/pull/2397 +* Axis pen improvements by @ibrewster in https://github.com/pyqtgraph/pyqtgraph/pull/2398 +* remove MatplotlibWidget from pg namespace by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2400 +* change the libOrder to favor Qt6 by @Wubbzi in https://github.com/pyqtgraph/pyqtgraph/pull/2157 + +Examples + +* Added Jupyter console widget and Example by @jonmatthis in https://github.com/pyqtgraph/pyqtgraph/pull/2353 +* Add glow example by @edumur in https://github.com/pyqtgraph/pyqtgraph/pull/2242 +* update multiplePlotSpeedTest.py to use PlotCurveItem instead of QGraphicsPathItem by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2426 + +Docs + +* Add GLTextItem to docs by @jebguth in https://github.com/pyqtgraph/pyqtgraph/pull/2419 +* Add logo to docs by @ixjlyons in https://github.com/pyqtgraph/pyqtgraph/pull/2384 +* Enable nit-picky mode in documentation and fix associated warnings by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/1753 +* Added UML class diagram to give overview of the most important classes by @titusjan in https://github.com/pyqtgraph/pyqtgraph/pull/1631 + +Other + +* Remove old Qt workarounds by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2224 +* Track when ScatterPlotItem clears the tooltip, only clear when needed by @ixjlyons in https://github.com/pyqtgraph/pyqtgraph/pull/2235 +* Avoid import error in HDF5 exporter by @campagnola in https://github.com/pyqtgraph/pyqtgraph/pull/2259 +* test enum using : "enums & enum" by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2250 +* add support for PySide6 6.3.0 QOverrideCursorGuard by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2263 +* Used the power of `blockIfUnchanged` decorator by @StSav012 in https://github.com/pyqtgraph/pyqtgraph/pull/2181 +* Handle/remove unused variables by @ksunden in https://github.com/pyqtgraph/pyqtgraph/pull/2094 +* BusyCursor and QPainter fixes for PyPy by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2349 +* Add a pyi stub file to import best-guess pyqt type hints by @outofculture in https://github.com/pyqtgraph/pyqtgraph/pull/2358 +* test_PlotCurveItem: unset skipFiniteCheck for next test by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2313 +* lazy create the rectangle selection item by @danielhrisca in https://github.com/pyqtgraph/pyqtgraph/pull/2168 +* fix: super().__init__ does not need self by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2359 +* Promote interactive `Run` action to group level by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2414 +* Enhance testing for creating parameters from saved states by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2319 +* add support for PySide6's usage of Python Enums by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2329 +* remove QFileDialog PyQt4 compatibility code by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2394 +* Bugfix: `interact` on decorated method that uses `self`. by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2408 +* use single generic template for all bindings by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2226 +* `interact()` defaults to `ON_ACTION` behavior and accepts `runActionTemplate` argument by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2432 + +## New Contributors +* @andriyor made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2212 +* @keziah55 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2191 +* @Cosmicoppai made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2236 +* @bbc131 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2264 +* @StSav012 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2181 +* @ardiloot made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2274 +* @sasha-sem made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2285 +* @swvanbuuren made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2185 +* @Anatoly1010 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2330 +* @LarsVoxen made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2327 +* @HallowedDust5 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2301 +* @ElpadoCan made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2300 +* @dependabot made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2342 +* @jaj42 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2389 +* @Dolphindalt made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2366 +* @kremeyer made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2397 +* @jonmatthis made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2353 +* @jebguth made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2419 +* @Puff-Machine made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2421 + +**Full Changelog**: https://github.com/pyqtgraph/pyqtgraph/compare/pyqtgraph-0.12.4...pyqtgraph-0.13.0 + pyqtgraph-0.12.4 Highlights: diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index fba4caf55f..404e30772e 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.12.4.dev0' +__version__ = '0.13.0' ### import all the goodies and add some helper functions for easy CLI use From 12a26013a756303020e9bff79f84f049d53659d8 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Tue, 27 Sep 2022 12:41:23 -0700 Subject: [PATCH 055/306] Update to dev version --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 404e30772e..141cb80d57 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.0' +__version__ = '0.13.0.dev0' ### import all the goodies and add some helper functions for easy CLI use From 98cefa68977f84109237bcec748b173a19535db8 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Tue, 27 Sep 2022 18:49:37 -0700 Subject: [PATCH 056/306] Move console template from binding specific to generic --- pyqtgraph/console/Console.py | 7 +- ...{template_pyqt6.py => template_generic.py} | 0 pyqtgraph/console/template_pyqt5.py | 114 ------------- pyqtgraph/console/template_pyside2.py | 113 ------------- pyqtgraph/console/template_pyside6.py | 155 ------------------ 5 files changed, 2 insertions(+), 387 deletions(-) rename pyqtgraph/console/{template_pyqt6.py => template_generic.py} (100%) delete mode 100644 pyqtgraph/console/template_pyqt5.py delete mode 100644 pyqtgraph/console/template_pyside2.py delete mode 100644 pyqtgraph/console/template_pyside6.py diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 06f19e0e61..0b3381552e 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,4 +1,3 @@ -import importlib import pickle import re import subprocess @@ -8,10 +7,8 @@ from .. import exceptionHandling as exceptionHandling from .. import getConfigOption from ..functions import SignalBlock -from ..Qt import QT_LIB, QtCore, QtGui, QtWidgets - -ui_template = importlib.import_module( - f'.template_{QT_LIB.lower()}', package=__package__) +from ..Qt import QtCore, QtGui, QtWidgets +from . import template_generic as ui_template class ConsoleWidget(QtWidgets.QWidget): diff --git a/pyqtgraph/console/template_pyqt6.py b/pyqtgraph/console/template_generic.py similarity index 100% rename from pyqtgraph/console/template_pyqt6.py rename to pyqtgraph/console/template_generic.py diff --git a/pyqtgraph/console/template_pyqt5.py b/pyqtgraph/console/template_pyqt5.py deleted file mode 100644 index f72ff241d9..0000000000 --- a/pyqtgraph/console/template_pyqt5.py +++ /dev/null @@ -1,114 +0,0 @@ - -# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui' -# -# Created by: PyQt5 UI code generator 5.5.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(739, 497) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setObjectName("verticalLayout") - self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) - font = QtGui.QFont() - font.setFamily("Monospace") - self.output.setFont(font) - self.output.setReadOnly(True) - self.output.setObjectName("output") - self.verticalLayout.addWidget(self.output) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.input = CmdInput(self.layoutWidget) - self.input.setObjectName("input") - self.horizontalLayout.addWidget(self.input) - self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) - self.historyBtn.setCheckable(True) - self.historyBtn.setObjectName("historyBtn") - self.horizontalLayout.addWidget(self.historyBtn) - self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.exceptionBtn.setCheckable(True) - self.exceptionBtn.setObjectName("exceptionBtn") - self.horizontalLayout.addWidget(self.exceptionBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - self.historyList = QtWidgets.QListWidget(self.splitter) - font = QtGui.QFont() - font.setFamily("Monospace") - self.historyList.setFont(font) - self.historyList.setObjectName("historyList") - self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) - self.exceptionGroup.setObjectName("exceptionGroup") - self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) - self.gridLayout_2.setHorizontalSpacing(2) - self.gridLayout_2.setVerticalSpacing(0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) - self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchAllExceptionsBtn.setCheckable(True) - self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") - self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) - self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchNextExceptionBtn.setCheckable(True) - self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") - self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) - self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.onlyUncaughtCheck.setChecked(True) - self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) - self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) - self.exceptionStackList.setAlternatingRowColors(True) - self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) - self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.runSelectedFrameCheck.setChecked(True) - self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) - self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) - self.exceptionInfoLabel.setWordWrap(True) - self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) - self.label = QtWidgets.QLabel(self.exceptionGroup) - self.label.setObjectName("label") - self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) - self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) - self.filterText.setObjectName("filterText") - self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Console")) - self.historyBtn.setText(_translate("Form", "History..")) - self.exceptionBtn.setText(_translate("Form", "Exceptions..")) - self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) - self.clearExceptionBtn.setText(_translate("Form", "Clear Stack")) - self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) - self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) - self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) - self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) - self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace")) - self.label.setText(_translate("Form", "Filter (regex):")) - -from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside2.py b/pyqtgraph/console/template_pyside2.py deleted file mode 100644 index 4b26a3b886..0000000000 --- a/pyqtgraph/console/template_pyside2.py +++ /dev/null @@ -1,113 +0,0 @@ - -# Form implementation generated from reading ui file 'template.ui' -# -# Created: Sun Sep 18 19:19:10 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(694, 497) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) - font = QtGui.QFont() - font.setFamily("Monospace") - self.output.setFont(font) - self.output.setReadOnly(True) - self.output.setObjectName("output") - self.verticalLayout.addWidget(self.output) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.input = CmdInput(self.layoutWidget) - self.input.setObjectName("input") - self.horizontalLayout.addWidget(self.input) - self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) - self.historyBtn.setCheckable(True) - self.historyBtn.setObjectName("historyBtn") - self.horizontalLayout.addWidget(self.historyBtn) - self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.exceptionBtn.setCheckable(True) - self.exceptionBtn.setObjectName("exceptionBtn") - self.horizontalLayout.addWidget(self.exceptionBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - self.historyList = QtWidgets.QListWidget(self.splitter) - font = QtGui.QFont() - font.setFamily("Monospace") - self.historyList.setFont(font) - self.historyList.setObjectName("historyList") - self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) - self.exceptionGroup.setObjectName("exceptionGroup") - self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) - self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchAllExceptionsBtn.setCheckable(True) - self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") - self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) - self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchNextExceptionBtn.setCheckable(True) - self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") - self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) - self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.onlyUncaughtCheck.setChecked(True) - self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) - self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) - self.exceptionStackList.setAlternatingRowColors(True) - self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) - self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.runSelectedFrameCheck.setChecked(True) - self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) - self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) - self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) - self.label = QtWidgets.QLabel(self.exceptionGroup) - self.label.setObjectName("label") - self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) - self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) - self.filterText.setObjectName("filterText") - self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Console", None, -1)) - self.historyBtn.setText(QtWidgets.QApplication.translate("Form", "History..", None, -1)) - self.exceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Exceptions..", None, -1)) - self.exceptionGroup.setTitle(QtWidgets.QApplication.translate("Form", "Exception Handling", None, -1)) - self.clearExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Clear Exception", None, -1)) - self.catchAllExceptionsBtn.setText(QtWidgets.QApplication.translate("Form", "Show All Exceptions", None, -1)) - self.catchNextExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Show Next Exception", None, -1)) - self.onlyUncaughtCheck.setText(QtWidgets.QApplication.translate("Form", "Only Uncaught Exceptions", None, -1)) - self.runSelectedFrameCheck.setText(QtWidgets.QApplication.translate("Form", "Run commands in selected stack frame", None, -1)) - self.exceptionInfoLabel.setText(QtWidgets.QApplication.translate("Form", "Exception Info", None, -1)) - self.label.setText(QtWidgets.QApplication.translate("Form", "Filter (regex):", None, -1)) - -from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside6.py b/pyqtgraph/console/template_pyside6.py deleted file mode 100644 index 2f07c2c0ce..0000000000 --- a/pyqtgraph/console/template_pyside6.py +++ /dev/null @@ -1,155 +0,0 @@ - -################################################################################ -## Form generated from reading UI file 'template.ui' -## -## Created by: Qt User Interface Compiler version 6.1.0 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import * -from PySide6.QtGui import * -from PySide6.QtWidgets import * - -from .CmdInput import CmdInput - - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(739, 497) - self.gridLayout = QGridLayout(Form) - self.gridLayout.setSpacing(0) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName(u"gridLayout") - self.splitter = QSplitter(Form) - self.splitter.setObjectName(u"splitter") - self.splitter.setOrientation(Qt.Vertical) - self.layoutWidget = QWidget(self.splitter) - self.layoutWidget.setObjectName(u"layoutWidget") - self.verticalLayout = QVBoxLayout(self.layoutWidget) - self.verticalLayout.setObjectName(u"verticalLayout") - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.output = QPlainTextEdit(self.layoutWidget) - self.output.setObjectName(u"output") - font = QFont() - font.setFamilies([u"Monospace"]) - self.output.setFont(font) - self.output.setReadOnly(True) - - self.verticalLayout.addWidget(self.output) - - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.input = CmdInput(self.layoutWidget) - self.input.setObjectName(u"input") - - self.horizontalLayout.addWidget(self.input) - - self.historyBtn = QPushButton(self.layoutWidget) - self.historyBtn.setObjectName(u"historyBtn") - self.historyBtn.setCheckable(True) - - self.horizontalLayout.addWidget(self.historyBtn) - - self.exceptionBtn = QPushButton(self.layoutWidget) - self.exceptionBtn.setObjectName(u"exceptionBtn") - self.exceptionBtn.setCheckable(True) - - self.horizontalLayout.addWidget(self.exceptionBtn) - - - self.verticalLayout.addLayout(self.horizontalLayout) - - self.splitter.addWidget(self.layoutWidget) - self.historyList = QListWidget(self.splitter) - self.historyList.setObjectName(u"historyList") - self.historyList.setFont(font) - self.splitter.addWidget(self.historyList) - self.exceptionGroup = QGroupBox(self.splitter) - self.exceptionGroup.setObjectName(u"exceptionGroup") - self.gridLayout_2 = QGridLayout(self.exceptionGroup) - self.gridLayout_2.setObjectName(u"gridLayout_2") - self.gridLayout_2.setHorizontalSpacing(2) - self.gridLayout_2.setVerticalSpacing(0) - self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) - self.clearExceptionBtn = QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setObjectName(u"clearExceptionBtn") - self.clearExceptionBtn.setEnabled(False) - - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) - - self.catchAllExceptionsBtn = QPushButton(self.exceptionGroup) - self.catchAllExceptionsBtn.setObjectName(u"catchAllExceptionsBtn") - self.catchAllExceptionsBtn.setCheckable(True) - - self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) - - self.catchNextExceptionBtn = QPushButton(self.exceptionGroup) - self.catchNextExceptionBtn.setObjectName(u"catchNextExceptionBtn") - self.catchNextExceptionBtn.setCheckable(True) - - self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) - - self.onlyUncaughtCheck = QCheckBox(self.exceptionGroup) - self.onlyUncaughtCheck.setObjectName(u"onlyUncaughtCheck") - self.onlyUncaughtCheck.setChecked(True) - - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) - - self.exceptionStackList = QListWidget(self.exceptionGroup) - self.exceptionStackList.setObjectName(u"exceptionStackList") - self.exceptionStackList.setAlternatingRowColors(True) - - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) - - self.runSelectedFrameCheck = QCheckBox(self.exceptionGroup) - self.runSelectedFrameCheck.setObjectName(u"runSelectedFrameCheck") - self.runSelectedFrameCheck.setChecked(True) - - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) - - self.exceptionInfoLabel = QLabel(self.exceptionGroup) - self.exceptionInfoLabel.setObjectName(u"exceptionInfoLabel") - self.exceptionInfoLabel.setWordWrap(True) - - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) - - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - - self.gridLayout_2.addItem(self.horizontalSpacer, 0, 5, 1, 1) - - self.label = QLabel(self.exceptionGroup) - self.label.setObjectName(u"label") - - self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) - - self.filterText = QLineEdit(self.exceptionGroup) - self.filterText.setObjectName(u"filterText") - - self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) - - self.splitter.addWidget(self.exceptionGroup) - - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - - self.retranslateUi(Form) - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"Console", None)) - self.historyBtn.setText(QCoreApplication.translate("Form", u"History..", None)) - self.exceptionBtn.setText(QCoreApplication.translate("Form", u"Exceptions..", None)) - self.exceptionGroup.setTitle(QCoreApplication.translate("Form", u"Exception Handling", None)) - self.clearExceptionBtn.setText(QCoreApplication.translate("Form", u"Clear Stack", None)) - self.catchAllExceptionsBtn.setText(QCoreApplication.translate("Form", u"Show All Exceptions", None)) - self.catchNextExceptionBtn.setText(QCoreApplication.translate("Form", u"Show Next Exception", None)) - self.onlyUncaughtCheck.setText(QCoreApplication.translate("Form", u"Only Uncaught Exceptions", None)) - self.runSelectedFrameCheck.setText(QCoreApplication.translate("Form", u"Run commands in selected stack frame", None)) - self.exceptionInfoLabel.setText(QCoreApplication.translate("Form", u"Stack Trace", None)) - self.label.setText(QCoreApplication.translate("Form", u"Filter (regex):", None)) - # retranslateUi From 5161e0dce3cf158bfaa316e1f56ffd6f43d97c01 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Tue, 27 Sep 2022 18:50:46 -0700 Subject: [PATCH 057/306] Have Console use Courier New font family --- pyqtgraph/console/template.ui | 17 ++++++++++++++--- pyqtgraph/console/template_generic.py | 8 +++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index 1237b5f3b1..aba1620818 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -14,7 +14,16 @@ Console - + + 0 + + + 0 + + + 0 + + 0 @@ -31,7 +40,8 @@ - Monospace + Courier New + PreferAntialias @@ -71,7 +81,8 @@ - Monospace + Courier New + PreferAntialias diff --git a/pyqtgraph/console/template_generic.py b/pyqtgraph/console/template_generic.py index 03a1a98d3c..e013cbb6d1 100644 --- a/pyqtgraph/console/template_generic.py +++ b/pyqtgraph/console/template_generic.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt6 import QtCore, QtGui, QtWidgets +from ..Qt import QtCore, QtGui, QtWidgets class Ui_Form(object): @@ -27,7 +27,8 @@ def setupUi(self, Form): self.verticalLayout.setObjectName("verticalLayout") self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) font = QtGui.QFont() - font.setFamily("Monospace") + font.setFamily("Courier New") + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) self.output.setFont(font) self.output.setReadOnly(True) self.output.setObjectName("output") @@ -48,7 +49,8 @@ def setupUi(self, Form): self.verticalLayout.addLayout(self.horizontalLayout) self.historyList = QtWidgets.QListWidget(self.splitter) font = QtGui.QFont() - font.setFamily("Monospace") + font.setFamily("Courier New") + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) self.historyList.setFont(font) self.historyList.setObjectName("historyList") self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) From aabca831de0ede0d62e7ced8a994abee6b4fdbc2 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 15 Dec 2021 01:49:40 -0600 Subject: [PATCH 058/306] change spacing and margins in console template --- pyqtgraph/console/template.ui | 6 ++++++ pyqtgraph/console/template_generic.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index aba1620818..ac031c333e 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -36,6 +36,9 @@ + + 4 + @@ -51,6 +54,9 @@ + + 6 + diff --git a/pyqtgraph/console/template_generic.py b/pyqtgraph/console/template_generic.py index e013cbb6d1..89302a9630 100644 --- a/pyqtgraph/console/template_generic.py +++ b/pyqtgraph/console/template_generic.py @@ -24,6 +24,7 @@ def setupUi(self, Form): self.layoutWidget.setObjectName("layoutWidget") self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(4) self.verticalLayout.setObjectName("verticalLayout") self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) font = QtGui.QFont() @@ -34,6 +35,7 @@ def setupUi(self, Form): self.output.setObjectName("output") self.verticalLayout.addWidget(self.output) self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setContentsMargins(6, -1, -1, -1) self.horizontalLayout.setObjectName("horizontalLayout") self.input = CmdInput(self.layoutWidget) self.input.setObjectName("input") From 3d8d55996c8517344284408541a9d92965f96e3e Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 15 Dec 2021 02:21:31 -0600 Subject: [PATCH 059/306] Input should be executed when enter or return are pressed. --- pyqtgraph/console/CmdInput.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/console/CmdInput.py b/pyqtgraph/console/CmdInput.py index 2ef99476e7..b291cbcee1 100644 --- a/pyqtgraph/console/CmdInput.py +++ b/pyqtgraph/console/CmdInput.py @@ -21,7 +21,7 @@ def keyPressEvent(self, ev): self.setHistory(self.ptr-1) ev.accept() return - elif ev.key() == QtCore.Qt.Key.Key_Return: + elif ev.key() in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter): self.execCmd() else: super().keyPressEvent(ev) From f18d8916c93da9b5451829a3e4eea93d1f4220ba Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 28 Sep 2022 09:44:09 -0400 Subject: [PATCH 060/306] Ensure example `Interactor` objects default to `RunOptions.ON_CHANGED` --- pyqtgraph/examples/InteractiveParameter.py | 2 +- pyqtgraph/examples/PlotSpeedTest.py | 4 +++- pyqtgraph/examples/ScatterPlotSpeedTest.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/examples/InteractiveParameter.py b/pyqtgraph/examples/InteractiveParameter.py index 139669ec7f..e8955f1b95 100644 --- a/pyqtgraph/examples/InteractiveParameter.py +++ b/pyqtgraph/examples/InteractiveParameter.py @@ -33,7 +33,7 @@ def wrapper(*args, **kwargs): host = Parameter.create(name="Interactive Parameter Use", type="group") -interactor = Interactor(parent=host) +interactor = Interactor(parent=host, runOptions=RunOptions.ON_CHANGED) @interactor.decorate() diff --git a/pyqtgraph/examples/PlotSpeedTest.py b/pyqtgraph/examples/PlotSpeedTest.py index f8f4e290c7..f427f83522 100644 --- a/pyqtgraph/examples/PlotSpeedTest.py +++ b/pyqtgraph/examples/PlotSpeedTest.py @@ -73,7 +73,9 @@ def paint(self, painter, opt, widget): splitter.addWidget(pw) splitter.show() -interactor = ptree.Interactor(parent=params, nest=False) +interactor = ptree.Interactor( + parent=params, nest=False, runOptions=ptree.RunOptions.ON_CHANGED +) pw.setWindowTitle('pyqtgraph example: PlotSpeedTest') pw.setLabel('bottom', 'Index', units='B') diff --git a/pyqtgraph/examples/ScatterPlotSpeedTest.py b/pyqtgraph/examples/ScatterPlotSpeedTest.py index a6063a433f..35307f88cb 100644 --- a/pyqtgraph/examples/ScatterPlotSpeedTest.py +++ b/pyqtgraph/examples/ScatterPlotSpeedTest.py @@ -42,7 +42,9 @@ def fmt(name): return translate("ScatterPlot", name.title().strip() + ": ") -interactor = ptree.Interactor(titleFormat=fmt, nest=False, parent=param) +interactor = ptree.Interactor( + titleFormat=fmt, nest=False, parent=param, runOptions=ptree.RunOptions.ON_CHANGED +) @interactor.decorate( From e0c38679619f2b02c4da2faf20386f9671dd1cf6 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 28 Sep 2022 09:49:10 -0400 Subject: [PATCH 061/306] Fix squished parameter tree on PlotSpeedTest.py --- pyqtgraph/examples/PlotSpeedTest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/examples/PlotSpeedTest.py b/pyqtgraph/examples/PlotSpeedTest.py index f427f83522..fe6c60b8b9 100644 --- a/pyqtgraph/examples/PlotSpeedTest.py +++ b/pyqtgraph/examples/PlotSpeedTest.py @@ -164,4 +164,7 @@ def updateOptions( timer.start(0) if __name__ == '__main__': + # Splitter by default gives too small of a width to the parameter tree, + # so fix that right before the event loop + pt.setMinimumSize(225,0) pg.exec() From 08cc69176d222173684372f5da948ff40e48e0a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 13:50:08 +0000 Subject: [PATCH 062/306] Bump sphinx from 5.2.1 to 5.2.2 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.1 to 5.2.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.1...v5.2.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 4f89a21a7a..454248e869 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -sphinx==5.2.1 +sphinx==5.2.2 PyQt6==6.3.1 sphinx-rtd-theme sphinx-qt-documentation From 1e76922bc07e6e610a5831bf518c065981dd2334 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 28 Sep 2022 20:08:20 -0700 Subject: [PATCH 063/306] Have release on publish, not on created --- .github/workflows/python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index f564c120c6..1ab8d63eae 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -5,7 +5,7 @@ name: Upload Python Package on: release: - types: [created] + types: [published] jobs: deploy: From a28c77a1728f8c1c4a2c2a17823db552c2740b0b Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 28 Sep 2022 20:28:48 -0700 Subject: [PATCH 064/306] Prepare for 0.13.1 release --- CHANGELOG | 18 ++++++++++++++++++ pyqtgraph/__init__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e30ec95bb1..5a5fb5199b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,21 @@ +pyqtgraph-0.13.1 + +## What's Changed + +Bug Fixes + +* Refactor examples using `Interactor` to run on changing function parameters by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2437 + +API Change + +* deprecate GraphicsObject::parentChanged method by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2420 + +Other + +* Move Console to generic template and make font Courier New by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2435 + + + pyqtgraph-0.13.0 ## What's Changed diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 141cb80d57..3b3f6ef7a9 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.0.dev0' +__version__ = '0.13.1' ### import all the goodies and add some helper functions for easy CLI use From 2775c9c8112d6f0d07a7277138b278a42bf216fc Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 28 Sep 2022 21:03:43 -0700 Subject: [PATCH 065/306] Append dev0 to version --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 3b3f6ef7a9..59c64c3cd8 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.1' +__version__ = '0.13.1.dev0' ### import all the goodies and add some helper functions for easy CLI use From 935914143d87f28575ea9d60bd9efada57b3bdd6 Mon Sep 17 00:00:00 2001 From: Yuri Victorovich Date: Thu, 29 Sep 2022 19:25:37 -0700 Subject: [PATCH 066/306] Fix renderView to not use mremap on FreeBSD FreeBSD doesn't have mremap. --- pyqtgraph/widgets/RemoteGraphicsView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index dfb60d957b..fea33392c0 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -313,7 +313,7 @@ def renderView(self): ## it also says (sometimes) 'access is denied' if we try to reuse the tag. self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) self.shm = mmap.mmap(-1, size, self.shmtag) - elif sys.platform == 'darwin': + elif sys.platform == 'darwin' or sys.platform.startswith('freebsd'): self.shm.close() fd = self.shmFile.fileno() os.ftruncate(fd, size + 1) From 1ef011fdfc5f54f1bfda8d02e620a12b5b6bad70 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 1 Oct 2022 10:18:08 -0700 Subject: [PATCH 067/306] Remove flowchart templates --- pyqtgraph/flowchart/Flowchart.py | 20 ++-- ...t6.py => FlowchartCtrlTemplate_generic.py} | 2 +- .../flowchart/FlowchartCtrlTemplate_pyqt5.py | 66 ------------- .../FlowchartCtrlTemplate_pyside2.py | 65 ------------- .../FlowchartCtrlTemplate_pyside6.py | 88 ----------------- pyqtgraph/flowchart/FlowchartTemplate.ui | 97 ------------------- .../flowchart/FlowchartTemplate_pyqt5.py | 54 ----------- .../flowchart/FlowchartTemplate_pyqt6.py | 53 ---------- .../flowchart/FlowchartTemplate_pyside2.py | 53 ---------- .../flowchart/FlowchartTemplate_pyside6.py | 67 ------------- 10 files changed, 7 insertions(+), 558 deletions(-) rename pyqtgraph/flowchart/{FlowchartCtrlTemplate_pyqt6.py => FlowchartCtrlTemplate_generic.py} (98%) delete mode 100644 pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py delete mode 100644 pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py delete mode 100644 pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside6.py delete mode 100644 pyqtgraph/flowchart/FlowchartTemplate.ui delete mode 100644 pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py delete mode 100644 pyqtgraph/flowchart/FlowchartTemplate_pyqt6.py delete mode 100644 pyqtgraph/flowchart/FlowchartTemplate_pyside2.py delete mode 100644 pyqtgraph/flowchart/FlowchartTemplate_pyside6.py diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 542f0f366b..ac6ee95bf8 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -1,25 +1,22 @@ __init__ = ["Flowchart", "FlowchartGraphicsItem", "FlowchartNode"] import importlib -from collections import OrderedDict import os +from collections import OrderedDict -from .. import DataTreeWidget, FileDialog -from ..Qt import QT_LIB, QtCore, QtWidgets -from .Node import Node - -FlowchartCtrlTemplate = importlib.import_module( - f'.FlowchartCtrlTemplate_{QT_LIB.lower()}', package=__package__) - from numpy import ndarray +from .. import DataTreeWidget, FileDialog from .. import configfile as configfile from .. import dockarea as dockarea from .. import functions as fn from ..debug import printExc from ..graphicsItems.GraphicsObject import GraphicsObject +from ..Qt import QtCore, QtWidgets +from . import FlowchartCtrlTemplate_generic as FlowchartCtrlTemplate from . import FlowchartGraphicsView from .library import LIBRARY +from .Node import Node from .Terminal import Terminal @@ -27,8 +24,6 @@ def strDict(d): return dict([(str(k), v) for k, v in d.items()]) - - class Flowchart(Node): sigFileLoaded = QtCore.Signal(object) sigFileSaved = QtCore.Signal(object) @@ -764,10 +759,7 @@ def __init__(self, chart, ctrl): self.hoverItem = None #self.setMinimumWidth(250) #self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Expanding)) - - #self.ui = FlowchartTemplate.Ui_Form() - #self.ui.setupUi(self) - + ## build user interface (it was easier to do it here than via developer) self.view = FlowchartGraphicsView.FlowchartGraphicsView(self) self.viewDock = dockarea.Dock('view', size=(1000,600)) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt6.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_generic.py similarity index 98% rename from pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt6.py rename to pyqtgraph/flowchart/FlowchartCtrlTemplate_generic.py index 86ee801d18..a188e825b9 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt6.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_generic.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt6 import QtCore, QtGui, QtWidgets +from ..Qt import QtCore, QtGui, QtWidgets class Ui_Form(object): diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py deleted file mode 100644 index b3499bc095..0000000000 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py +++ /dev/null @@ -1,66 +0,0 @@ - -# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui' -# -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(217, 499) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setVerticalSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.loadBtn = QtWidgets.QPushButton(Form) - self.loadBtn.setObjectName("loadBtn") - self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) - self.saveBtn = FeedbackButton(Form) - self.saveBtn.setObjectName("saveBtn") - self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) - self.saveAsBtn = FeedbackButton(Form) - self.saveAsBtn.setObjectName("saveAsBtn") - self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) - self.reloadBtn = FeedbackButton(Form) - self.reloadBtn.setCheckable(False) - self.reloadBtn.setFlat(False) - self.reloadBtn.setObjectName("reloadBtn") - self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) - self.showChartBtn = QtWidgets.QPushButton(Form) - self.showChartBtn.setCheckable(True) - self.showChartBtn.setObjectName("showChartBtn") - self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) - self.ctrlList = TreeWidget(Form) - self.ctrlList.setObjectName("ctrlList") - self.ctrlList.headerItem().setText(0, "1") - self.ctrlList.header().setVisible(False) - self.ctrlList.header().setStretchLastSection(False) - self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) - self.fileNameLabel = QtWidgets.QLabel(Form) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.fileNameLabel.setFont(font) - self.fileNameLabel.setText("") - self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) - self.fileNameLabel.setObjectName("fileNameLabel") - self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "PyQtGraph")) - self.loadBtn.setText(_translate("Form", "Load..")) - self.saveBtn.setText(_translate("Form", "Save")) - self.saveAsBtn.setText(_translate("Form", "As..")) - self.reloadBtn.setText(_translate("Form", "Reload Libs")) - self.showChartBtn.setText(_translate("Form", "Flowchart")) - -from ..widgets.FeedbackButton import FeedbackButton -from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py deleted file mode 100644 index 201571f2ac..0000000000 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py +++ /dev/null @@ -1,65 +0,0 @@ - -# Form implementation generated from reading ui file 'FlowchartCtrlTemplate.ui' -# -# Created: Sun Sep 18 19:16:46 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(217, 499) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setVerticalSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.loadBtn = QtWidgets.QPushButton(Form) - self.loadBtn.setObjectName("loadBtn") - self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) - self.saveBtn = FeedbackButton(Form) - self.saveBtn.setObjectName("saveBtn") - self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) - self.saveAsBtn = FeedbackButton(Form) - self.saveAsBtn.setObjectName("saveAsBtn") - self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) - self.reloadBtn = FeedbackButton(Form) - self.reloadBtn.setCheckable(False) - self.reloadBtn.setFlat(False) - self.reloadBtn.setObjectName("reloadBtn") - self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) - self.showChartBtn = QtWidgets.QPushButton(Form) - self.showChartBtn.setCheckable(True) - self.showChartBtn.setObjectName("showChartBtn") - self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) - self.ctrlList = TreeWidget(Form) - self.ctrlList.setObjectName("ctrlList") - self.ctrlList.headerItem().setText(0, "1") - self.ctrlList.header().setVisible(False) - self.ctrlList.header().setStretchLastSection(False) - self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) - self.fileNameLabel = QtWidgets.QLabel(Form) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.fileNameLabel.setFont(font) - self.fileNameLabel.setText("") - self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) - self.fileNameLabel.setObjectName("fileNameLabel") - self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.loadBtn.setText(QtWidgets.QApplication.translate("Form", "Load..", None, -1)) - self.saveBtn.setText(QtWidgets.QApplication.translate("Form", "Save", None, -1)) - self.saveAsBtn.setText(QtWidgets.QApplication.translate("Form", "As..", None, -1)) - self.reloadBtn.setText(QtWidgets.QApplication.translate("Form", "Reload Libs", None, -1)) - self.showChartBtn.setText(QtWidgets.QApplication.translate("Form", "Flowchart", None, -1)) - -from ..widgets.FeedbackButton import FeedbackButton -from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside6.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside6.py deleted file mode 100644 index 030f354f70..0000000000 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside6.py +++ /dev/null @@ -1,88 +0,0 @@ - -################################################################################ -## Form generated from reading UI file 'FlowchartCtrlTemplate.ui' -## -## Created by: Qt User Interface Compiler version 6.1.0 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import * -from PySide6.QtGui import * -from PySide6.QtWidgets import * - -from ..widgets.TreeWidget import TreeWidget -from ..widgets.FeedbackButton import FeedbackButton - - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(217, 499) - self.gridLayout = QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName(u"gridLayout") - self.gridLayout.setVerticalSpacing(0) - self.loadBtn = QPushButton(Form) - self.loadBtn.setObjectName(u"loadBtn") - - self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) - - self.saveBtn = FeedbackButton(Form) - self.saveBtn.setObjectName(u"saveBtn") - - self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) - - self.saveAsBtn = FeedbackButton(Form) - self.saveAsBtn.setObjectName(u"saveAsBtn") - - self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) - - self.reloadBtn = FeedbackButton(Form) - self.reloadBtn.setObjectName(u"reloadBtn") - self.reloadBtn.setCheckable(False) - self.reloadBtn.setFlat(False) - - self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) - - self.showChartBtn = QPushButton(Form) - self.showChartBtn.setObjectName(u"showChartBtn") - self.showChartBtn.setCheckable(True) - - self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) - - self.ctrlList = TreeWidget(Form) - __qtreewidgetitem = QTreeWidgetItem() - __qtreewidgetitem.setText(0, u"1"); - self.ctrlList.setHeaderItem(__qtreewidgetitem) - self.ctrlList.setObjectName(u"ctrlList") - self.ctrlList.header().setVisible(False) - self.ctrlList.header().setStretchLastSection(False) - - self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) - - self.fileNameLabel = QLabel(Form) - self.fileNameLabel.setObjectName(u"fileNameLabel") - font = QFont() - font.setBold(True) - self.fileNameLabel.setFont(font) - self.fileNameLabel.setAlignment(Qt.AlignCenter) - - self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) - - - self.retranslateUi(Form) - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"PyQtGraph", None)) - self.loadBtn.setText(QCoreApplication.translate("Form", u"Load..", None)) - self.saveBtn.setText(QCoreApplication.translate("Form", u"Save", None)) - self.saveAsBtn.setText(QCoreApplication.translate("Form", u"As..", None)) - self.reloadBtn.setText(QCoreApplication.translate("Form", u"Reload Libs", None)) - self.showChartBtn.setText(QCoreApplication.translate("Form", u"Flowchart", None)) - self.fileNameLabel.setText("") - # retranslateUi diff --git a/pyqtgraph/flowchart/FlowchartTemplate.ui b/pyqtgraph/flowchart/FlowchartTemplate.ui deleted file mode 100644 index 8b4ef814d5..0000000000 --- a/pyqtgraph/flowchart/FlowchartTemplate.ui +++ /dev/null @@ -1,97 +0,0 @@ - - - Form - - - - 0 - 0 - 529 - 329 - - - - PyQtGraph - - - - - 260 - 10 - 264 - 222 - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - - true - - - - - - - - - - - - 1 - - - - - - - - - - 0 - 240 - 521 - 81 - - - - - - - 0 - 0 - 256 - 192 - - - - - - - DataTreeWidget - QTreeWidget -
..widgets.DataTreeWidget
-
- - FlowchartGraphicsView - QGraphicsView -
..flowchart.FlowchartGraphicsView
-
-
- - -
diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py deleted file mode 100644 index f3c3ccf03e..0000000000 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py +++ /dev/null @@ -1,54 +0,0 @@ - -# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui' -# -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(529, 329) - self.selInfoWidget = QtWidgets.QWidget(Form) - self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) - self.selInfoWidget.setObjectName("selInfoWidget") - self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) - self.selDescLabel.setText("") - self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.selDescLabel.setWordWrap(True) - self.selDescLabel.setObjectName("selDescLabel") - self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) - self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.selNameLabel.setFont(font) - self.selNameLabel.setText("") - self.selNameLabel.setObjectName("selNameLabel") - self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) - self.selectedTree = DataTreeWidget(self.selInfoWidget) - self.selectedTree.setObjectName("selectedTree") - self.selectedTree.headerItem().setText(0, "1") - self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) - self.hoverText = QtWidgets.QTextEdit(Form) - self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) - self.hoverText.setObjectName("hoverText") - self.view = FlowchartGraphicsView(Form) - self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) - self.view.setObjectName("view") - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "PyQtGraph")) - -from ..widgets.DataTreeWidget import DataTreeWidget -from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt6.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt6.py deleted file mode 100644 index d7f077ae25..0000000000 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt6.py +++ /dev/null @@ -1,53 +0,0 @@ -# Form implementation generated from reading ui file '../pyqtgraph/flowchart/FlowchartTemplate.ui' -# -# Created by: PyQt6 UI code generator 6.1.0 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt6 import QtCore, QtGui, QtWidgets - - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(529, 329) - self.selInfoWidget = QtWidgets.QWidget(Form) - self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) - self.selInfoWidget.setObjectName("selInfoWidget") - self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) - self.selDescLabel.setText("") - self.selDescLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) - self.selDescLabel.setWordWrap(True) - self.selDescLabel.setObjectName("selDescLabel") - self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) - self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) - font = QtGui.QFont() - font.setBold(True) - self.selNameLabel.setFont(font) - self.selNameLabel.setText("") - self.selNameLabel.setObjectName("selNameLabel") - self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) - self.selectedTree = DataTreeWidget(self.selInfoWidget) - self.selectedTree.setObjectName("selectedTree") - self.selectedTree.headerItem().setText(0, "1") - self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) - self.hoverText = QtWidgets.QTextEdit(Form) - self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) - self.hoverText.setObjectName("hoverText") - self.view = FlowchartGraphicsView(Form) - self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) - self.view.setObjectName("view") - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "PyQtGraph")) -from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView -from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py deleted file mode 100644 index ce2d23fe65..0000000000 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py +++ /dev/null @@ -1,53 +0,0 @@ - -# Form implementation generated from reading ui file 'FlowchartTemplate.ui' -# -# Created: Sun Sep 18 19:16:03 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(529, 329) - self.selInfoWidget = QtWidgets.QWidget(Form) - self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) - self.selInfoWidget.setObjectName("selInfoWidget") - self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) - self.selDescLabel.setText("") - self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.selDescLabel.setWordWrap(True) - self.selDescLabel.setObjectName("selDescLabel") - self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) - self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.selNameLabel.setFont(font) - self.selNameLabel.setText("") - self.selNameLabel.setObjectName("selNameLabel") - self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) - self.selectedTree = DataTreeWidget(self.selInfoWidget) - self.selectedTree.setObjectName("selectedTree") - self.selectedTree.headerItem().setText(0, "1") - self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) - self.hoverText = QtWidgets.QTextEdit(Form) - self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) - self.hoverText.setObjectName("hoverText") - self.view = FlowchartGraphicsView(Form) - self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) - self.view.setObjectName("view") - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - -from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView -from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside6.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside6.py deleted file mode 100644 index c117c500c8..0000000000 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside6.py +++ /dev/null @@ -1,67 +0,0 @@ - -################################################################################ -## Form generated from reading UI file 'FlowchartTemplate.ui' -## -## Created by: Qt User Interface Compiler version 6.1.0 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import * -from PySide6.QtGui import * -from PySide6.QtWidgets import * - -from ..widgets.DataTreeWidget import DataTreeWidget -from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView - - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(529, 329) - self.selInfoWidget = QWidget(Form) - self.selInfoWidget.setObjectName(u"selInfoWidget") - self.selInfoWidget.setGeometry(QRect(260, 10, 264, 222)) - self.gridLayout = QGridLayout(self.selInfoWidget) - self.gridLayout.setObjectName(u"gridLayout") - self.selDescLabel = QLabel(self.selInfoWidget) - self.selDescLabel.setObjectName(u"selDescLabel") - self.selDescLabel.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop) - self.selDescLabel.setWordWrap(True) - - self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) - - self.selNameLabel = QLabel(self.selInfoWidget) - self.selNameLabel.setObjectName(u"selNameLabel") - font = QFont() - font.setBold(True) - self.selNameLabel.setFont(font) - - self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) - - self.selectedTree = DataTreeWidget(self.selInfoWidget) - __qtreewidgetitem = QTreeWidgetItem() - __qtreewidgetitem.setText(0, u"1"); - self.selectedTree.setHeaderItem(__qtreewidgetitem) - self.selectedTree.setObjectName(u"selectedTree") - - self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) - - self.hoverText = QTextEdit(Form) - self.hoverText.setObjectName(u"hoverText") - self.hoverText.setGeometry(QRect(0, 240, 521, 81)) - self.view = FlowchartGraphicsView(Form) - self.view.setObjectName(u"view") - self.view.setGeometry(QRect(0, 0, 256, 192)) - - self.retranslateUi(Form) - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"PyQtGraph", None)) - self.selDescLabel.setText("") - self.selNameLabel.setText("") - # retranslateUi From d685460e58bec65724e889cceaaac03d89fcda29 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 1 Oct 2022 11:42:01 -0700 Subject: [PATCH 068/306] Remove canvas templates --- pyqtgraph/canvas/Canvas.py | 10 +- pyqtgraph/canvas/CanvasItem.py | 8 +- ...ate_pyqt6.py => CanvasTemplate_generic.py} | 2 +- pyqtgraph/canvas/CanvasTemplate_pyqt5.py | 91 ------------- pyqtgraph/canvas/CanvasTemplate_pyside2.py | 86 ------------ pyqtgraph/canvas/CanvasTemplate_pyside6.py | 124 ------------------ ...qt6.py => TransformGuiTemplate_generic.py} | 2 +- .../canvas/TransformGuiTemplate_pyqt5.py | 53 -------- .../canvas/TransformGuiTemplate_pyside2.py | 53 -------- .../canvas/TransformGuiTemplate_pyside6.py | 75 ----------- 10 files changed, 7 insertions(+), 497 deletions(-) rename pyqtgraph/canvas/{CanvasTemplate_pyqt6.py => CanvasTemplate_generic.py} (99%) delete mode 100644 pyqtgraph/canvas/CanvasTemplate_pyqt5.py delete mode 100644 pyqtgraph/canvas/CanvasTemplate_pyside2.py delete mode 100644 pyqtgraph/canvas/CanvasTemplate_pyside6.py rename pyqtgraph/canvas/{TransformGuiTemplate_pyqt6.py => TransformGuiTemplate_generic.py} (98%) delete mode 100644 pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py delete mode 100644 pyqtgraph/canvas/TransformGuiTemplate_pyside2.py delete mode 100644 pyqtgraph/canvas/TransformGuiTemplate_pyside6.py diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index a6f1632bde..fef1ddb5dd 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -1,18 +1,14 @@ __all__ = ["Canvas"] +import gc import importlib +import weakref from ..graphicsItems.GridItem import GridItem from ..graphicsItems.ROI import ROI from ..graphicsItems.ViewBox import ViewBox from ..Qt import QT_LIB, QtCore, QtGui, QtWidgets - -ui_template = importlib.import_module( - f'.CanvasTemplate_{QT_LIB.lower()}', package=__package__) - -import gc -import weakref - +from . import CanvasTemplate_generic as ui_template from .CanvasItem import CanvasItem, GroupCanvasItem from .CanvasManager import CanvasManager diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index f67e6d3e01..4601d9e0ff 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -2,15 +2,11 @@ import importlib -from .. import ItemGroup, SRTTransform +from .. import ItemGroup, SRTTransform, debug from .. import functions as fn from ..graphicsItems.ROI import ROI from ..Qt import QT_LIB, QtCore, QtWidgets - -ui_template = importlib.import_module( - f'.TransformGuiTemplate_{QT_LIB.lower()}', package=__package__) - -from .. import debug +from . import TransformGuiTemplate_generic as ui_template translate = QtCore.QCoreApplication.translate diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt6.py b/pyqtgraph/canvas/CanvasTemplate_generic.py similarity index 99% rename from pyqtgraph/canvas/CanvasTemplate_pyqt6.py rename to pyqtgraph/canvas/CanvasTemplate_generic.py index 5603e54fa1..add2b5852f 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt6.py +++ b/pyqtgraph/canvas/CanvasTemplate_generic.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt6 import QtCore, QtGui, QtWidgets +from ..Qt import QtCore, QtGui, QtWidgets class Ui_Form(object): diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py deleted file mode 100644 index 0a341c6f38..0000000000 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py +++ /dev/null @@ -1,91 +0,0 @@ - -# Form implementation generated from reading ui file 'CanvasTemplate.ui' -# -# Created by: PyQt5 UI code generator 5.7.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(821, 578) - self.gridLayout_2 = QtWidgets.QGridLayout(Form) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.view = GraphicsView(self.splitter) - self.view.setObjectName("view") - self.vsplitter = QtWidgets.QSplitter(self.splitter) - self.vsplitter.setOrientation(QtCore.Qt.Vertical) - self.vsplitter.setObjectName("vsplitter") - self.canvasCtrlWidget = QtWidgets.QWidget(self.vsplitter) - self.canvasCtrlWidget.setObjectName("canvasCtrlWidget") - self.gridLayout = QtWidgets.QGridLayout(self.canvasCtrlWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.autoRangeBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) - self.autoRangeBtn.setSizePolicy(sizePolicy) - self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtWidgets.QCheckBox(self.canvasCtrlWidget) - self.redirectCheck.setObjectName("redirectCheck") - self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) - self.redirectCombo.setObjectName("redirectCombo") - self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) - self.itemList = TreeWidget(self.canvasCtrlWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(100) - sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) - self.itemList.setSizePolicy(sizePolicy) - self.itemList.setHeaderHidden(True) - self.itemList.setObjectName("itemList") - self.itemList.headerItem().setText(0, "1") - self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) - self.resetTransformsBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) - self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) - self.mirrorSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) - self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) - self.reflectSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) - self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) - self.canvasItemCtrl = QtWidgets.QWidget(self.vsplitter) - self.canvasItemCtrl.setObjectName("canvasItemCtrl") - self.ctrlLayout = QtWidgets.QGridLayout(self.canvasItemCtrl) - self.ctrlLayout.setContentsMargins(0, 0, 0, 0) - self.ctrlLayout.setSpacing(0) - self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "PyQtGraph")) - self.autoRangeBtn.setText(_translate("Form", "Auto Range")) - self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.")) - self.redirectCheck.setText(_translate("Form", "Redirect")) - self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms")) - self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection")) - self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY")) - -from ..widgets.GraphicsView import GraphicsView -from ..widgets.TreeWidget import TreeWidget -from .CanvasManager import CanvasCombo diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside2.py b/pyqtgraph/canvas/CanvasTemplate_pyside2.py deleted file mode 100644 index 42891bed90..0000000000 --- a/pyqtgraph/canvas/CanvasTemplate_pyside2.py +++ /dev/null @@ -1,86 +0,0 @@ - -# Form implementation generated from reading ui file 'CanvasTemplate.ui' -# -# Created: Sun Sep 18 19:18:22 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(490, 414) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.view = GraphicsView(self.splitter) - self.view.setObjectName("view") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) - self.autoRangeBtn.setSizePolicy(sizePolicy) - self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) - self.redirectCheck.setObjectName("redirectCheck") - self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) - self.redirectCombo.setObjectName("redirectCombo") - self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(100) - sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) - self.itemList.setSizePolicy(sizePolicy) - self.itemList.setHeaderHidden(True) - self.itemList.setObjectName("itemList") - self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtWidgets.QGridLayout() - self.ctrlLayout.setSpacing(0) - self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) - self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.autoRangeBtn.setText(QtWidgets.QApplication.translate("Form", "Auto Range", None, -1)) - self.redirectCheck.setToolTip(QtWidgets.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, -1)) - self.redirectCheck.setText(QtWidgets.QApplication.translate("Form", "Redirect", None, -1)) - self.resetTransformsBtn.setText(QtWidgets.QApplication.translate("Form", "Reset Transforms", None, -1)) - self.mirrorSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror Selection", None, -1)) - self.reflectSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "MirrorXY", None, -1)) - -from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo -from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside6.py b/pyqtgraph/canvas/CanvasTemplate_pyside6.py deleted file mode 100644 index 540692de28..0000000000 --- a/pyqtgraph/canvas/CanvasTemplate_pyside6.py +++ /dev/null @@ -1,124 +0,0 @@ - -################################################################################ -## Form generated from reading UI file 'CanvasTemplate.ui' -## -## Created by: Qt User Interface Compiler version 6.1.0 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import * -from PySide6.QtGui import * -from PySide6.QtWidgets import * - -from ..widgets.TreeWidget import TreeWidget -from ..widgets.GraphicsView import GraphicsView -from .CanvasManager import CanvasCombo - - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(821, 578) - self.gridLayout_2 = QGridLayout(Form) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName(u"gridLayout_2") - self.splitter = QSplitter(Form) - self.splitter.setObjectName(u"splitter") - self.splitter.setOrientation(Qt.Horizontal) - self.view = GraphicsView(self.splitter) - self.view.setObjectName(u"view") - self.splitter.addWidget(self.view) - self.vsplitter = QSplitter(self.splitter) - self.vsplitter.setObjectName(u"vsplitter") - self.vsplitter.setOrientation(Qt.Vertical) - self.canvasCtrlWidget = QWidget(self.vsplitter) - self.canvasCtrlWidget.setObjectName(u"canvasCtrlWidget") - self.gridLayout = QGridLayout(self.canvasCtrlWidget) - self.gridLayout.setObjectName(u"gridLayout") - self.autoRangeBtn = QPushButton(self.canvasCtrlWidget) - self.autoRangeBtn.setObjectName(u"autoRangeBtn") - sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) - self.autoRangeBtn.setSizePolicy(sizePolicy) - - self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) - - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.redirectCheck = QCheckBox(self.canvasCtrlWidget) - self.redirectCheck.setObjectName(u"redirectCheck") - - self.horizontalLayout.addWidget(self.redirectCheck) - - self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) - self.redirectCombo.setObjectName(u"redirectCombo") - - self.horizontalLayout.addWidget(self.redirectCombo) - - - self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) - - self.itemList = TreeWidget(self.canvasCtrlWidget) - __qtreewidgetitem = QTreeWidgetItem() - __qtreewidgetitem.setText(0, u"1"); - self.itemList.setHeaderItem(__qtreewidgetitem) - self.itemList.setObjectName(u"itemList") - sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - sizePolicy1.setHorizontalStretch(0) - sizePolicy1.setVerticalStretch(100) - sizePolicy1.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) - self.itemList.setSizePolicy(sizePolicy1) - self.itemList.setHeaderHidden(True) - - self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) - - self.resetTransformsBtn = QPushButton(self.canvasCtrlWidget) - self.resetTransformsBtn.setObjectName(u"resetTransformsBtn") - - self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) - - self.mirrorSelectionBtn = QPushButton(self.canvasCtrlWidget) - self.mirrorSelectionBtn.setObjectName(u"mirrorSelectionBtn") - - self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) - - self.reflectSelectionBtn = QPushButton(self.canvasCtrlWidget) - self.reflectSelectionBtn.setObjectName(u"reflectSelectionBtn") - - self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) - - self.vsplitter.addWidget(self.canvasCtrlWidget) - self.canvasItemCtrl = QWidget(self.vsplitter) - self.canvasItemCtrl.setObjectName(u"canvasItemCtrl") - self.ctrlLayout = QGridLayout(self.canvasItemCtrl) - self.ctrlLayout.setSpacing(0) - self.ctrlLayout.setContentsMargins(0, 0, 0, 0) - self.ctrlLayout.setObjectName(u"ctrlLayout") - self.vsplitter.addWidget(self.canvasItemCtrl) - self.splitter.addWidget(self.vsplitter) - - self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) - - - self.retranslateUi(Form) - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"PyQtGraph", None)) - self.autoRangeBtn.setText(QCoreApplication.translate("Form", u"Auto Range", None)) -#if QT_CONFIG(tooltip) - self.redirectCheck.setToolTip(QCoreApplication.translate("Form", u"Check to display all local items in a remote canvas.", None)) -#endif // QT_CONFIG(tooltip) - self.redirectCheck.setText(QCoreApplication.translate("Form", u"Redirect", None)) - self.resetTransformsBtn.setText(QCoreApplication.translate("Form", u"Reset Transforms", None)) - self.mirrorSelectionBtn.setText(QCoreApplication.translate("Form", u"Mirror Selection", None)) - self.reflectSelectionBtn.setText(QCoreApplication.translate("Form", u"MirrorXY", None)) - # retranslateUi diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt6.py b/pyqtgraph/canvas/TransformGuiTemplate_generic.py similarity index 98% rename from pyqtgraph/canvas/TransformGuiTemplate_pyqt6.py rename to pyqtgraph/canvas/TransformGuiTemplate_generic.py index 71d72ac879..f167efa7e7 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt6.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_generic.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt6 import QtCore, QtGui, QtWidgets +from ..Qt import QtCore, QtGui, QtWidgets class Ui_Form(object): diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py deleted file mode 100644 index be09f66dc9..0000000000 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py +++ /dev/null @@ -1,53 +0,0 @@ - -# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' -# -# Created by: PyQt5 UI code generator 5.5.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(224, 117) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) - Form.setSizePolicy(sizePolicy) - self.verticalLayout = QtWidgets.QVBoxLayout(Form) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setSpacing(1) - self.verticalLayout.setObjectName("verticalLayout") - self.translateLabel = QtWidgets.QLabel(Form) - self.translateLabel.setObjectName("translateLabel") - self.verticalLayout.addWidget(self.translateLabel) - self.rotateLabel = QtWidgets.QLabel(Form) - self.rotateLabel.setObjectName("rotateLabel") - self.verticalLayout.addWidget(self.rotateLabel) - self.scaleLabel = QtWidgets.QLabel(Form) - self.scaleLabel.setObjectName("scaleLabel") - self.verticalLayout.addWidget(self.scaleLabel) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.mirrorImageBtn = QtWidgets.QPushButton(Form) - self.mirrorImageBtn.setToolTip("") - self.mirrorImageBtn.setObjectName("mirrorImageBtn") - self.horizontalLayout.addWidget(self.mirrorImageBtn) - self.reflectImageBtn = QtWidgets.QPushButton(Form) - self.reflectImageBtn.setObjectName("reflectImageBtn") - self.horizontalLayout.addWidget(self.reflectImageBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "PyQtGraph")) - self.translateLabel.setText(_translate("Form", "Translate:")) - self.rotateLabel.setText(_translate("Form", "Rotate:")) - self.scaleLabel.setText(_translate("Form", "Scale:")) - self.mirrorImageBtn.setText(_translate("Form", "Mirror")) - self.reflectImageBtn.setText(_translate("Form", "Reflect")) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py deleted file mode 100644 index b85e519c55..0000000000 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py +++ /dev/null @@ -1,53 +0,0 @@ - -# Form implementation generated from reading ui file 'TransformGuiTemplate.ui' -# -# Created: Sun Sep 18 19:18:41 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(224, 117) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) - Form.setSizePolicy(sizePolicy) - self.verticalLayout = QtWidgets.QVBoxLayout(Form) - self.verticalLayout.setSpacing(1) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.translateLabel = QtWidgets.QLabel(Form) - self.translateLabel.setObjectName("translateLabel") - self.verticalLayout.addWidget(self.translateLabel) - self.rotateLabel = QtWidgets.QLabel(Form) - self.rotateLabel.setObjectName("rotateLabel") - self.verticalLayout.addWidget(self.rotateLabel) - self.scaleLabel = QtWidgets.QLabel(Form) - self.scaleLabel.setObjectName("scaleLabel") - self.verticalLayout.addWidget(self.scaleLabel) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.mirrorImageBtn = QtWidgets.QPushButton(Form) - self.mirrorImageBtn.setToolTip("") - self.mirrorImageBtn.setObjectName("mirrorImageBtn") - self.horizontalLayout.addWidget(self.mirrorImageBtn) - self.reflectImageBtn = QtWidgets.QPushButton(Form) - self.reflectImageBtn.setObjectName("reflectImageBtn") - self.horizontalLayout.addWidget(self.reflectImageBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.translateLabel.setText(QtWidgets.QApplication.translate("Form", "Translate:", None, -1)) - self.rotateLabel.setText(QtWidgets.QApplication.translate("Form", "Rotate:", None, -1)) - self.scaleLabel.setText(QtWidgets.QApplication.translate("Form", "Scale:", None, -1)) - self.mirrorImageBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror", None, -1)) - self.reflectImageBtn.setText(QtWidgets.QApplication.translate("Form", "Reflect", None, -1)) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside6.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside6.py deleted file mode 100644 index 4d085f6ebd..0000000000 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside6.py +++ /dev/null @@ -1,75 +0,0 @@ - -################################################################################ -## Form generated from reading UI file 'TransformGuiTemplate.ui' -## -## Created by: Qt User Interface Compiler version 6.1.0 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import * -from PySide6.QtGui import * -from PySide6.QtWidgets import * - - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(224, 117) - sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) - Form.setSizePolicy(sizePolicy) - self.verticalLayout = QVBoxLayout(Form) - self.verticalLayout.setSpacing(1) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName(u"verticalLayout") - self.translateLabel = QLabel(Form) - self.translateLabel.setObjectName(u"translateLabel") - - self.verticalLayout.addWidget(self.translateLabel) - - self.rotateLabel = QLabel(Form) - self.rotateLabel.setObjectName(u"rotateLabel") - - self.verticalLayout.addWidget(self.rotateLabel) - - self.scaleLabel = QLabel(Form) - self.scaleLabel.setObjectName(u"scaleLabel") - - self.verticalLayout.addWidget(self.scaleLabel) - - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.mirrorImageBtn = QPushButton(Form) - self.mirrorImageBtn.setObjectName(u"mirrorImageBtn") - - self.horizontalLayout.addWidget(self.mirrorImageBtn) - - self.reflectImageBtn = QPushButton(Form) - self.reflectImageBtn.setObjectName(u"reflectImageBtn") - - self.horizontalLayout.addWidget(self.reflectImageBtn) - - - self.verticalLayout.addLayout(self.horizontalLayout) - - - self.retranslateUi(Form) - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"PyQtGraph", None)) - self.translateLabel.setText(QCoreApplication.translate("Form", u"Translate:", None)) - self.rotateLabel.setText(QCoreApplication.translate("Form", u"Rotate:", None)) - self.scaleLabel.setText(QCoreApplication.translate("Form", u"Scale:", None)) -#if QT_CONFIG(tooltip) - self.mirrorImageBtn.setToolTip("") -#endif // QT_CONFIG(tooltip) - self.mirrorImageBtn.setText(QCoreApplication.translate("Form", u"Mirror", None)) - self.reflectImageBtn.setText(QCoreApplication.translate("Form", u"Reflect", None)) - # retranslateUi From 40d9c8dce0c18d37a4a9b8ba827e7e35a2e90ca2 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 1 Oct 2022 12:43:22 -0700 Subject: [PATCH 069/306] Update instructions and script to generate template files --- CONTRIBUTING.md | 5 +++++ tools/rebuildUi.py | 42 +++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a117f9d9d..9595808c70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,10 @@ PyQtGraph has adopted [NEP-29](https://numpy.org/neps/nep-0029-deprecation_polic * Documentation is generated with sphinx, and usage of [numpy-docstyle](https://numpydoc.readthedocs.io/en/latest/format.html) is encouraged (many places in the library do not use this docstring style at present, it's a gradual process to migrate). * The docs built for this PR can be previewed by clicking on the "Details" link for the read-the-docs entry in the checks section of the PR conversation page. +## Templates + +PyQtGraph makes use of `.ui` files where are compiled using `uic`. These files are identified by ending with `_generic.py` and have a `.ui` file next to them in the same directory. In past versions of PyQtGraph, there was a file for each binding. These are generated using tools/rebuildUi.py. Upon completion, the compiled files need to be modified such that they do not inherit from PyQt6 but from pyqtgraph's Qt abstraction layer. + ## Style guidelines ### Formatting ~~Rules~~ Suggestions @@ -55,6 +59,7 @@ PyQtGraph has adopted [NEP-29](https://numpy.org/neps/nep-0029-deprecation_polic PyQtGraph developers are highly encouraged to (but not required) to use [`pre-commit`](https://pre-commit.com/). `pre-commit` does a number of checks when attempting to commit the code to being committed, such as ensuring no large files are accidentally added, address mixed-line-endings types and so on. Check the [pre-commit documentation](https://pre-commit.com) on how to setup. + ## Testing ### Basic Setup diff --git a/tools/rebuildUi.py b/tools/rebuildUi.py index 8c64c07040..07f8897141 100644 --- a/tools/rebuildUi.py +++ b/tools/rebuildUi.py @@ -1,18 +1,16 @@ #!/usr/bin/python """ Script for compiling Qt Designer .ui files to .py +""" +import os +import subprocess +import sys -""" -import os, sys, subprocess, tempfile - -pyqt5uic = 'pyuic5' pyqt6uic = 'pyuic6' -pyside2uic = 'pyside2-uic' -pyside6uic = 'pyside6-uic' -usage = """Compile .ui files to .py for all supported pyqt/pyside versions. +usage = """Compile .ui files to .py for PyQt6 Usage: python rebuildUi.py [--force] [.ui files|search paths] @@ -31,7 +29,6 @@ print(usage) sys.exit(-1) - uifiles = [] for arg in args: if os.path.isfile(arg) and arg.endswith('.ui'): @@ -39,26 +36,25 @@ elif os.path.isdir(arg): # recursively search for ui files in this directory for path, sd, files in os.walk(arg): - for f in files: - if not f.endswith('.ui'): - continue - uifiles.append(os.path.join(path, f)) + uifiles.extend(os.path.join(path, f) for f in files if f.endswith('.ui')) else: print('Argument "%s" is not a directory or .ui file.' % arg) sys.exit(-1) +compiler = pyqt6uic +extension = '_generic.py' # rebuild all requested ui files for ui in uifiles: base, _ = os.path.splitext(ui) - for compiler, ext in [(pyqt5uic, '_pyqt5.py'), (pyside2uic, '_pyside2.py'), - (pyqt6uic, '_pyqt6.py'), (pyside6uic, '_pyside6.py')]: - py = base + ext - if not force and os.path.exists(py) and os.stat(ui).st_mtime <= os.stat(py).st_mtime: - print("Skipping %s; already compiled." % py) + py = base + ext + if not force and os.path.exists(py) and os.stat(ui).st_mtime <= os.stat(py).st_mtime: + print(f"Skipping {py}; already compiled.") + else: + cmd = f'{compiler} {ui} > {py}' + print(cmd) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError: + os.remove(py) else: - cmd = '%s %s > %s' % (compiler, ui, py) - print(cmd) - try: - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError: - os.remove(py) + print(f"{py} created, modify import to import from pyqtgraph.Qt not PyQt6") From 2320c7d7e9a3775f600a04ae6601f00cedb756ee Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 1 Oct 2022 09:44:24 -0700 Subject: [PATCH 070/306] Have canvas deprecation warning --- pyqtgraph/canvas/Canvas.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index fef1ddb5dd..8b7a4fcc1c 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -3,6 +3,7 @@ import gc import importlib import weakref +import warnings from ..graphicsItems.GridItem import GridItem from ..graphicsItems.ROI import ROI @@ -22,6 +23,12 @@ class Canvas(QtWidgets.QWidget): def __init__(self, parent=None, allowTransforms=True, hideCtrl=False, name=None): QtWidgets.QWidget.__init__(self, parent) + warnings.warn( + 'pyqtgrapoh.cavas will be deprecated in pyqtgraph and migrate to ' + 'acq4. Removal will occur after September, 2023.', + DeprecationWarning, stacklevel=2 + ) + self.ui = ui_template.Ui_Form() self.ui.setupUi(self) self.view = ViewBox() From 9b59e3e4d520573a6373576535e883c7e6ea00e8 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Sun, 2 Oct 2022 00:19:51 -0400 Subject: [PATCH 071/306] Fix action parameter button that is briefly made visible before getting a parent --- pyqtgraph/parametertree/parameterTypes/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes/action.py b/pyqtgraph/parametertree/parameterTypes/action.py index 04f956969b..ff69d09422 100644 --- a/pyqtgraph/parametertree/parameterTypes/action.py +++ b/pyqtgraph/parametertree/parameterTypes/action.py @@ -52,7 +52,7 @@ def __init__(self, param, depth): self.layout = QtWidgets.QHBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layoutWidget.setLayout(self.layout) - self.button = ParameterControlledButton(param) + self.button = ParameterControlledButton(param, self.layoutWidget) #self.layout.addSpacing(100) self.layout.addWidget(self.button) self.layout.addStretch() From 2e8f61c13d767a8cc35807ad54536cdd0b28bb8a Mon Sep 17 00:00:00 2001 From: Dennis Goeries Date: Mon, 3 Oct 2022 00:47:27 +0200 Subject: [PATCH 072/306] Fix disconnect of signal proxy --- pyqtgraph/SignalProxy.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 1c6012f3dd..ae0c8cb48e 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -10,10 +10,10 @@ class SignalProxy(QtCore.QObject): """Object which collects rapid-fire signals and condenses them - into a single signal or a rate-limited stream of signals. - Used, for example, to prevent a SpinBox from generating multiple + into a single signal or a rate-limited stream of signals. + Used, for example, to prevent a SpinBox from generating multiple signals when the mouse wheel is rolled over it. - + Emits sigDelayed after input signals have stopped for a certain period of time. """ @@ -25,7 +25,7 @@ def __init__(self, signal, delay=0.3, rateLimit=0, slot=None): signal - a bound Signal or pyqtSignal instance delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s) slot - Optional function to connect sigDelayed to. - rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a + rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a steady rate while they are being received. """ @@ -86,9 +86,9 @@ def disconnect(self): except: pass try: - # XXX: This is a weakref, however segfaulting on PySide and - # Python 2. We come back later - self.sigDelayed.disconnect(self.slot) + slot = self.slot() + if slot is not None: + self.sigDelayed.disconnect(slot) except: pass finally: From 3f3639cc07f9d0381a3b2915aa6635e6ccde5c13 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 2 Oct 2022 16:15:22 -0700 Subject: [PATCH 073/306] Fix InfiniteLine bounding rect calculation (#2407) * Fix InfiniteLine bounding rect calculation * Another approach, seems to work completely correctly now --- pyqtgraph/graphicsItems/InfiniteLine.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 04cacbbb84..e3697eafdd 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -300,10 +300,12 @@ def _computeBoundingRect(self): vr = self.viewRect() # bounds of containing ViewBox mapped to local coords. if vr is None: return QtCore.QRectF() - - px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line - if px is None: - px = 0 + + # compute the pixel size orthogonal to the line + # this is more complicated than it seems, maybe it can be simplified + _, ortho = self.pixelVectors(direction=Point(1, 0)) + px = 0 if ortho is None else ortho.y() + pw = max(self.pen.width() / 2, self.hoverPen.width() / 2) w = (self._maxMarkerSize + pw + 1) * px br = QtCore.QRectF(vr) From 7117f3754bb3cb88a92c76bd98ebca03f61bd31f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 1 Oct 2022 15:59:29 -0700 Subject: [PATCH 074/306] Add sphinx-design to requirements --- doc/requirements.txt | 1 + doc/source/conf.py | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index 454248e869..2352cbb393 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,5 +2,6 @@ sphinx==5.2.2 PyQt6==6.3.1 sphinx-rtd-theme sphinx-qt-documentation +sphinx-design numpy pyopengl diff --git a/doc/source/conf.py b/doc/source/conf.py index 0bcd95fe91..ebec67d9ce 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -36,6 +36,7 @@ "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx_qt_documentation", + "sphinx_design" ] # Add any paths that contain templates here, relative to this directory. From 0ab300364e16a822bec1e572f196a2fb377f5f8f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 1 Oct 2022 15:21:08 -0700 Subject: [PATCH 075/306] Moves doc files to new homes --- doc/requirements.txt | 2 +- doc/source/{ => api_reference}/colormap.rst | 8 ++++---- doc/source/{ => api_reference}/dockarea.rst | 0 doc/source/{ => api_reference}/flowchart/flowchart.rst | 0 doc/source/{ => api_reference}/flowchart/index.rst | 0 doc/source/{ => api_reference}/flowchart/node.rst | 0 doc/source/{ => api_reference}/flowchart/terminal.rst | 0 doc/source/{ => api_reference}/functions.rst | 0 .../{ => api_reference}/graphicsItems/arrowitem.rst | 0 doc/source/{ => api_reference}/graphicsItems/axisitem.rst | 0 .../{ => api_reference}/graphicsItems/bargraphitem.rst | 0 .../{ => api_reference}/graphicsItems/buttonitem.rst | 0 .../{ => api_reference}/graphicsItems/colorbaritem.rst | 0 .../{ => api_reference}/graphicsItems/curvearrow.rst | 0 .../{ => api_reference}/graphicsItems/curvepoint.rst | 0 .../{ => api_reference}/graphicsItems/dateaxisitem.rst | 0 .../{ => api_reference}/graphicsItems/errorbaritem.rst | 0 .../{ => api_reference}/graphicsItems/fillbetweenitem.rst | 0 .../graphicsItems/gradienteditoritem.rst | 0 .../{ => api_reference}/graphicsItems/gradientlegend.rst | 0 .../{ => api_reference}/graphicsItems/graphicsitem.rst | 0 .../{ => api_reference}/graphicsItems/graphicslayout.rst | 0 .../{ => api_reference}/graphicsItems/graphicsobject.rst | 0 .../{ => api_reference}/graphicsItems/graphicswidget.rst | 0 .../graphicsItems/graphicswidgetanchor.rst | 0 .../{ => api_reference}/graphicsItems/graphitem.rst | 0 doc/source/{ => api_reference}/graphicsItems/griditem.rst | 0 .../graphicsItems/histogramlutitem.rst | 0 .../{ => api_reference}/graphicsItems/imageitem.rst | 4 ++-- doc/source/{ => api_reference}/graphicsItems/index.rst | 0 .../{ => api_reference}/graphicsItems/infiniteline.rst | 0 .../{ => api_reference}/graphicsItems/isocurveitem.rst | 0 .../{ => api_reference}/graphicsItems/labelitem.rst | 0 .../{ => api_reference}/graphicsItems/legenditem.rst | 0 .../graphicsItems/linearregionitem.rst | 0 doc/source/{ => api_reference}/graphicsItems/make | 0 .../{ => api_reference}/graphicsItems/multiplotitem.rst | 0 .../{ => api_reference}/graphicsItems/pcolormeshitem.rst | 0 .../{ => api_reference}/graphicsItems/plotcurveitem.rst | 0 .../{ => api_reference}/graphicsItems/plotdataitem.rst | 0 doc/source/{ => api_reference}/graphicsItems/plotitem.rst | 0 doc/source/{ => api_reference}/graphicsItems/roi.rst | 0 doc/source/{ => api_reference}/graphicsItems/scalebar.rst | 0 .../{ => api_reference}/graphicsItems/scatterplotitem.rst | 0 .../{ => api_reference}/graphicsItems/targetitem.rst | 0 doc/source/{ => api_reference}/graphicsItems/textitem.rst | 0 .../{ => api_reference}/graphicsItems/uigraphicsitem.rst | 0 doc/source/{ => api_reference}/graphicsItems/viewbox.rst | 0 .../{ => api_reference}/graphicsItems/vtickgroup.rst | 0 .../{ => api_reference}/graphicsscene/graphicsscene.rst | 0 .../{ => api_reference}/graphicsscene/hoverevent.rst | 0 doc/source/{ => api_reference}/graphicsscene/index.rst | 0 .../{ => api_reference}/graphicsscene/mouseclickevent.rst | 0 .../{ => api_reference}/graphicsscene/mousedragevent.rst | 0 doc/source/{apireference.rst => api_reference/index.rst} | 0 doc/source/{ => api_reference}/parametertree/apiref.rst | 0 doc/source/{ => api_reference}/parametertree/index.rst | 0 .../parametertree/interactiveparameters.rst | 0 .../{ => api_reference}/parametertree/parameter.rst | 0 .../{ => api_reference}/parametertree/parameteritem.rst | 0 .../{ => api_reference}/parametertree/parametertree.rst | 0 .../{ => api_reference}/parametertree/parametertypes.rst | 0 doc/source/{ => api_reference}/point.rst | 0 doc/source/{ => api_reference}/transform3d.rst | 0 doc/source/{ => api_reference}/uml_overview.rst | 3 +-- doc/source/{ => api_reference}/widgets/busycursor.rst | 0 doc/source/{ => api_reference}/widgets/checktable.rst | 0 doc/source/{ => api_reference}/widgets/colorbutton.rst | 0 doc/source/{ => api_reference}/widgets/colormapwidget.rst | 0 doc/source/{ => api_reference}/widgets/combobox.rst | 0 doc/source/{ => api_reference}/widgets/consolewidget.rst | 0 doc/source/{ => api_reference}/widgets/datatreewidget.rst | 0 doc/source/{ => api_reference}/widgets/feedbackbutton.rst | 0 doc/source/{ => api_reference}/widgets/filedialog.rst | 0 doc/source/{ => api_reference}/widgets/gradientwidget.rst | 0 .../{ => api_reference}/widgets/graphicslayoutwidget.rst | 0 doc/source/{ => api_reference}/widgets/graphicsview.rst | 0 .../{ => api_reference}/widgets/histogramlutwidget.rst | 0 doc/source/{ => api_reference}/widgets/imageview.rst | 0 doc/source/{ => api_reference}/widgets/index.rst | 0 doc/source/{ => api_reference}/widgets/joystickbutton.rst | 0 doc/source/{ => api_reference}/widgets/layoutwidget.rst | 0 doc/source/{ => api_reference}/widgets/make | 0 .../{ => api_reference}/widgets/matplotlibwidget.rst | 0 .../{ => api_reference}/widgets/multiplotwidget.rst | 0 doc/source/{ => api_reference}/widgets/pathbutton.rst | 0 doc/source/{ => api_reference}/widgets/plotwidget.rst | 0 doc/source/{ => api_reference}/widgets/progressdialog.rst | 0 doc/source/{ => api_reference}/widgets/rawimagewidget.rst | 0 .../{ => api_reference}/widgets/remotegraphicsview.rst | 0 .../{ => api_reference}/widgets/scatterplotwidget.rst | 0 doc/source/{ => api_reference}/widgets/spinbox.rst | 0 doc/source/{ => api_reference}/widgets/tablewidget.rst | 0 doc/source/{ => api_reference}/widgets/treewidget.rst | 0 doc/source/{ => api_reference}/widgets/valuelabel.rst | 0 doc/source/{ => api_reference}/widgets/verticallabel.rst | 0 doc/source/conf.py | 2 +- doc/source/{ => developer_guide}/internals.rst | 0 doc/source/{ => getting_started}/how_to_use.rst | 0 doc/source/{ => getting_started}/images.rst | 0 doc/source/{ => getting_started}/installation.rst | 0 doc/source/{ => getting_started}/introduction.rst | 0 doc/source/{ => getting_started/overview}/3dgraphics.rst | 0 .../overview}/3dgraphics/glaxisitem.rst | 0 .../overview}/3dgraphics/glgraphicsitem.rst | 0 .../overview}/3dgraphics/glgraphitem.rst | 0 .../overview}/3dgraphics/glgriditem.rst | 0 .../overview}/3dgraphics/glimageitem.rst | 0 .../overview}/3dgraphics/gllineplotitem.rst | 0 .../overview}/3dgraphics/glmeshitem.rst | 0 .../overview}/3dgraphics/glscatterplotitem.rst | 0 .../overview}/3dgraphics/glsurfaceplotitem.rst | 0 .../overview}/3dgraphics/gltextitem.rst | 0 .../overview}/3dgraphics/glviewwidget.rst | 0 .../overview}/3dgraphics/glvolumeitem.rst | 0 .../{ => getting_started/overview}/3dgraphics/index.rst | 0 .../overview}/3dgraphics/meshdata.rst | 0 doc/source/{ => getting_started}/plotting.rst | 2 +- doc/source/{ => getting_started}/prototyping.rst | 0 doc/source/{ => getting_started}/qtcrashcourse.rst | 0 doc/source/{ => user_guide}/config_options.rst | 0 doc/source/{ => user_guide}/exporting.rst | 0 doc/source/{ => user_guide}/mouse_interaction.rst | 0 doc/source/{ => user_guide}/region_of_interest.rst | 0 doc/source/{ => user_guide}/style.rst | 0 125 files changed, 10 insertions(+), 11 deletions(-) rename doc/source/{ => api_reference}/colormap.rst (93%) rename doc/source/{ => api_reference}/dockarea.rst (100%) rename doc/source/{ => api_reference}/flowchart/flowchart.rst (100%) rename doc/source/{ => api_reference}/flowchart/index.rst (100%) rename doc/source/{ => api_reference}/flowchart/node.rst (100%) rename doc/source/{ => api_reference}/flowchart/terminal.rst (100%) rename doc/source/{ => api_reference}/functions.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/arrowitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/axisitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/bargraphitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/buttonitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/colorbaritem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/curvearrow.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/curvepoint.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/dateaxisitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/errorbaritem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/fillbetweenitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/gradienteditoritem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/gradientlegend.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/graphicsitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/graphicslayout.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/graphicsobject.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/graphicswidget.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/graphicswidgetanchor.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/graphitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/griditem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/histogramlutitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/imageitem.rst (96%) rename doc/source/{ => api_reference}/graphicsItems/index.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/infiniteline.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/isocurveitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/labelitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/legenditem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/linearregionitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/make (100%) rename doc/source/{ => api_reference}/graphicsItems/multiplotitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/pcolormeshitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/plotcurveitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/plotdataitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/plotitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/roi.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/scalebar.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/scatterplotitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/targetitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/textitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/uigraphicsitem.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/viewbox.rst (100%) rename doc/source/{ => api_reference}/graphicsItems/vtickgroup.rst (100%) rename doc/source/{ => api_reference}/graphicsscene/graphicsscene.rst (100%) rename doc/source/{ => api_reference}/graphicsscene/hoverevent.rst (100%) rename doc/source/{ => api_reference}/graphicsscene/index.rst (100%) rename doc/source/{ => api_reference}/graphicsscene/mouseclickevent.rst (100%) rename doc/source/{ => api_reference}/graphicsscene/mousedragevent.rst (100%) rename doc/source/{apireference.rst => api_reference/index.rst} (100%) rename doc/source/{ => api_reference}/parametertree/apiref.rst (100%) rename doc/source/{ => api_reference}/parametertree/index.rst (100%) rename doc/source/{ => api_reference}/parametertree/interactiveparameters.rst (100%) rename doc/source/{ => api_reference}/parametertree/parameter.rst (100%) rename doc/source/{ => api_reference}/parametertree/parameteritem.rst (100%) rename doc/source/{ => api_reference}/parametertree/parametertree.rst (100%) rename doc/source/{ => api_reference}/parametertree/parametertypes.rst (100%) rename doc/source/{ => api_reference}/point.rst (100%) rename doc/source/{ => api_reference}/transform3d.rst (100%) rename doc/source/{ => api_reference}/uml_overview.rst (94%) rename doc/source/{ => api_reference}/widgets/busycursor.rst (100%) rename doc/source/{ => api_reference}/widgets/checktable.rst (100%) rename doc/source/{ => api_reference}/widgets/colorbutton.rst (100%) rename doc/source/{ => api_reference}/widgets/colormapwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/combobox.rst (100%) rename doc/source/{ => api_reference}/widgets/consolewidget.rst (100%) rename doc/source/{ => api_reference}/widgets/datatreewidget.rst (100%) rename doc/source/{ => api_reference}/widgets/feedbackbutton.rst (100%) rename doc/source/{ => api_reference}/widgets/filedialog.rst (100%) rename doc/source/{ => api_reference}/widgets/gradientwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/graphicslayoutwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/graphicsview.rst (100%) rename doc/source/{ => api_reference}/widgets/histogramlutwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/imageview.rst (100%) rename doc/source/{ => api_reference}/widgets/index.rst (100%) rename doc/source/{ => api_reference}/widgets/joystickbutton.rst (100%) rename doc/source/{ => api_reference}/widgets/layoutwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/make (100%) rename doc/source/{ => api_reference}/widgets/matplotlibwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/multiplotwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/pathbutton.rst (100%) rename doc/source/{ => api_reference}/widgets/plotwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/progressdialog.rst (100%) rename doc/source/{ => api_reference}/widgets/rawimagewidget.rst (100%) rename doc/source/{ => api_reference}/widgets/remotegraphicsview.rst (100%) rename doc/source/{ => api_reference}/widgets/scatterplotwidget.rst (100%) rename doc/source/{ => api_reference}/widgets/spinbox.rst (100%) rename doc/source/{ => api_reference}/widgets/tablewidget.rst (100%) rename doc/source/{ => api_reference}/widgets/treewidget.rst (100%) rename doc/source/{ => api_reference}/widgets/valuelabel.rst (100%) rename doc/source/{ => api_reference}/widgets/verticallabel.rst (100%) rename doc/source/{ => developer_guide}/internals.rst (100%) rename doc/source/{ => getting_started}/how_to_use.rst (100%) rename doc/source/{ => getting_started}/images.rst (100%) rename doc/source/{ => getting_started}/installation.rst (100%) rename doc/source/{ => getting_started}/introduction.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glaxisitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glgraphicsitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glgraphitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glgriditem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glimageitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/gllineplotitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glmeshitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glscatterplotitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glsurfaceplotitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/gltextitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glviewwidget.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/glvolumeitem.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/index.rst (100%) rename doc/source/{ => getting_started/overview}/3dgraphics/meshdata.rst (100%) rename doc/source/{ => getting_started}/plotting.rst (99%) rename doc/source/{ => getting_started}/prototyping.rst (100%) rename doc/source/{ => getting_started}/qtcrashcourse.rst (100%) rename doc/source/{ => user_guide}/config_options.rst (100%) rename doc/source/{ => user_guide}/exporting.rst (100%) rename doc/source/{ => user_guide}/mouse_interaction.rst (100%) rename doc/source/{ => user_guide}/region_of_interest.rst (100%) rename doc/source/{ => user_guide}/style.rst (100%) diff --git a/doc/requirements.txt b/doc/requirements.txt index 2352cbb393..d6ebca69b4 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ sphinx==5.2.2 PyQt6==6.3.1 -sphinx-rtd-theme +pydata-sphinx-theme sphinx-qt-documentation sphinx-design numpy diff --git a/doc/source/colormap.rst b/doc/source/api_reference/colormap.rst similarity index 93% rename from doc/source/colormap.rst rename to doc/source/api_reference/colormap.rst index c3c3b85b5a..4e40fa3fdc 100644 --- a/doc/source/colormap.rst +++ b/doc/source/api_reference/colormap.rst @@ -45,23 +45,23 @@ Examples False color display of a 2D data set. Display levels are controlled by a :class:`ColorBarItem `: -.. literalinclude:: images/gen_example_false_color_image.py +.. literalinclude:: /images/gen_example_false_color_image.py :lines: 18-28 :dedent: 8 Using QtGui.QPen and QtGui.QBrush to color plots according to the plotted value: -.. literalinclude:: images/gen_example_gradient_plot.py +.. literalinclude:: /images/gen_example_gradient_plot.py :lines: 16-33 :dedent: 8 .. image:: - images/example_false_color_image.png + /images/example_false_color_image.png :width: 49% :alt: Example of a false color image .. image:: - images/example_gradient_plot.png + /images/example_gradient_plot.png :width: 49% :alt: Example of drawing and filling plots with gradients diff --git a/doc/source/dockarea.rst b/doc/source/api_reference/dockarea.rst similarity index 100% rename from doc/source/dockarea.rst rename to doc/source/api_reference/dockarea.rst diff --git a/doc/source/flowchart/flowchart.rst b/doc/source/api_reference/flowchart/flowchart.rst similarity index 100% rename from doc/source/flowchart/flowchart.rst rename to doc/source/api_reference/flowchart/flowchart.rst diff --git a/doc/source/flowchart/index.rst b/doc/source/api_reference/flowchart/index.rst similarity index 100% rename from doc/source/flowchart/index.rst rename to doc/source/api_reference/flowchart/index.rst diff --git a/doc/source/flowchart/node.rst b/doc/source/api_reference/flowchart/node.rst similarity index 100% rename from doc/source/flowchart/node.rst rename to doc/source/api_reference/flowchart/node.rst diff --git a/doc/source/flowchart/terminal.rst b/doc/source/api_reference/flowchart/terminal.rst similarity index 100% rename from doc/source/flowchart/terminal.rst rename to doc/source/api_reference/flowchart/terminal.rst diff --git a/doc/source/functions.rst b/doc/source/api_reference/functions.rst similarity index 100% rename from doc/source/functions.rst rename to doc/source/api_reference/functions.rst diff --git a/doc/source/graphicsItems/arrowitem.rst b/doc/source/api_reference/graphicsItems/arrowitem.rst similarity index 100% rename from doc/source/graphicsItems/arrowitem.rst rename to doc/source/api_reference/graphicsItems/arrowitem.rst diff --git a/doc/source/graphicsItems/axisitem.rst b/doc/source/api_reference/graphicsItems/axisitem.rst similarity index 100% rename from doc/source/graphicsItems/axisitem.rst rename to doc/source/api_reference/graphicsItems/axisitem.rst diff --git a/doc/source/graphicsItems/bargraphitem.rst b/doc/source/api_reference/graphicsItems/bargraphitem.rst similarity index 100% rename from doc/source/graphicsItems/bargraphitem.rst rename to doc/source/api_reference/graphicsItems/bargraphitem.rst diff --git a/doc/source/graphicsItems/buttonitem.rst b/doc/source/api_reference/graphicsItems/buttonitem.rst similarity index 100% rename from doc/source/graphicsItems/buttonitem.rst rename to doc/source/api_reference/graphicsItems/buttonitem.rst diff --git a/doc/source/graphicsItems/colorbaritem.rst b/doc/source/api_reference/graphicsItems/colorbaritem.rst similarity index 100% rename from doc/source/graphicsItems/colorbaritem.rst rename to doc/source/api_reference/graphicsItems/colorbaritem.rst diff --git a/doc/source/graphicsItems/curvearrow.rst b/doc/source/api_reference/graphicsItems/curvearrow.rst similarity index 100% rename from doc/source/graphicsItems/curvearrow.rst rename to doc/source/api_reference/graphicsItems/curvearrow.rst diff --git a/doc/source/graphicsItems/curvepoint.rst b/doc/source/api_reference/graphicsItems/curvepoint.rst similarity index 100% rename from doc/source/graphicsItems/curvepoint.rst rename to doc/source/api_reference/graphicsItems/curvepoint.rst diff --git a/doc/source/graphicsItems/dateaxisitem.rst b/doc/source/api_reference/graphicsItems/dateaxisitem.rst similarity index 100% rename from doc/source/graphicsItems/dateaxisitem.rst rename to doc/source/api_reference/graphicsItems/dateaxisitem.rst diff --git a/doc/source/graphicsItems/errorbaritem.rst b/doc/source/api_reference/graphicsItems/errorbaritem.rst similarity index 100% rename from doc/source/graphicsItems/errorbaritem.rst rename to doc/source/api_reference/graphicsItems/errorbaritem.rst diff --git a/doc/source/graphicsItems/fillbetweenitem.rst b/doc/source/api_reference/graphicsItems/fillbetweenitem.rst similarity index 100% rename from doc/source/graphicsItems/fillbetweenitem.rst rename to doc/source/api_reference/graphicsItems/fillbetweenitem.rst diff --git a/doc/source/graphicsItems/gradienteditoritem.rst b/doc/source/api_reference/graphicsItems/gradienteditoritem.rst similarity index 100% rename from doc/source/graphicsItems/gradienteditoritem.rst rename to doc/source/api_reference/graphicsItems/gradienteditoritem.rst diff --git a/doc/source/graphicsItems/gradientlegend.rst b/doc/source/api_reference/graphicsItems/gradientlegend.rst similarity index 100% rename from doc/source/graphicsItems/gradientlegend.rst rename to doc/source/api_reference/graphicsItems/gradientlegend.rst diff --git a/doc/source/graphicsItems/graphicsitem.rst b/doc/source/api_reference/graphicsItems/graphicsitem.rst similarity index 100% rename from doc/source/graphicsItems/graphicsitem.rst rename to doc/source/api_reference/graphicsItems/graphicsitem.rst diff --git a/doc/source/graphicsItems/graphicslayout.rst b/doc/source/api_reference/graphicsItems/graphicslayout.rst similarity index 100% rename from doc/source/graphicsItems/graphicslayout.rst rename to doc/source/api_reference/graphicsItems/graphicslayout.rst diff --git a/doc/source/graphicsItems/graphicsobject.rst b/doc/source/api_reference/graphicsItems/graphicsobject.rst similarity index 100% rename from doc/source/graphicsItems/graphicsobject.rst rename to doc/source/api_reference/graphicsItems/graphicsobject.rst diff --git a/doc/source/graphicsItems/graphicswidget.rst b/doc/source/api_reference/graphicsItems/graphicswidget.rst similarity index 100% rename from doc/source/graphicsItems/graphicswidget.rst rename to doc/source/api_reference/graphicsItems/graphicswidget.rst diff --git a/doc/source/graphicsItems/graphicswidgetanchor.rst b/doc/source/api_reference/graphicsItems/graphicswidgetanchor.rst similarity index 100% rename from doc/source/graphicsItems/graphicswidgetanchor.rst rename to doc/source/api_reference/graphicsItems/graphicswidgetanchor.rst diff --git a/doc/source/graphicsItems/graphitem.rst b/doc/source/api_reference/graphicsItems/graphitem.rst similarity index 100% rename from doc/source/graphicsItems/graphitem.rst rename to doc/source/api_reference/graphicsItems/graphitem.rst diff --git a/doc/source/graphicsItems/griditem.rst b/doc/source/api_reference/graphicsItems/griditem.rst similarity index 100% rename from doc/source/graphicsItems/griditem.rst rename to doc/source/api_reference/graphicsItems/griditem.rst diff --git a/doc/source/graphicsItems/histogramlutitem.rst b/doc/source/api_reference/graphicsItems/histogramlutitem.rst similarity index 100% rename from doc/source/graphicsItems/histogramlutitem.rst rename to doc/source/api_reference/graphicsItems/histogramlutitem.rst diff --git a/doc/source/graphicsItems/imageitem.rst b/doc/source/api_reference/graphicsItems/imageitem.rst similarity index 96% rename from doc/source/graphicsItems/imageitem.rst rename to doc/source/api_reference/graphicsItems/imageitem.rst index c590f80f0a..d33c02f6c7 100644 --- a/doc/source/graphicsItems/imageitem.rst +++ b/doc/source/api_reference/graphicsItems/imageitem.rst @@ -50,12 +50,12 @@ If performance is critial, the following points may be worth investigating: Examples -------- -.. literalinclude:: ../images/gen_example_imageitem_transform.py +.. literalinclude:: /images/gen_example_imageitem_transform.py :lines: 19-28 :dedent: 8 .. image:: - ../images/example_imageitem_transform.png + /images/example_imageitem_transform.png :width: 49% :alt: Example of transformed image display diff --git a/doc/source/graphicsItems/index.rst b/doc/source/api_reference/graphicsItems/index.rst similarity index 100% rename from doc/source/graphicsItems/index.rst rename to doc/source/api_reference/graphicsItems/index.rst diff --git a/doc/source/graphicsItems/infiniteline.rst b/doc/source/api_reference/graphicsItems/infiniteline.rst similarity index 100% rename from doc/source/graphicsItems/infiniteline.rst rename to doc/source/api_reference/graphicsItems/infiniteline.rst diff --git a/doc/source/graphicsItems/isocurveitem.rst b/doc/source/api_reference/graphicsItems/isocurveitem.rst similarity index 100% rename from doc/source/graphicsItems/isocurveitem.rst rename to doc/source/api_reference/graphicsItems/isocurveitem.rst diff --git a/doc/source/graphicsItems/labelitem.rst b/doc/source/api_reference/graphicsItems/labelitem.rst similarity index 100% rename from doc/source/graphicsItems/labelitem.rst rename to doc/source/api_reference/graphicsItems/labelitem.rst diff --git a/doc/source/graphicsItems/legenditem.rst b/doc/source/api_reference/graphicsItems/legenditem.rst similarity index 100% rename from doc/source/graphicsItems/legenditem.rst rename to doc/source/api_reference/graphicsItems/legenditem.rst diff --git a/doc/source/graphicsItems/linearregionitem.rst b/doc/source/api_reference/graphicsItems/linearregionitem.rst similarity index 100% rename from doc/source/graphicsItems/linearregionitem.rst rename to doc/source/api_reference/graphicsItems/linearregionitem.rst diff --git a/doc/source/graphicsItems/make b/doc/source/api_reference/graphicsItems/make similarity index 100% rename from doc/source/graphicsItems/make rename to doc/source/api_reference/graphicsItems/make diff --git a/doc/source/graphicsItems/multiplotitem.rst b/doc/source/api_reference/graphicsItems/multiplotitem.rst similarity index 100% rename from doc/source/graphicsItems/multiplotitem.rst rename to doc/source/api_reference/graphicsItems/multiplotitem.rst diff --git a/doc/source/graphicsItems/pcolormeshitem.rst b/doc/source/api_reference/graphicsItems/pcolormeshitem.rst similarity index 100% rename from doc/source/graphicsItems/pcolormeshitem.rst rename to doc/source/api_reference/graphicsItems/pcolormeshitem.rst diff --git a/doc/source/graphicsItems/plotcurveitem.rst b/doc/source/api_reference/graphicsItems/plotcurveitem.rst similarity index 100% rename from doc/source/graphicsItems/plotcurveitem.rst rename to doc/source/api_reference/graphicsItems/plotcurveitem.rst diff --git a/doc/source/graphicsItems/plotdataitem.rst b/doc/source/api_reference/graphicsItems/plotdataitem.rst similarity index 100% rename from doc/source/graphicsItems/plotdataitem.rst rename to doc/source/api_reference/graphicsItems/plotdataitem.rst diff --git a/doc/source/graphicsItems/plotitem.rst b/doc/source/api_reference/graphicsItems/plotitem.rst similarity index 100% rename from doc/source/graphicsItems/plotitem.rst rename to doc/source/api_reference/graphicsItems/plotitem.rst diff --git a/doc/source/graphicsItems/roi.rst b/doc/source/api_reference/graphicsItems/roi.rst similarity index 100% rename from doc/source/graphicsItems/roi.rst rename to doc/source/api_reference/graphicsItems/roi.rst diff --git a/doc/source/graphicsItems/scalebar.rst b/doc/source/api_reference/graphicsItems/scalebar.rst similarity index 100% rename from doc/source/graphicsItems/scalebar.rst rename to doc/source/api_reference/graphicsItems/scalebar.rst diff --git a/doc/source/graphicsItems/scatterplotitem.rst b/doc/source/api_reference/graphicsItems/scatterplotitem.rst similarity index 100% rename from doc/source/graphicsItems/scatterplotitem.rst rename to doc/source/api_reference/graphicsItems/scatterplotitem.rst diff --git a/doc/source/graphicsItems/targetitem.rst b/doc/source/api_reference/graphicsItems/targetitem.rst similarity index 100% rename from doc/source/graphicsItems/targetitem.rst rename to doc/source/api_reference/graphicsItems/targetitem.rst diff --git a/doc/source/graphicsItems/textitem.rst b/doc/source/api_reference/graphicsItems/textitem.rst similarity index 100% rename from doc/source/graphicsItems/textitem.rst rename to doc/source/api_reference/graphicsItems/textitem.rst diff --git a/doc/source/graphicsItems/uigraphicsitem.rst b/doc/source/api_reference/graphicsItems/uigraphicsitem.rst similarity index 100% rename from doc/source/graphicsItems/uigraphicsitem.rst rename to doc/source/api_reference/graphicsItems/uigraphicsitem.rst diff --git a/doc/source/graphicsItems/viewbox.rst b/doc/source/api_reference/graphicsItems/viewbox.rst similarity index 100% rename from doc/source/graphicsItems/viewbox.rst rename to doc/source/api_reference/graphicsItems/viewbox.rst diff --git a/doc/source/graphicsItems/vtickgroup.rst b/doc/source/api_reference/graphicsItems/vtickgroup.rst similarity index 100% rename from doc/source/graphicsItems/vtickgroup.rst rename to doc/source/api_reference/graphicsItems/vtickgroup.rst diff --git a/doc/source/graphicsscene/graphicsscene.rst b/doc/source/api_reference/graphicsscene/graphicsscene.rst similarity index 100% rename from doc/source/graphicsscene/graphicsscene.rst rename to doc/source/api_reference/graphicsscene/graphicsscene.rst diff --git a/doc/source/graphicsscene/hoverevent.rst b/doc/source/api_reference/graphicsscene/hoverevent.rst similarity index 100% rename from doc/source/graphicsscene/hoverevent.rst rename to doc/source/api_reference/graphicsscene/hoverevent.rst diff --git a/doc/source/graphicsscene/index.rst b/doc/source/api_reference/graphicsscene/index.rst similarity index 100% rename from doc/source/graphicsscene/index.rst rename to doc/source/api_reference/graphicsscene/index.rst diff --git a/doc/source/graphicsscene/mouseclickevent.rst b/doc/source/api_reference/graphicsscene/mouseclickevent.rst similarity index 100% rename from doc/source/graphicsscene/mouseclickevent.rst rename to doc/source/api_reference/graphicsscene/mouseclickevent.rst diff --git a/doc/source/graphicsscene/mousedragevent.rst b/doc/source/api_reference/graphicsscene/mousedragevent.rst similarity index 100% rename from doc/source/graphicsscene/mousedragevent.rst rename to doc/source/api_reference/graphicsscene/mousedragevent.rst diff --git a/doc/source/apireference.rst b/doc/source/api_reference/index.rst similarity index 100% rename from doc/source/apireference.rst rename to doc/source/api_reference/index.rst diff --git a/doc/source/parametertree/apiref.rst b/doc/source/api_reference/parametertree/apiref.rst similarity index 100% rename from doc/source/parametertree/apiref.rst rename to doc/source/api_reference/parametertree/apiref.rst diff --git a/doc/source/parametertree/index.rst b/doc/source/api_reference/parametertree/index.rst similarity index 100% rename from doc/source/parametertree/index.rst rename to doc/source/api_reference/parametertree/index.rst diff --git a/doc/source/parametertree/interactiveparameters.rst b/doc/source/api_reference/parametertree/interactiveparameters.rst similarity index 100% rename from doc/source/parametertree/interactiveparameters.rst rename to doc/source/api_reference/parametertree/interactiveparameters.rst diff --git a/doc/source/parametertree/parameter.rst b/doc/source/api_reference/parametertree/parameter.rst similarity index 100% rename from doc/source/parametertree/parameter.rst rename to doc/source/api_reference/parametertree/parameter.rst diff --git a/doc/source/parametertree/parameteritem.rst b/doc/source/api_reference/parametertree/parameteritem.rst similarity index 100% rename from doc/source/parametertree/parameteritem.rst rename to doc/source/api_reference/parametertree/parameteritem.rst diff --git a/doc/source/parametertree/parametertree.rst b/doc/source/api_reference/parametertree/parametertree.rst similarity index 100% rename from doc/source/parametertree/parametertree.rst rename to doc/source/api_reference/parametertree/parametertree.rst diff --git a/doc/source/parametertree/parametertypes.rst b/doc/source/api_reference/parametertree/parametertypes.rst similarity index 100% rename from doc/source/parametertree/parametertypes.rst rename to doc/source/api_reference/parametertree/parametertypes.rst diff --git a/doc/source/point.rst b/doc/source/api_reference/point.rst similarity index 100% rename from doc/source/point.rst rename to doc/source/api_reference/point.rst diff --git a/doc/source/transform3d.rst b/doc/source/api_reference/transform3d.rst similarity index 100% rename from doc/source/transform3d.rst rename to doc/source/api_reference/transform3d.rst diff --git a/doc/source/uml_overview.rst b/doc/source/api_reference/uml_overview.rst similarity index 94% rename from doc/source/uml_overview.rst rename to doc/source/api_reference/uml_overview.rst index 5e71849892..18168ee8a0 100644 --- a/doc/source/uml_overview.rst +++ b/doc/source/api_reference/uml_overview.rst @@ -11,5 +11,4 @@ The black arrows indicate inheritance between two classes (with the parent class The gray lines with the diamonds indicate an aggregation relation. For example the :class:`PlotDataItem ` class has a ``curve`` attribute that is a reference to a :class:`PlotCurveItem ` object. -.. image:: images/overview_uml.svg - +.. image:: /images/overview_uml.svg diff --git a/doc/source/widgets/busycursor.rst b/doc/source/api_reference/widgets/busycursor.rst similarity index 100% rename from doc/source/widgets/busycursor.rst rename to doc/source/api_reference/widgets/busycursor.rst diff --git a/doc/source/widgets/checktable.rst b/doc/source/api_reference/widgets/checktable.rst similarity index 100% rename from doc/source/widgets/checktable.rst rename to doc/source/api_reference/widgets/checktable.rst diff --git a/doc/source/widgets/colorbutton.rst b/doc/source/api_reference/widgets/colorbutton.rst similarity index 100% rename from doc/source/widgets/colorbutton.rst rename to doc/source/api_reference/widgets/colorbutton.rst diff --git a/doc/source/widgets/colormapwidget.rst b/doc/source/api_reference/widgets/colormapwidget.rst similarity index 100% rename from doc/source/widgets/colormapwidget.rst rename to doc/source/api_reference/widgets/colormapwidget.rst diff --git a/doc/source/widgets/combobox.rst b/doc/source/api_reference/widgets/combobox.rst similarity index 100% rename from doc/source/widgets/combobox.rst rename to doc/source/api_reference/widgets/combobox.rst diff --git a/doc/source/widgets/consolewidget.rst b/doc/source/api_reference/widgets/consolewidget.rst similarity index 100% rename from doc/source/widgets/consolewidget.rst rename to doc/source/api_reference/widgets/consolewidget.rst diff --git a/doc/source/widgets/datatreewidget.rst b/doc/source/api_reference/widgets/datatreewidget.rst similarity index 100% rename from doc/source/widgets/datatreewidget.rst rename to doc/source/api_reference/widgets/datatreewidget.rst diff --git a/doc/source/widgets/feedbackbutton.rst b/doc/source/api_reference/widgets/feedbackbutton.rst similarity index 100% rename from doc/source/widgets/feedbackbutton.rst rename to doc/source/api_reference/widgets/feedbackbutton.rst diff --git a/doc/source/widgets/filedialog.rst b/doc/source/api_reference/widgets/filedialog.rst similarity index 100% rename from doc/source/widgets/filedialog.rst rename to doc/source/api_reference/widgets/filedialog.rst diff --git a/doc/source/widgets/gradientwidget.rst b/doc/source/api_reference/widgets/gradientwidget.rst similarity index 100% rename from doc/source/widgets/gradientwidget.rst rename to doc/source/api_reference/widgets/gradientwidget.rst diff --git a/doc/source/widgets/graphicslayoutwidget.rst b/doc/source/api_reference/widgets/graphicslayoutwidget.rst similarity index 100% rename from doc/source/widgets/graphicslayoutwidget.rst rename to doc/source/api_reference/widgets/graphicslayoutwidget.rst diff --git a/doc/source/widgets/graphicsview.rst b/doc/source/api_reference/widgets/graphicsview.rst similarity index 100% rename from doc/source/widgets/graphicsview.rst rename to doc/source/api_reference/widgets/graphicsview.rst diff --git a/doc/source/widgets/histogramlutwidget.rst b/doc/source/api_reference/widgets/histogramlutwidget.rst similarity index 100% rename from doc/source/widgets/histogramlutwidget.rst rename to doc/source/api_reference/widgets/histogramlutwidget.rst diff --git a/doc/source/widgets/imageview.rst b/doc/source/api_reference/widgets/imageview.rst similarity index 100% rename from doc/source/widgets/imageview.rst rename to doc/source/api_reference/widgets/imageview.rst diff --git a/doc/source/widgets/index.rst b/doc/source/api_reference/widgets/index.rst similarity index 100% rename from doc/source/widgets/index.rst rename to doc/source/api_reference/widgets/index.rst diff --git a/doc/source/widgets/joystickbutton.rst b/doc/source/api_reference/widgets/joystickbutton.rst similarity index 100% rename from doc/source/widgets/joystickbutton.rst rename to doc/source/api_reference/widgets/joystickbutton.rst diff --git a/doc/source/widgets/layoutwidget.rst b/doc/source/api_reference/widgets/layoutwidget.rst similarity index 100% rename from doc/source/widgets/layoutwidget.rst rename to doc/source/api_reference/widgets/layoutwidget.rst diff --git a/doc/source/widgets/make b/doc/source/api_reference/widgets/make similarity index 100% rename from doc/source/widgets/make rename to doc/source/api_reference/widgets/make diff --git a/doc/source/widgets/matplotlibwidget.rst b/doc/source/api_reference/widgets/matplotlibwidget.rst similarity index 100% rename from doc/source/widgets/matplotlibwidget.rst rename to doc/source/api_reference/widgets/matplotlibwidget.rst diff --git a/doc/source/widgets/multiplotwidget.rst b/doc/source/api_reference/widgets/multiplotwidget.rst similarity index 100% rename from doc/source/widgets/multiplotwidget.rst rename to doc/source/api_reference/widgets/multiplotwidget.rst diff --git a/doc/source/widgets/pathbutton.rst b/doc/source/api_reference/widgets/pathbutton.rst similarity index 100% rename from doc/source/widgets/pathbutton.rst rename to doc/source/api_reference/widgets/pathbutton.rst diff --git a/doc/source/widgets/plotwidget.rst b/doc/source/api_reference/widgets/plotwidget.rst similarity index 100% rename from doc/source/widgets/plotwidget.rst rename to doc/source/api_reference/widgets/plotwidget.rst diff --git a/doc/source/widgets/progressdialog.rst b/doc/source/api_reference/widgets/progressdialog.rst similarity index 100% rename from doc/source/widgets/progressdialog.rst rename to doc/source/api_reference/widgets/progressdialog.rst diff --git a/doc/source/widgets/rawimagewidget.rst b/doc/source/api_reference/widgets/rawimagewidget.rst similarity index 100% rename from doc/source/widgets/rawimagewidget.rst rename to doc/source/api_reference/widgets/rawimagewidget.rst diff --git a/doc/source/widgets/remotegraphicsview.rst b/doc/source/api_reference/widgets/remotegraphicsview.rst similarity index 100% rename from doc/source/widgets/remotegraphicsview.rst rename to doc/source/api_reference/widgets/remotegraphicsview.rst diff --git a/doc/source/widgets/scatterplotwidget.rst b/doc/source/api_reference/widgets/scatterplotwidget.rst similarity index 100% rename from doc/source/widgets/scatterplotwidget.rst rename to doc/source/api_reference/widgets/scatterplotwidget.rst diff --git a/doc/source/widgets/spinbox.rst b/doc/source/api_reference/widgets/spinbox.rst similarity index 100% rename from doc/source/widgets/spinbox.rst rename to doc/source/api_reference/widgets/spinbox.rst diff --git a/doc/source/widgets/tablewidget.rst b/doc/source/api_reference/widgets/tablewidget.rst similarity index 100% rename from doc/source/widgets/tablewidget.rst rename to doc/source/api_reference/widgets/tablewidget.rst diff --git a/doc/source/widgets/treewidget.rst b/doc/source/api_reference/widgets/treewidget.rst similarity index 100% rename from doc/source/widgets/treewidget.rst rename to doc/source/api_reference/widgets/treewidget.rst diff --git a/doc/source/widgets/valuelabel.rst b/doc/source/api_reference/widgets/valuelabel.rst similarity index 100% rename from doc/source/widgets/valuelabel.rst rename to doc/source/api_reference/widgets/valuelabel.rst diff --git a/doc/source/widgets/verticallabel.rst b/doc/source/api_reference/widgets/verticallabel.rst similarity index 100% rename from doc/source/widgets/verticallabel.rst rename to doc/source/api_reference/widgets/verticallabel.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index ebec67d9ce..8e95ab224e 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -132,7 +132,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = 'pydata_sphinx_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/doc/source/internals.rst b/doc/source/developer_guide/internals.rst similarity index 100% rename from doc/source/internals.rst rename to doc/source/developer_guide/internals.rst diff --git a/doc/source/how_to_use.rst b/doc/source/getting_started/how_to_use.rst similarity index 100% rename from doc/source/how_to_use.rst rename to doc/source/getting_started/how_to_use.rst diff --git a/doc/source/images.rst b/doc/source/getting_started/images.rst similarity index 100% rename from doc/source/images.rst rename to doc/source/getting_started/images.rst diff --git a/doc/source/installation.rst b/doc/source/getting_started/installation.rst similarity index 100% rename from doc/source/installation.rst rename to doc/source/getting_started/installation.rst diff --git a/doc/source/introduction.rst b/doc/source/getting_started/introduction.rst similarity index 100% rename from doc/source/introduction.rst rename to doc/source/getting_started/introduction.rst diff --git a/doc/source/3dgraphics.rst b/doc/source/getting_started/overview/3dgraphics.rst similarity index 100% rename from doc/source/3dgraphics.rst rename to doc/source/getting_started/overview/3dgraphics.rst diff --git a/doc/source/3dgraphics/glaxisitem.rst b/doc/source/getting_started/overview/3dgraphics/glaxisitem.rst similarity index 100% rename from doc/source/3dgraphics/glaxisitem.rst rename to doc/source/getting_started/overview/3dgraphics/glaxisitem.rst diff --git a/doc/source/3dgraphics/glgraphicsitem.rst b/doc/source/getting_started/overview/3dgraphics/glgraphicsitem.rst similarity index 100% rename from doc/source/3dgraphics/glgraphicsitem.rst rename to doc/source/getting_started/overview/3dgraphics/glgraphicsitem.rst diff --git a/doc/source/3dgraphics/glgraphitem.rst b/doc/source/getting_started/overview/3dgraphics/glgraphitem.rst similarity index 100% rename from doc/source/3dgraphics/glgraphitem.rst rename to doc/source/getting_started/overview/3dgraphics/glgraphitem.rst diff --git a/doc/source/3dgraphics/glgriditem.rst b/doc/source/getting_started/overview/3dgraphics/glgriditem.rst similarity index 100% rename from doc/source/3dgraphics/glgriditem.rst rename to doc/source/getting_started/overview/3dgraphics/glgriditem.rst diff --git a/doc/source/3dgraphics/glimageitem.rst b/doc/source/getting_started/overview/3dgraphics/glimageitem.rst similarity index 100% rename from doc/source/3dgraphics/glimageitem.rst rename to doc/source/getting_started/overview/3dgraphics/glimageitem.rst diff --git a/doc/source/3dgraphics/gllineplotitem.rst b/doc/source/getting_started/overview/3dgraphics/gllineplotitem.rst similarity index 100% rename from doc/source/3dgraphics/gllineplotitem.rst rename to doc/source/getting_started/overview/3dgraphics/gllineplotitem.rst diff --git a/doc/source/3dgraphics/glmeshitem.rst b/doc/source/getting_started/overview/3dgraphics/glmeshitem.rst similarity index 100% rename from doc/source/3dgraphics/glmeshitem.rst rename to doc/source/getting_started/overview/3dgraphics/glmeshitem.rst diff --git a/doc/source/3dgraphics/glscatterplotitem.rst b/doc/source/getting_started/overview/3dgraphics/glscatterplotitem.rst similarity index 100% rename from doc/source/3dgraphics/glscatterplotitem.rst rename to doc/source/getting_started/overview/3dgraphics/glscatterplotitem.rst diff --git a/doc/source/3dgraphics/glsurfaceplotitem.rst b/doc/source/getting_started/overview/3dgraphics/glsurfaceplotitem.rst similarity index 100% rename from doc/source/3dgraphics/glsurfaceplotitem.rst rename to doc/source/getting_started/overview/3dgraphics/glsurfaceplotitem.rst diff --git a/doc/source/3dgraphics/gltextitem.rst b/doc/source/getting_started/overview/3dgraphics/gltextitem.rst similarity index 100% rename from doc/source/3dgraphics/gltextitem.rst rename to doc/source/getting_started/overview/3dgraphics/gltextitem.rst diff --git a/doc/source/3dgraphics/glviewwidget.rst b/doc/source/getting_started/overview/3dgraphics/glviewwidget.rst similarity index 100% rename from doc/source/3dgraphics/glviewwidget.rst rename to doc/source/getting_started/overview/3dgraphics/glviewwidget.rst diff --git a/doc/source/3dgraphics/glvolumeitem.rst b/doc/source/getting_started/overview/3dgraphics/glvolumeitem.rst similarity index 100% rename from doc/source/3dgraphics/glvolumeitem.rst rename to doc/source/getting_started/overview/3dgraphics/glvolumeitem.rst diff --git a/doc/source/3dgraphics/index.rst b/doc/source/getting_started/overview/3dgraphics/index.rst similarity index 100% rename from doc/source/3dgraphics/index.rst rename to doc/source/getting_started/overview/3dgraphics/index.rst diff --git a/doc/source/3dgraphics/meshdata.rst b/doc/source/getting_started/overview/3dgraphics/meshdata.rst similarity index 100% rename from doc/source/3dgraphics/meshdata.rst rename to doc/source/getting_started/overview/3dgraphics/meshdata.rst diff --git a/doc/source/plotting.rst b/doc/source/getting_started/plotting.rst similarity index 99% rename from doc/source/plotting.rst rename to doc/source/getting_started/plotting.rst index 0c9abb97ef..c9a489e266 100644 --- a/doc/source/plotting.rst +++ b/doc/source/getting_started/plotting.rst @@ -43,7 +43,7 @@ There are several classes invloved in displaying plot data. Most of these classe * :class:`PlotWidget ` - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. * :class:`GraphicsLayoutWidget ` - QWidget subclass displaying a single :class:`~pyqtgraph.GraphicsLayout`. Most of the methods provided by :class:`~pyqtgraph.GraphicsLayout` are also available through GraphicsLayoutWidget. -.. image:: images/plottingClasses.png +.. image:: /images/plottingClasses.png See the :ref:`UML class diagram ` page for a more detailed figure of the most important classes and their relations. diff --git a/doc/source/prototyping.rst b/doc/source/getting_started/prototyping.rst similarity index 100% rename from doc/source/prototyping.rst rename to doc/source/getting_started/prototyping.rst diff --git a/doc/source/qtcrashcourse.rst b/doc/source/getting_started/qtcrashcourse.rst similarity index 100% rename from doc/source/qtcrashcourse.rst rename to doc/source/getting_started/qtcrashcourse.rst diff --git a/doc/source/config_options.rst b/doc/source/user_guide/config_options.rst similarity index 100% rename from doc/source/config_options.rst rename to doc/source/user_guide/config_options.rst diff --git a/doc/source/exporting.rst b/doc/source/user_guide/exporting.rst similarity index 100% rename from doc/source/exporting.rst rename to doc/source/user_guide/exporting.rst diff --git a/doc/source/mouse_interaction.rst b/doc/source/user_guide/mouse_interaction.rst similarity index 100% rename from doc/source/mouse_interaction.rst rename to doc/source/user_guide/mouse_interaction.rst diff --git a/doc/source/region_of_interest.rst b/doc/source/user_guide/region_of_interest.rst similarity index 100% rename from doc/source/region_of_interest.rst rename to doc/source/user_guide/region_of_interest.rst diff --git a/doc/source/style.rst b/doc/source/user_guide/style.rst similarity index 100% rename from doc/source/style.rst rename to doc/source/user_guide/style.rst From 3e4babe3058bc0d18e1fbaadfc049a547bc617b7 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 1 Oct 2022 15:57:42 -0700 Subject: [PATCH 076/306] Add index files for each new top-level section Also move 3dgraphics API stubs back to API reference section Redo landing page with standard buttons, thanks SciPy try using the scipy.css file Use svg images from xarray use css and conf.py from scipy CSS styling more in line with what we want More descriptive text for each section Attribute scipy, add better text for landing page like the green buttons more on the landing page --- doc/source/_static/custom.css | 172 ++++++++++++++++-- .../3dgraphics/glaxisitem.rst | 0 .../3dgraphics/glgraphicsitem.rst | 0 .../3dgraphics/glgraphitem.rst | 0 .../3dgraphics/glgriditem.rst | 0 .../3dgraphics/glimageitem.rst | 0 .../3dgraphics/gllineplotitem.rst | 0 .../3dgraphics/glmeshitem.rst | 0 .../3dgraphics/glscatterplotitem.rst | 0 .../3dgraphics/glsurfaceplotitem.rst | 0 .../3dgraphics/gltextitem.rst | 0 .../3dgraphics/glviewwidget.rst | 0 .../3dgraphics/glvolumeitem.rst | 0 .../3dgraphics/index.rst | 0 .../3dgraphics/meshdata.rst | 0 .../config_options.rst | 0 doc/source/api_reference/index.rst | 4 +- doc/source/conf.py | 31 +++- doc/source/developer_guide/index.rst | 8 + .../{overview => }/3dgraphics.rst | 2 +- doc/source/getting_started/index.rst | 16 ++ doc/source/images/index_api.svg | 97 ++++++++++ doc/source/images/index_contribute.svg | 76 ++++++++ doc/source/images/index_getting_started.svg | 66 +++++++ doc/source/images/index_user_guide.svg | 67 +++++++ doc/source/index.rst | 113 ++++++++---- doc/source/user_guide/index.rst | 12 ++ 27 files changed, 612 insertions(+), 52 deletions(-) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glaxisitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glgraphicsitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glgraphitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glgriditem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glimageitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/gllineplotitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glmeshitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glscatterplotitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glsurfaceplotitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/gltextitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glviewwidget.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/glvolumeitem.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/index.rst (100%) rename doc/source/{getting_started/overview => api_reference}/3dgraphics/meshdata.rst (100%) rename doc/source/{user_guide => api_reference}/config_options.rst (100%) create mode 100644 doc/source/developer_guide/index.rst rename doc/source/getting_started/{overview => }/3dgraphics.rst (88%) create mode 100644 doc/source/getting_started/index.rst create mode 100644 doc/source/images/index_api.svg create mode 100644 doc/source/images/index_contribute.svg create mode 100644 doc/source/images/index_getting_started.svg create mode 100644 doc/source/images/index_user_guide.svg create mode 100644 doc/source/user_guide/index.rst diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css index 6046173833..6d9f25e891 100644 --- a/doc/source/_static/custom.css +++ b/doc/source/_static/custom.css @@ -1,18 +1,164 @@ -/* Customizations to the theme */ +/* Contents/Template taken from the SciPy project */ +/* https://github.com/scipy/scipy/blob/9ae8fd0f4341d7d8785777d460cca4f7b6a93edd/doc/source/_static/scipy.css */ +/* PyQtGraph specifics */ -/* override table width restrictions */ -/* https://github.com/readthedocs/sphinx_rtd_theme/issues/117 */ -@media screen and (min-width: 768px) { - .wy-table-responsive table td, .wy-table-responsive table th { - white-space: normal !important; - } +/* Remove parenthesis around module using fictive font and add them back. + This is needed for better wrapping in the sidebar. */ +.bd-sidebar .nav li > a > code { + white-space: nowrap; +} + +.bd-sidebar .nav li > a > code:before { + content:'('; +} + +.bd-sidebar .nav li > a > code:after { + content:')'; +} + +.bd-sidebar .nav li > a { + font-family: "no-parens", sans-serif; +} + +/* Retrieved from https://codepen.io/jonneal/pen/bXLEdB (MIT) + It replaces (, ) with a zero-width font. This version is lighter than + the original font from Adobe. +*/ +@font-face { + font-family: no-parens; + src: url("data:application/x-font-woff;base64,d09GRk9UVE8AABuoAAoAAAAASrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAANJAAADlwAABk8NN4INERTSUcAABugAAAACAAAAAgAAAABT1MvMgAAAVAAAABRAAAAYABfsZtjbWFwAAAEQAAACM0AABnoJENu0WhlYWQAAAD0AAAAMwAAADYFl9tDaGhlYQAAASgAAAAeAAAAJAdaA+9obXR4AAAbgAAAAB8AABAGA+gAfG1heHAAAAFIAAAABgAAAAYIAVAAbmFtZQAAAaQAAAKbAAAF6yBNB5Jwb3N0AAANEAAAABMAAAAg/7gAMnjaY2BkYGBg5G6tPXx8azy/zVcGZuYXQBGGiz6un+F0zf8O5hzmAiCXmYEJJAoAkoQNcAB42mNgZGBgLvjfASRfMNQw1DDnMABFUAATAHAaBFEAAAAAUAAIAQAAeNpjYGZ+wTiBgZWBgamLKYKBgcEbQjPGMRgx3GFAAt//r/v/+/7///wPGOxBfEcXJ38GBwaG//+ZC/53MDAwFzBUJOgz/kfSosDAAAAMpBWaAAAAeNqdU9tu00AQPU6TcqmoRIV46YvFE5Vgm7ZOVDVPSS8iIkqquBTxhJzEuSiOHWwnwH8g/oHfgW9A/AZnx5smQZWg2MrumZ0z47MzEwCP8R0W9GNhS1b95HCPVoY3sIsdg/MrnAJO8NLgTTzEgEwr/4DWF3ww2MJTq2BwDtvWrsEbKFt7BudXOAWk1nuDN/HE+mHwfTjWL4O34OQWeR7lvuZaBm/Dyf+s9qKOb9cCLxy3/cEs8OIDVXRKlepZrVURp/hot2rn136cjKLQziiXrgHDKO1G4Vxb6viwMvHGfpT2VTDqHKqSKh85xfIyE04RYYrPiDFiCYZIYeMbf4co4gBHeHGDS0RV9MjvwCd2GZWQ72PC3UYdIbr0xsynV098PXqeS96U5yfY5/tRXkXGIpuSyAl9e8SrX6khIC/EGG3aA8zEjqlHUZVDVRXyz8hrCVpELuMyf4sn57imJ6baEVkhs69mueSN1k+GZKWiLMT8xqdwzIpUqNZjdl84fZ4GzNqhRzFWoczaOWSXb9X0P3X89xqmzDjlyT6uGDWSrBdyi1S+F1FvymhdR60gY2j9XdohraxvM+KeVMwmf2jU1tHg3pIvhGuZG2sZ9OTcVm/9s++krCd7KjPaoarFXGU5PVmfsaauVM8l1nNTFa2u6HhLdIVXVP2Gu7arnKc21ybtOifDlTu1uZ5yb3Ji6uLROPNdyPw38Y77a3o0R+f2qSqrTizWJ1ZGq09EeySnI/ZlKhXWypXc1Zcb3r2uNmsUrfUkkZguWX1h2mbO9L/F45r1YioKJ1LLRUcSU7+e6f9E7qInbukfEM0lNuSpzmpzviLmjmVGMk26c5miv3VV/THJCRXrzk55ltCrtQXc9R0H9OvKN34D31P2fwB42i3YLfAsS2GG8X9Pf3dP97QjqOBAUAUOHDhwxAUHLnHgwIEDBw4cOHDgEgeOuIsjLnHgAMU1tw7PnvNs1fT7zlfV7q9rd2bn7e0tv729RZYvsySWb76Ft9fr82wN77fHt/F+e3m73+8J74/8zPsxvdbqu3fvXjsYg2e/P/LTP33f367PfMj67sPZjXjsh/iU/V+If7W/Tvms/XPEF+xfJL5kf73lr9i/SnzN/nXiG/Z/I/7d/k3iW/ZvE/9h/0/iO/bvEt+zf5/4gf2HxI/sPyZ+Yn99xJ/Zf078wv5L4lf2XxO/sf+W+C/7fxO/s/+e+IP9f4iP7H8k/mT/f+LP9r8Qf7X/jfiH/WPik48+9E/Y8e4Tpvjv72cl6B/wD/oH/IP+Af+gf8A/6B/wD/oH/IP+Af+gf8A/6B/wD/oH/IP+Af+gf8A/6B/wD/oH/IP+Af+gf8A/6B/wD/oH/IP+Af+gf8A/6B/wD/oH/IP+Af+gf8A/6B/wD/oH/IP+4X8Z/8/OXATnIjAXwbkIkAfnIjAX4eVPv15fA/0v/C/9L/wv/S/8L/1fX5lL/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/wv/S/8L/0v/C/9L/9cvXNQ/4h/1j/hH/SP+Uf+If9Q/4h/1j/hH/SP+Uf+If9Q/4h/1j/hH/SP+Uf+If9Q/4h/1j/hH/SP+Uf+If9Q/4h/1j/hH/SP+Uf+If9Q/4h/1j/hH/SP+Uf+If9Q/4h/1j/hH/SP+Uf+If9Q/4h/1j/hH/SP+Uf/XlSXpn/BP+if8k/4J/6R/wj/pn/BP+if8k/4J/6R/wj/pn/BP+if8k/4J/6R/wj/pn/BP+if8k/4J/6R/wj/pn/BP+if8k/4J/6R/wj/pn/BP+if8k/4J/6R/wj/pn/BP+if8k/4J/6R/wj/pn/BP+if8k/4J/6T/6yqf9c/4Z/0z/ln/jH/WP+Of9c/4Z/0z/ln/jH/WP+Of9c/4Z/0z/ln/jH/WP+Of9c/4Z/0z/ln/jH/WP+Of9c/4Z/0z/ln/jH/WP+Of9c/4Z/0z/ln/jH/WP+Of9c/4Z/0z/ln/jH/WP+Of9c/4Z/0z/ln/jH/WvzAW/Qv+Rf+Cf9G/4F/0L/gX/Qv+Rf+Cf9G/4F/0L/gX/Qv+Rf+Cf9G/4F/0L/gX/Qv+Rf+Cf9G/4F/0L/gX/Qv+Rf+Cf9G/4F/0L/gX/Qv+Rf+Cf9G/4F/0L/gX/Qv+Rf+Cf9G/4F/0L/gX/Qv+Rf+Cf9G/4F/0r6/bT/0r/lX/in/Vv+Jf9a/4V/0r/lX/in/Vv+Jf9a/4V/0r/lX/in/Vv+Jf9a/4V/0r/lX/in/Vv+Jf9a/4V/0r/lX/in/Vv+Jf9a/4V/0r/lX/in/Vv+Jf9a/4V/0r/lX/in/Vv+Jf9a/4V/0r/lX/in/Vv378uuX/4P+65W/6N1aa/g3/pn/Dv+nf8G/6N/yb/g3/pn/Dv+nf8G/6N/yb/g3/pn/Dv+nf8G/6N/yb/g3/pn/Dv+nf8G/6N/yb/g3/pn/Dv+nf8G/6N/yb/g3/pn/Dv+nf8G/6N/yb/g3/pn/Dv+nf8G/6N/yb/g3/pn/Dv+nfGbv+Hf+uf8e/69/x7/p3/Lv+Hf+uf8e/69/x7/p3/Lv+Hf+uf8e/69/x7/p3/Lv+Hf+uf8e/69/x7/p3/Lv+Hf+uf8e/69/x7/p3/Lv+Hf+uf8e/69/x7/p3/Lv+Hf+uf8e/69/x7/p3/Lv+Hf+uf8e/69/x7/q//kEP/Qf+Q/+B/9B/4D/0H/gP/Qf+Q/+B/9B/4D/0H/gP/Qf+Q/+B/9B/4D/0H/gP/Qf+Q/+B/9B/4D/0H/gP/Qf+Q/+B/9B/4D/0H/gP/Qf+Q/+B/9B/4D/0H/gP/Qf+Q/+B/9B/4D/0H/gP/Qf+Q/+B/9B/4D/0n4xT/4n/1H/iP/Wf+E/9J/5T/4n/1H/iP/Wf+E/9J/5T/4n/1H/iP/Wf+E/9J/5T/4n/1H/iP/Wf+E/9J/5T/4n/1H/iP/Wf+E/9J/5T/4n/1H/iP/Wf+E/9J/5T/4n/1H/iP/Wf+E/9J/5T/4n/1H/iP/Wf+E/9X8+Dbv1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9b/xv/W/8b/1v/G/9F+PSf+G/9F/4L/0X/kv/hf/Sf+G/9F/4L/0X/kv/hf/Sf+G/9F/4L/0X/kv/hf/Sf+G/9F/4L/0X/kv/hf/Sf+G/9F/4L/0X/kv/hf/Sf+G/9F/4L/0X/kv/hf/Sf+G/9F/4L/0X/kv/hf/Sf+G/9F/4L/0X/kv/zbj13/hv/Tf+W/+N/9Z/47/13/hv/Tf+W/+N/9Z/47/13/hv/Tf+W/+N/9Z/47/13/hv/Tf+W/+N/9Z/47/13/hv/Tf+W/+N/9Z/47/13/hv/Tf+W/+N/9Z/47/13/hv/Tf+W/+N/9Z/47/13/hv/Tf+W/+N/9b/eT1y1v/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/B/9H/wf/R/8H/0f/5+PWY/4P/6zH/0f/gf/Q/7Dj6H/yP/gf/o//B/+h/8D/6H/yP/gf/o//B/+h/8D/6H/yP/gf/o//B/+h/8D/6H/yP/gf/o//B/+h/8D/6H/yP/gf/o//B/+h/8D/6H/yP/gf/o//B/+h/8D/6H/yP/gf/o//B/+h/8D/6H/zPB/9/AsqUaXgAAAB42mNgZgCD/1sZjBiwAAAswgHqAHja7ZhVc5BNkIWn/QWCEzRAcHd3d3eX4J4Awd0luLu7e3B3d3d3h4RgC99e7I9YnoupOjXdXaempqamGxyjA4AoxVoENmtZvENAp/Z/ZdbwROF+IT5JwhNDeBIM+e4T4SJYkiTkJj5J/TzwSR5WK3pYs5hh9X1S+SVI6pPSCYBGqx0Q9F+Zci1adgpuG9yrRGBQry5tW7cJ9s+eNVuOjH/XXP7/RfjX6NU1uGXHrv7lOjUP7BIU2CUguGUL/7RtgoOD8mfJ0qNHj8wBf8MyNw/smCVd5v9N+c/c/9nMlD1rznzO/XFvv8mBc84DD/5IV8FVdJVcZVfFVXXVXHVXw9V0tVxtV8fVdfVcfdfANXSNXGPXxDV1Aa6Za+5auJaulWvt2ri2rp1r7zq4jq6TC3RBrrPr4rq6YNfNdXc9XE/Xy/V2fVxf18/1dwPcQDfIDXZD3FA3zA13I9xIN8qNdiFujBvrxrnxboKb6Ca5yW6Km+qmueluhpvpZrnZbo6b6+a5+W6BW+gWucVuiVvqlrnlboVb6Va51W6NW+vWufVug9voNrnNbovb6ra5ULfd7XA73S632+1xe90+t98dcAfdIXfYHXFH3TF33J1wJ90pd9qdcWfdOXfeXXAX3SV32V1xV901d93dcDfdLXfb3XF33T133z1wD90j99g9cU/dM/fcvXAv3Sv32r1xb9079959cB/dJ/fZfXFfXZgLd99chPvufrif7pf7DX+vCgIBg4CC/Tn/SBAZooAPRIVoEB1iQEyIBbEhDvhCXIgH8SEBJIRE4AeJIQkkBX9IBskhBaSEVJAa0kBaSAfpIQNkhEyQGbJAVsgG2SEH5IRckBvyQF7IB/mhABSEQlAYikBRKAbFoQSUhFJQGspAWSgH5aECVIRKUBmqQFWoBtWhBtSEWlAb6kBdqAf1oQE0hEbQGJpAUwiAZtAcWkBLaAWtoQ20hXbQHjpAR+gEgRAEnaELdIVg6AbdoQf0hF7QG/pAX+gH/WEADIRBMBiGwFAYBsNhBIyEUTAaQmAMjIVxMB4mwESYBJNhCkyFaTAdZsBMmAWzYQ7MhXkwHxbAQlgEi2EJLIVlsBxWwEpYBathDayFdbAeNsBG2ASbYQtshW0QCtthB+yEXbAb9sBe2Af74QAchENwGI7AUTgGx+EEnIRTcBrOwFk4B+fhAlyES3AZrsBVuAbX4QbchFtwG+7AXbgH9+EBPIRH8BiewFN4Bs/hBbyEV/Aa3sBbeAfv4QN8hE/wGb7AVwiDcPgGEfAdfsBP+AW/0SEgIiGjoKKhh5EwMkZBH4yK0TA6xsCYGAtjYxz0xbgYD+NjAkyIidAPE2MSTIr+mAyTYwpMiakwNabBtJgO02MGzIiZMDNmwayYDbNjDsyJuTA35sG8mA/zYwEsiIWwMBbBolgMi2MJLImlsDSWwbJYDstjBayIlbAyVsGqWA2rYw2sibWwNtbBulgP62MDbIiNsDE2waYYgM2wObbAltgKW2MbbIvtsD12wI7YCQMxCDtjF+yKwdgNu2MP7Im9sDf2wb7YD/vjAByIg3AwDsGhOAyH4wgciaNwNIbgGByL43A8TsCJOAkn4xScitNwOs7AmTgLZ+McnIvzcD4uwIW4CBfjElyKy3A5rsCVuApX4xpci+twPW7AjbgJN+MW3IrbMBS34w7cibtwN+7BvbgP9+MBPIiH8DAewaN4DI/jCTyJp/A0nsGzeA7P4wW8iJfwMl7Bq3gNr+MNvIm38Dbewbt4D+/jA3yIj/AxPsGn+Ayf4wt8ia/wNb7Bt/gO3+MH/Iif8DN+wa8YhuH4DSPwO/7An/gL/zy7BIRExCSkZORRJIpMUciHolI0ik4xKCbFotgUh3wpLsWj+JSAElIi8qPElISSkj8lo+SUglJSKkpNaSgtpaP0lIEyUibKTFkoK2Wj7JSDclIuyk15KC/lo/xUgApSISpMRagoFaPiVIJKUikqTWWoLJWj8lSBKlIlqkxVqCpVo+pUg2pSLapNdagu1aP61IAaUiNqTE2oKQVQM2pOLagltaLW1IbaUjtqTx2oI3WiQAqiztSFulIwdaPu1IN6Ui/qTX2oL/Wj/jSABtIgGkxDaCgNo+E0gkbSKBpNITSGxtI4Gk8TaCJNosk0habSNJpOM2gmzaLZNIfm0jyaTwtoIS2ixbSEltIyWk4raCWtotW0htbSOlpPG2gjbaLNtIW20jYKpe20g3bSLtpNe2gv7aP9dIAO0iE6TEfoKB2j43SCTtIpOk1n6Cydo/N0gS7SJbpMV+gqXaPrdINu0i26TXfoLt2j+/SAHtIjekxP6Ck9o+f0gl7SK3pNb+gtvaP39IE+0if6TF/oK4VROH2jCPpOP+gn/aLf7BgYmZhZWNnY40gcmaOwD0flaBydY3BMjsWxOQ77clyOx/E5ASfkROzHiTkJJ2V/TsbJOQWn5FScmtNwWk7H6TkDZ+RMnJmzcFbOxtk5B+fkXJyb83Bezsf5uQAX5EJcmItwUS7GxbkEl+RSXJrLcFkux+W5AlfkSlyZq3BVrsbVuQbX5Fpcm+twXa7H9bkBN+RG3JibcFMO4GbcnFtwS27FrbkNt+V23J47cEfuxIEcxJ25C3flYO7G3bkH9+Re3Jv7cF/ux/15AA/kQTyYh/BQHsbDeQSP5FE8mkN4DI/lcTyeJ/BEnsSTeQpP5Wk8nWfwTJ7Fs3kOz+V5PJ8X8EJexIt5CS/lZbycV/BKXsWreQ2v5XW8njfwRt7Em3kLb+VtHMrbeQfv5F28m/fwXt7H+/kAH+RDfJiP8FE+xsf5BJ/kU3yaz/BZPsfn+QJf5Et8ma/wVb7G1/kG3+RbfJvv8F2+x/f5AT/kR/yYn/BTfsbP+QW/5Ff8mt/wW37H7/kDf+RP/Jm/8FcO43D+xhH8nX/wT/7Fv+XPt09QSFhEVEw8iSSRJYr4SFSJJtElhsSUWBJb4oivxJV4El8SSEJJJH6SWJJIUvGXZJJcUkhKSSWpJY2klXSSXjJIRskkmSWLZJVskl1ySE7JJbklj+SVfJJfCkhBKSSFpYgUlWJSXEpISSklpaWMlJVyUl4qSEWpJJWlilSValJdakhNqSW1pY7UlXpSXxpIQ2kkjaWJNJUAaSbNpYW0lFbSWtpIW2kn7aWDdJROEihB0lm6SFcJlm7SXXpIT+klvaWP9JV+0l8GyEAZJINliAyVYTJcRshIGSWjJUTGyFgZJ+NlgkyUSTJZpshUmSbTZYbMlFkyW+bIXJkn82WBLJRFsliWyFJZJstlhayUVbJa1shaWSfrZYNslE2yWbbIVtkmobJddshO2SW7ZY/slX2yXw7IQTkkh+WIHJVjclxOyEk5JafljJyVc3JeLshFuSSX5YpclWtyXW7ITbklt+WO3JV7cl8eyEN5JI/liTyVZ/JcXshLeSWv5Y28lXfyXj7IR/kkn+WLfJUwCZdvEiHf5Yf8lF/yW52CopKyiqqaehpJI2sU9dGoGk2jawyNqbE0tsZRX42r8TS+JtCEmkj9NLEm0aTqr8k0uabQlJpKU2saTavpNL1m0IyaSTNrFs2q2TS75tCcmktzax7Nq/k0vxbQglpIC2sRLarFtLiW0JJaSktrGS2r5bS8VtCKWkkraxWtqtW0utbQmlpLa2sdrav1tL420IbaSBtrE22qAdpMm2sLbamttLW20bbaTttrB+2onTRQg7SzdtGuGqzdtLv20J7aS3trH+2r/bS/DtCBOkgH6xAdqsN0uI7QkTpKR2uIjtGxOk7H6wSdqJN0sk7RqTpNp+sMnamzdLbO0bk6T+frAl2oi3SxLtGlukyX6wpdqat0ta7RtbpO1+sG3aibdLNu0a26TUN1u+7QnbpLd+se3av7dL8e0IN6SA/rET2qx/S4ntCTekpP6xk9q+f0vF7Qi3pJL+sVvarX9Lre0Jt6S2/rHb2r9/S+PtCH+kgf6xN9qs/0ub7Ql/pKX+sbfavv9L1+0I/6ST/rF/2qYRqu3zRCv+sP/am/9Lc5A0MjYxNTM/MskkW2KOZjUS2aRbcYFtNiWWyLY74W1+JZfEtgCS2R+VliS2JJzd+SWXJLYSktlaW2NJbW0ll6y2AZLZNltiyW1bJZdsthOS2X5bY8ltfyWX4rYAWtkBW2IlbUillxK2ElrZSVtjJW1spZeatgFa2SVbYqVtWqWXWrYTWtltW2OlbX6ll9a2ANrZE1tibW1AKsmTW3FtbSWllra2NtrZ21tw7W0TpZoAVZZ+tiXS3Yull362E9rZf1tj7W1/pZfxtgA22QDbYhNtSG2XAbYSNtlI22EBtjY22cjbcJNtEm2WSbYlNtmk23GTbTZtlsm2NzbZ7NtwW20BbZYltiS22ZLbcVttJW2WpbY2ttna23DbbRNtlm22JbbZuF2nbbYTttl+22PbbX9tl+O2AH7ZAdtiN21I7ZcTthJ+2UnbYzdtbO2Xm7YBftkl22K3bVrtl1u2E37Zbdtjt21+7ZfXtgD+2RPbYn9tSe2XN7YS/tlb22N/bW3tl7+2Af7ZN9ti/21cIs3L5ZhH23H/bTftlv72/LjR557ImnnnmeF8mL7EXxfLyoXjQvuhfDi+nF8mJ7cTxfL64Xz4vvJfASeok8Py+xl8RL6vl7ybzkXgovpZfKS+2l8dJ66bz0XgYvo5fJy+xl8bJ62bzsXg4vp5fLy+3l8fJ6+bz8XgGvoFfIK+wV8Yp6xbziXgmvpFfKK+2V8cp65bzyXgX/7z6hESlDISxG6LeMoRQWI4J9f/X9NjSir/2s+yuN77eLFnbkRw5ZtsH3+5HwPBL+VZc18/150f6oHBLUyvfPbh758VWj/eMf//jHP/7xj/9//B1wRw5P6pN6ll+CTLG+jwvxk9IhuifynigRz3z/B+I69cx42u3BAQ0AAAgDoG/WNvBjGERgmg0AAADwwAGHXgFoAAAAAAEAAAAA"); + unicode-range: U+0028, U+0029; +} + +/* Colors from: + +Wong, B. Points of view: Color blindness. +Nat Methods 8, 441 (2011). https://doi.org/10.1038/nmeth.1618 +*/ + +/* If the active version has the name "dev", style it orange */ +#version_switcher_button[data-active-version-name*="dev"] { + background-color: #E69F00; + border-color: #E69F00; + color: white; +} + +/* green for `stable` */ +#version_switcher_button[data-active-version-name*="stable"] { + background-color: #009E73; + border-color: #009E73; + color: white; +} + +/* red for `old` */ +#version_switcher_button:not([data-active-version-name*="stable"]):not([data-active-version-name*="dev"]):not([data-active-version-name*="pull"]) { + background-color: #980F0F; + border-color: #980F0F; + color: white; +} + +/* Main index page overview cards */ + +.sd-card { + background: #fff; + border-radius: 0; + padding: 30px 10px 20px 10px; + margin: 10px 0px; +} + +.sd-card .sd-card-header { + text-align: center; +} + +.sd-card .sd-card-header .sd-card-text { + margin: 0px; +} + + +.sd-card .sd-card-img-top { + height: 52px; + width: 52px; + margin-left: auto; + margin-right: auto; +} + +.sd-card .sd-card-header { + border: none; + background-color:white; + color: #150458 !important; + font-size: var(--pst-font-size-h5); + font-weight: bold; + padding: 2.5rem 0rem 0.5rem 0rem; + border-bottom: none !important; +} + +.sd-card .sd-card-footer { + border: none; + background-color:white; + border-top: none !important; +} + +.sd-card .sd-card-footer .sd-card-text{ + max-width: 220px; + margin-left: auto; + margin-right: auto; +} + +.custom-button { + background-color:#DCDCDC; + border: none; + color: #484848; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 0.9rem; + border-radius: 0.5rem; + max-width: 120px; + padding: 0.5rem 0rem; +} + +.custom-button a { + color: #484848; +} + +.custom-button p { + margin-top: 0; + margin-bottom: 0rem; + color: #484848; +} + + +/* Dark theme tweaking + +Matplotlib images are in png and inverted while other output +types are assumed to be normal images. + +*/ +html[data-theme=dark] img[src*='.svg']:not(.only-dark) { + filter: brightness(0.8) invert(0.82) contrast(1.2); + background: none; +} + +html[data-theme=dark] .MathJax_SVG * { + fill: var(--pst-color-text-base); +} + +/* Main index page overview cards */ + +html[data-theme=dark] .sd-card { + background-color:var(--pst-color-background); + border: none +} + +html[data-theme=dark] .sd-shadow-sm { + box-shadow: 0 .1rem 0.5rem rgba(250, 250, 250, .2) !important +} - .wy-table-responsive { - overflow: visible !important; - max-width: 100%; - } +html[data-theme=dark] .sd-card .sd-card-header { + background-color:var(--pst-color-background); + color: #150458 !important; } -.wy-side-nav-search, .wy-nav-top { - background: #444466; +html[data-theme=dark] .sd-card .sd-card-footer { + background-color:var(--pst-color-background); } diff --git a/doc/source/getting_started/overview/3dgraphics/glaxisitem.rst b/doc/source/api_reference/3dgraphics/glaxisitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glaxisitem.rst rename to doc/source/api_reference/3dgraphics/glaxisitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glgraphicsitem.rst b/doc/source/api_reference/3dgraphics/glgraphicsitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glgraphicsitem.rst rename to doc/source/api_reference/3dgraphics/glgraphicsitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glgraphitem.rst b/doc/source/api_reference/3dgraphics/glgraphitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glgraphitem.rst rename to doc/source/api_reference/3dgraphics/glgraphitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glgriditem.rst b/doc/source/api_reference/3dgraphics/glgriditem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glgriditem.rst rename to doc/source/api_reference/3dgraphics/glgriditem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glimageitem.rst b/doc/source/api_reference/3dgraphics/glimageitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glimageitem.rst rename to doc/source/api_reference/3dgraphics/glimageitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/gllineplotitem.rst b/doc/source/api_reference/3dgraphics/gllineplotitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/gllineplotitem.rst rename to doc/source/api_reference/3dgraphics/gllineplotitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glmeshitem.rst b/doc/source/api_reference/3dgraphics/glmeshitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glmeshitem.rst rename to doc/source/api_reference/3dgraphics/glmeshitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glscatterplotitem.rst b/doc/source/api_reference/3dgraphics/glscatterplotitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glscatterplotitem.rst rename to doc/source/api_reference/3dgraphics/glscatterplotitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glsurfaceplotitem.rst b/doc/source/api_reference/3dgraphics/glsurfaceplotitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glsurfaceplotitem.rst rename to doc/source/api_reference/3dgraphics/glsurfaceplotitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/gltextitem.rst b/doc/source/api_reference/3dgraphics/gltextitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/gltextitem.rst rename to doc/source/api_reference/3dgraphics/gltextitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glviewwidget.rst b/doc/source/api_reference/3dgraphics/glviewwidget.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glviewwidget.rst rename to doc/source/api_reference/3dgraphics/glviewwidget.rst diff --git a/doc/source/getting_started/overview/3dgraphics/glvolumeitem.rst b/doc/source/api_reference/3dgraphics/glvolumeitem.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/glvolumeitem.rst rename to doc/source/api_reference/3dgraphics/glvolumeitem.rst diff --git a/doc/source/getting_started/overview/3dgraphics/index.rst b/doc/source/api_reference/3dgraphics/index.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/index.rst rename to doc/source/api_reference/3dgraphics/index.rst diff --git a/doc/source/getting_started/overview/3dgraphics/meshdata.rst b/doc/source/api_reference/3dgraphics/meshdata.rst similarity index 100% rename from doc/source/getting_started/overview/3dgraphics/meshdata.rst rename to doc/source/api_reference/3dgraphics/meshdata.rst diff --git a/doc/source/user_guide/config_options.rst b/doc/source/api_reference/config_options.rst similarity index 100% rename from doc/source/user_guide/config_options.rst rename to doc/source/api_reference/config_options.rst diff --git a/doc/source/api_reference/index.rst b/doc/source/api_reference/index.rst index 2d6411c133..7a91d2762e 100644 --- a/doc/source/api_reference/index.rst +++ b/doc/source/api_reference/index.rst @@ -1,3 +1,5 @@ +.. _pyqtgraph_api_ref: + API Reference ============= @@ -12,7 +14,7 @@ Contents: widgets/index 3dgraphics/index colormap - parametertree/apiref + parametertree/index dockarea graphicsscene/index flowchart/index diff --git a/doc/source/conf.py b/doc/source/conf.py index 8e95ab224e..a5092afa66 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -15,6 +15,8 @@ import time from datetime import datetime +from sphinx.application import Sphinx + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -137,7 +139,11 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = dict( + github_url="https://github.com/pyqtgraph/pyqtgraph", + navbar_end=["theme-switcher", "navbar-icon-links"], + twitter_url="https://twitter.com/pyqtgraph", +) # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -164,8 +170,18 @@ html_static_path = ['_static'] # add the theme customizations -def setup(app): - app.add_css_file("custom.css") +# def setup(app): +# app.add_css_file("custom.css") + +html_css_files = [ + "custom.css", +] + +# Redirects for pages that were moved to new locations +# TODO: fill out the remaining remappings +rediraffe_redirects = { + "functions.rst": "api_reference/functions.rst", +} # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -259,3 +275,12 @@ def setup(app): ('index', 'pyqtgraph', 'pyqtgraph Documentation', ['Luke Campagnola'], 1) ] + + +def html_page_context(app, pagename, templatename, context, doctree): + # Disable edit button for docstring generated pages + if "generated" in pagename: + context["theme_use_edit_page_button"] = False + +def setup(app: Sphinx): + app.connect("html-page-context", html_page_context) diff --git a/doc/source/developer_guide/index.rst b/doc/source/developer_guide/index.rst new file mode 100644 index 0000000000..ca896093ff --- /dev/null +++ b/doc/source/developer_guide/index.rst @@ -0,0 +1,8 @@ +.. _developer_guide_ref: + +Developer Guide +=============== + +.. toctree:: + + internals diff --git a/doc/source/getting_started/overview/3dgraphics.rst b/doc/source/getting_started/3dgraphics.rst similarity index 88% rename from doc/source/getting_started/overview/3dgraphics.rst rename to doc/source/getting_started/3dgraphics.rst index eed8eb69db..819b8cde1b 100644 --- a/doc/source/getting_started/overview/3dgraphics.rst +++ b/doc/source/getting_started/3dgraphics.rst @@ -11,7 +11,7 @@ Current capabilities include: * Volumetric rendering item * Grid/axis items -See the :doc:`API Reference ` and the Volumetric (GLVolumeItem.py) and Isosurface (GLMeshItem.py) examples for more information. +See the :doc:`API Reference <../api_reference/3dgraphics/index>` and the Volumetric (GLVolumeItem.py) and Isosurface (GLMeshItem.py) examples for more information. Basic usage example:: diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst new file mode 100644 index 0000000000..d26ea5fbe5 --- /dev/null +++ b/doc/source/getting_started/index.rst @@ -0,0 +1,16 @@ +.. _getting_started_ref: + +Getting Started +=============== + +.. toctree:: + :maxdepth: 1 + + introduction + installation + how_to_use + plotting + images + 3dgraphics + prototyping + qtcrashcourse diff --git a/doc/source/images/index_api.svg b/doc/source/images/index_api.svg new file mode 100644 index 0000000000..87013d24ce --- /dev/null +++ b/doc/source/images/index_api.svg @@ -0,0 +1,97 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/doc/source/images/index_contribute.svg b/doc/source/images/index_contribute.svg new file mode 100644 index 0000000000..399f1d7630 --- /dev/null +++ b/doc/source/images/index_contribute.svg @@ -0,0 +1,76 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/doc/source/images/index_getting_started.svg b/doc/source/images/index_getting_started.svg new file mode 100644 index 0000000000..d1c7b08a2a --- /dev/null +++ b/doc/source/images/index_getting_started.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/doc/source/images/index_user_guide.svg b/doc/source/images/index_user_guide.svg new file mode 100644 index 0000000000..bff2482423 --- /dev/null +++ b/doc/source/images/index_user_guide.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/doc/source/index.rst b/doc/source/index.rst index c24b55f41a..5316849cdd 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,37 +1,82 @@ -.. pyqtgraph documentation master file, created by - sphinx-quickstart on Fri Nov 18 19:33:12 2011. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +PyQtGraph +========= -Welcome to the documentation for pyqtgraph -========================================== +.. toctree:: + :maxdepth: 1 -Contents: + getting_started/index + user_guide/index + developer_guide/index + api_reference/index -.. toctree:: - :maxdepth: 2 - - introduction - mouse_interaction - how_to_use - installation - qtcrashcourse - plotting - images - 3dgraphics - style - region_of_interest - exporting - prototyping - parametertree/index - flowchart/index - internals - apireference - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +.. grid:: 2 + + .. grid-item-card:: + :img-top: /images/index_getting_started.svg + + Getting started + ^^^^^^^^^^^^^^^ + + New to PyQtGraph? Check out the getting started guides. Content here includes introductions to + PyQtGraph concepts and capabilities, as well as basic tutorials. + + +++ + + .. button-ref:: getting_started_ref + :expand: + :color: success + :click-parent: + + To the getting started guides + + .. grid-item-card:: + :img-top: /images/index_user_guide.svg + + User guide + ^^^^^^^^^^ + + The user guide provides in-depth information on the key concepts of PyQtGraph. More complicated examples are presented and greater detail of the capabilities of the library are highlighted. Performance related considerations are detailed here. + + +++ + + .. button-ref:: user_guide_ref + :expand: + :color: success + :click-parent: + + To the user guide + + .. grid-item-card:: + :img-top: /images/index_api.svg + + API Reference + ^^^^^^^^^^^^^ + + The reference guide contains a detailed description of the PyQtGraph API + + +++ + + .. button-ref:: pyqtgraph_api_ref + :expand: + :color: success + :click-parent: + + To the reference guide + + + .. grid-item-card:: + :img-top: /images/index_contribute.svg + + Developer guide + ^^^^^^^^^^^^^^^ + + Guide to contributing to PyQtGraph. Provides guidance on how to contribute to PyQtGraph and factors to be careful of when trying to remain compatible with all the Qt bindings that are supported. + + +++ + + .. button-ref:: developer_guide_ref + :expand: + :color: success + :click-parent: + + To the development guide diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst new file mode 100644 index 0000000000..196b09e997 --- /dev/null +++ b/doc/source/user_guide/index.rst @@ -0,0 +1,12 @@ +.. _user_guide_ref: + +User Guide +========== + +.. toctree:: + :maxdepth: 2 + + exporting + mouse_interaction + region_of_interest + style From 14887f33423a409627f4197ac83d9ad492ae2375 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 2 Oct 2022 12:03:01 -0700 Subject: [PATCH 077/306] Handle redirects of old existing pages Minor fixups of tables of contents, references --- doc/requirements.txt | 1 + doc/source/api_reference/flowchart/index.rst | 2 + doc/source/api_reference/graphicsItems/make | 39 ------ doc/source/api_reference/widgets/make | 31 ----- doc/source/conf.py | 127 ++++++++++++++++++- doc/source/developer_guide/index.rst | 3 + doc/source/getting_started/index.rst | 2 + doc/source/getting_started/introduction.rst | 3 - doc/source/getting_started/prototyping.rst | 4 +- doc/source/index.rst | 5 +- doc/source/user_guide/index.rst | 5 +- 11 files changed, 140 insertions(+), 82 deletions(-) delete mode 100644 doc/source/api_reference/graphicsItems/make delete mode 100644 doc/source/api_reference/widgets/make diff --git a/doc/requirements.txt b/doc/requirements.txt index d6ebca69b4..8b25313547 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,5 +3,6 @@ PyQt6==6.3.1 pydata-sphinx-theme sphinx-qt-documentation sphinx-design +sphinxext-rediraffe numpy pyopengl diff --git a/doc/source/api_reference/flowchart/index.rst b/doc/source/api_reference/flowchart/index.rst index 243a055cea..8f95ef8109 100644 --- a/doc/source/api_reference/flowchart/index.rst +++ b/doc/source/api_reference/flowchart/index.rst @@ -1,3 +1,5 @@ +.. _flowchart: + Visual Programming with Flowcharts ================================== diff --git a/doc/source/api_reference/graphicsItems/make b/doc/source/api_reference/graphicsItems/make deleted file mode 100644 index 14e62118e4..0000000000 --- a/doc/source/api_reference/graphicsItems/make +++ /dev/null @@ -1,39 +0,0 @@ -files = """ArrowItem -AxisItem -ButtonItem -CurvePoint -DateAxisItem -GradientEditorItem -GradientLegend -GraphicsLayout -GraphicsObject -GraphicsWidget -GridItem -HistogramLUTItem -ImageItem -PColorMeshItem -InfiniteLine -LabelItem -LinearRegionItem -PlotCurveItem -PlotDataItem -ROI -ScaleBar -ScatterPlotItem -UIGraphicsItem -ViewBox -VTickGroup""".split('\n') - -for f in files: - print(f) - fh = open(f.lower()+'.rst', 'w') - fh.write( -"""%s -%s - -.. autoclass:: pyqtgraph.%s - :members: - - .. automethod:: pyqtgraph.%s.__init__ - -""" % (f, '='*len(f), f, f)) diff --git a/doc/source/api_reference/widgets/make b/doc/source/api_reference/widgets/make deleted file mode 100644 index 1c7d379e7f..0000000000 --- a/doc/source/api_reference/widgets/make +++ /dev/null @@ -1,31 +0,0 @@ -files = """CheckTable -ColorButton -DataTreeWidget -FileDialog -GradientWidget -GraphicsLayoutWidget -GraphicsView -HistogramLUTWidget -JoystickButton -MultiPlotWidget -PlotWidget -ProgressDialog -RawImageWidget -SpinBox -TableWidget -TreeWidget -VerticalLabel""".split('\n') - -for f in files: - print(f) - fh = open(f.lower()+'.rst', 'w') - fh.write( -"""%s -%s - -.. autoclass:: pyqtgraph.%s - :members: - - .. automethod:: pyqtgraph.%s.__init__ - -""" % (f, '='*len(f), f, f)) diff --git a/doc/source/conf.py b/doc/source/conf.py index a5092afa66..f7b702ab6b 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -38,7 +38,8 @@ "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx_qt_documentation", - "sphinx_design" + "sphinx_design", + "sphinxext.rediraffe" ] # Add any paths that contain templates here, relative to this directory. @@ -170,17 +171,133 @@ html_static_path = ['_static'] # add the theme customizations -# def setup(app): -# app.add_css_file("custom.css") - html_css_files = [ "custom.css", ] # Redirects for pages that were moved to new locations -# TODO: fill out the remaining remappings rediraffe_redirects = { + "3dgraphics/glaxisitem.rst": "api_reference/3dgraphics/glaxisitem.rst", + "3dgraphics/glgraphicsitem.rst": "api_reference/3dgraphics/glgraphicsitem.rst", + "3dgraphics/glgraphitem.rst": "api_reference/3dgraphics/glgraphitem.rst", + "3dgraphics/glgriditem.rst": "api_reference/3dgraphics/glgriditem.rst", + "3dgraphics/glimageitem.rst": "api_reference/3dgraphics/glimageitem.rst", + "3dgraphics/gllineplotitem.rst": "api_reference/3dgraphics/gllineplotitem.rst", + "3dgraphics/glmeshitem.rst": "api_reference/3dgraphics/glmeshitem.rst", + "3dgraphics/glscatterplotitem.rst": "api_reference/3dgraphics/glscatterplotitem.rst", + "3dgraphics/glsurfaceplotitem.rst": "api_reference/3dgraphics/glsurfaceplotitem.rst", + "3dgraphics/gltextitem.rst": "api_reference/3dgraphics/gltextitem.rst", + "3dgraphics/glviewwidget.rst": "api_reference/3dgraphics/glviewwidget.rst", + "3dgraphics/glvolumeitem.rst": "api_reference/3dgraphics/glvolumeitem.rst", + "3dgraphics/index.rst": "api_reference/3dgraphics/index.rst", + "3dgraphics/meshdata.rst": "api_reference/3dgraphics/meshdata.rst", + "colormap.rst": "api_reference/colormap.rst", + "config_options.rst": "api_reference/config_options.rst", + "dockarea.rst": "api_reference/dockarea.rst", + "flowchart/flowchart.rst": "api_reference/flowchart/flowchart.rst", + "flowchart/index.rst": "api_reference/flowchart/index.rst", + "flowchart/node.rst": "api_reference/flowchart/node.rst", + "flowchart/terminal.rst": "api_reference/flowchart/terminal.rst", "functions.rst": "api_reference/functions.rst", + "graphicsItems/arrowitem.rst": "api_reference/graphicsItems/arrowitem.rst", + "graphicsItems/axisitem.rst": "api_reference/graphicsItems/axisitem.rst", + "graphicsItems/bargraphitem.rst": "api_reference/graphicsItems/bargraphitem.rst", + "graphicsItems/buttonitem.rst": "api_reference/graphicsItems/buttonitem.rst", + "graphicsItems/colorbaritem.rst": "api_reference/graphicsItems/colorbaritem.rst", + "graphicsItems/curvearrow.rst": "api_reference/graphicsItems/curvearrow.rst", + "graphicsItems/curvepoint.rst": "api_reference/graphicsItems/curvepoint.rst", + "graphicsItems/dateaxisitem.rst": "api_reference/graphicsItems/dateaxisitem.rst", + "graphicsItems/errorbaritem.rst": "api_reference/graphicsItems/errorbaritem.rst", + "graphicsItems/fillbetweenitem.rst": "api_reference/graphicsItems/fillbetweenitem.rst", + "graphicsItems/gradienteditoritem.rst": "api_reference/graphicsItems/gradienteditoritem.rst", + "graphicsItems/gradientlegend.rst": "api_reference/graphicsItems/gradientlegend.rst", + "graphicsItems/graphicsitem.rst": "api_reference/graphicsItems/graphicsitem.rst", + "graphicsItems/graphicslayout.rst": "api_reference/graphicsItems/graphicslayout.rst", + "graphicsItems/graphicsobject.rst": "api_reference/graphicsItems/graphicsobject.rst", + "graphicsItems/graphicswidget.rst": "api_reference/graphicsItems/graphicswidget.rst", + "graphicsItems/graphicswidgetanchor.rst": "api_reference/graphicsItems/graphicswidgetanchor.rst", + "graphicsItems/graphitem.rst": "api_reference/graphicsItems/graphitem.rst", + "graphicsItems/griditem.rst": "api_reference/graphicsItems/griditem.rst", + "graphicsItems/histogramlutitem.rst": "api_reference/graphicsItems/histogramlutitem.rst", + "graphicsItems/imageitem.rst": "api_reference/graphicsItems/imageitem.rst", + "graphicsItems/index.rst": "api_reference/graphicsItems/index.rst", + "graphicsItems/infiniteline.rst": "api_reference/graphicsItems/infiniteline.rst", + "graphicsItems/isocurveitem.rst": "api_reference/graphicsItems/isocurveitem.rst", + "graphicsItems/labelitem.rst": "api_reference/graphicsItems/labelitem.rst", + "graphicsItems/legenditem.rst": "api_reference/graphicsItems/legenditem.rst", + "graphicsItems/linearregionitem.rst": "api_reference/graphicsItems/linearregionitem.rst", + "graphicsItems/multiplotitem.rst": "api_reference/graphicsItems/multiplotitem.rst", + "graphicsItems/pcolormeshitem.rst": "api_reference/graphicsItems/pcolormeshitem.rst", + "graphicsItems/plotcurveitem.rst": "api_reference/graphicsItems/plotcurveitem.rst", + "graphicsItems/plotdataitem.rst": "api_reference/graphicsItems/plotdataitem.rst", + "graphicsItems/plotitem.rst": "api_reference/graphicsItems/plotitem.rst", + "graphicsItems/roi.rst": "api_reference/graphicsItems/roi.rst", + "graphicsItems/scalebar.rst": "api_reference/graphicsItems/scalebar.rst", + "graphicsItems/scatterplotitem.rst": "api_reference/graphicsItems/scatterplotitem.rst", + "graphicsItems/targetitem.rst": "api_reference/graphicsItems/targetitem.rst", + "graphicsItems/textitem.rst": "api_reference/graphicsItems/textitem.rst", + "graphicsItems/uigraphicsitem.rst": "api_reference/graphicsItems/uigraphicsitem.rst", + "graphicsItems/viewbox.rst": "api_reference/graphicsItems/viewbox.rst", + "graphicsItems/vtickgroup.rst": "api_reference/graphicsItems/vtickgroup.rst", + "graphicsscene/graphicsscene.rst": "api_reference/graphicsscene/graphicsscene.rst", + "graphicsscene/hoverevent.rst": "api_reference/graphicsscene/hoverevent.rst", + "graphicsscene/index.rst": "api_reference/graphicsscene/index.rst", + "graphicsscene/mouseclickevent.rst": "api_reference/graphicsscene/mouseclickevent.rst", + "graphicsscene/mousedragevent.rst": "api_reference/graphicsscene/mousedragevent.rst", + "apireference.rst": "api_reference/index.rst", + "parametertree/apiref.rst": "api_reference/parametertree/apiref.rst", + "parametertree/index.rst": "api_reference/parametertree/index.rst", + "parametertree/interactiveparameters.rst": "api_reference/parametertree/interactiveparameters.rst", + "parametertree/parameter.rst": "api_reference/parametertree/parameter.rst", + "parametertree/parameteritem.rst": "api_reference/parametertree/parameteritem.rst", + "parametertree/parametertree.rst": "api_reference/parametertree/parametertree.rst", + "parametertree/parametertypes.rst": "api_reference/parametertree/parametertypes.rst", + "point.rst": "api_reference/point.rst", + "transform3d.rst": "api_reference/transform3d.rst", + "uml_overview.rst": "api_reference/uml_overview.rst", + "widgets/busycursor.rst": "api_reference/widgets/busycursor.rst", + "widgets/checktable.rst": "api_reference/widgets/checktable.rst", + "widgets/colorbutton.rst": "api_reference/widgets/colorbutton.rst", + "widgets/colormapwidget.rst": "api_reference/widgets/colormapwidget.rst", + "widgets/combobox.rst": "api_reference/widgets/combobox.rst", + "widgets/consolewidget.rst": "api_reference/widgets/consolewidget.rst", + "widgets/datatreewidget.rst": "api_reference/widgets/datatreewidget.rst", + "widgets/feedbackbutton.rst": "api_reference/widgets/feedbackbutton.rst", + "widgets/filedialog.rst": "api_reference/widgets/filedialog.rst", + "widgets/gradientwidget.rst": "api_reference/widgets/gradientwidget.rst", + "widgets/graphicslayoutwidget.rst": "api_reference/widgets/graphicslayoutwidget.rst", + "widgets/graphicsview.rst": "api_reference/widgets/graphicsview.rst", + "widgets/histogramlutwidget.rst": "api_reference/widgets/histogramlutwidget.rst", + "widgets/imageview.rst": "api_reference/widgets/imageview.rst", + "widgets/index.rst": "api_reference/widgets/index.rst", + "widgets/joystickbutton.rst": "api_reference/widgets/joystickbutton.rst", + "widgets/layoutwidget.rst": "api_reference/widgets/layoutwidget.rst", + "widgets/matplotlibwidget.rst": "api_reference/widgets/matplotlibwidget.rst", + "widgets/multiplotwidget.rst": "api_reference/widgets/multiplotwidget.rst", + "widgets/pathbutton.rst": "api_reference/widgets/pathbutton.rst", + "widgets/plotwidget.rst": "api_reference/widgets/plotwidget.rst", + "widgets/progressdialog.rst": "api_reference/widgets/progressdialog.rst", + "widgets/rawimagewidget.rst": "api_reference/widgets/rawimagewidget.rst", + "widgets/remotegraphicsview.rst": "api_reference/widgets/remotegraphicsview.rst", + "widgets/scatterplotwidget.rst": "api_reference/widgets/scatterplotwidget.rst", + "widgets/spinbox.rst": "api_reference/widgets/spinbox.rst", + "widgets/tablewidget.rst": "api_reference/widgets/tablewidget.rst", + "widgets/treewidget.rst": "api_reference/widgets/treewidget.rst", + "widgets/valuelabel.rst": "api_reference/widgets/valuelabel.rst", + "widgets/verticallabel.rst": "api_reference/widgets/verticallabel.rst", + "internals.rst": "developer_guide/internals.rst", + "3dgraphics.rst": "getting_started/3dgraphics.rst", + "how_to_use.rst": "getting_started/how_to_use.rst", + "images.rst": "getting_started/images.rst", + "installation.rst": "getting_started/installation.rst", + "introduction.rst": "getting_started/introduction.rst", + "plotting.rst": "getting_started/plotting.rst", + "prototyping.rst": "getting_started/prototyping.rst", + "qtcrashcourse.rst": "getting_started/qtcrashcourse.rst", + "exporting.rst": "user_guide/exporting.rst", + "mouse_interaction.rst": "user_guide/mouse_interaction.rst", + "region_of_interest.rst": "user_guide/region_of_interest.rst", + "style.rst": "user_guide/style.rst" } # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, diff --git a/doc/source/developer_guide/index.rst b/doc/source/developer_guide/index.rst index ca896093ff..1ad4947872 100644 --- a/doc/source/developer_guide/index.rst +++ b/doc/source/developer_guide/index.rst @@ -3,6 +3,9 @@ Developer Guide =============== +The PyQtGraph library welcomes contributions from the community. +This guide is intended to help you get started with contributing to the project. + .. toctree:: internals diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst index d26ea5fbe5..e64c39b94f 100644 --- a/doc/source/getting_started/index.rst +++ b/doc/source/getting_started/index.rst @@ -3,6 +3,8 @@ Getting Started =============== +This section will guide you through the process of installing PyQtGraph and some common use cases, such as plotting data, displaying video and images, and building graphical user interfaces. + .. toctree:: :maxdepth: 1 diff --git a/doc/source/getting_started/introduction.rst b/doc/source/getting_started/introduction.rst index 21ced2b6e3..8f61ae28b0 100644 --- a/doc/source/getting_started/introduction.rst +++ b/doc/source/getting_started/introduction.rst @@ -78,6 +78,3 @@ How does it compare to... (My experience with these libraries is somewhat outdated; please correct me if I am wrong here) - - -.. rubric:: Footnotes diff --git a/doc/source/getting_started/prototyping.rst b/doc/source/getting_started/prototyping.rst index 3d2688d112..98f59d5d76 100644 --- a/doc/source/getting_started/prototyping.rst +++ b/doc/source/getting_started/prototyping.rst @@ -10,7 +10,7 @@ Parameter Trees The parameter tree system provides a widget displaying a tree of modifiable values similar to those used in most GUI editor applications. This allows a large number of variables to be controlled by the user with relatively little programming effort. The system also provides separation between the data being controlled and the user interface controlling it (model/view architecture). Parameters may be grouped/nested to any depth and custom parameter types can be built by subclassing from Parameter and ParameterItem. -See the `parametertree documentation `_ for more information. +See the :ref:`parametertree` documentation for more information. Visual Programming Flowcharts @@ -18,7 +18,7 @@ Visual Programming Flowcharts PyQtGraph's flowcharts provide a visual programming environment similar in concept to LabView--functional modules are added to a flowchart and connected by wires to define a more complex and arbitrarily configurable algorithm. A small number of predefined modules (called Nodes) are included with pyqtgraph, but most flowchart developers will want to define their own library of Nodes. At their core, the Nodes are little more than 1) a Python function 2) a list of input/output terminals, and 3) an optional widget providing a control panel for the Node. Nodes may transmit/receive any type of Python object via their terminals. -See the `flowchart documentation `_ and the flowchart examples for more information. +See the :ref:`flowchart` documentation and the flowchart examples for more information. .. _Canvas: diff --git a/doc/source/index.rst b/doc/source/index.rst index 5316849cdd..37b8069553 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,7 +1,10 @@ PyQtGraph ========= +A pure-python graphics and GUI library built on PyQt / PySide and numpy for use in mathematics / scientific / engineering applications. + .. toctree:: + :hidden: :maxdepth: 1 getting_started/index @@ -52,7 +55,7 @@ PyQtGraph API Reference ^^^^^^^^^^^^^ - The reference guide contains a detailed description of the PyQtGraph API + The reference guide contains a detailed description of the PyQtGraph API. +++ diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst index 196b09e997..c14ec716ed 100644 --- a/doc/source/user_guide/index.rst +++ b/doc/source/user_guide/index.rst @@ -3,8 +3,11 @@ User Guide ========== +This user guide provides an in-depth description of PyQtGraph features and how they apply to common scientific visualization tasks. +It is intended to be a reference for users who are already familiar with the basics of PyQtGraph. + .. toctree:: - :maxdepth: 2 + :maxdepth: 1 exporting mouse_interaction From f201e6287d565f05b1824410f66d7eea468328b8 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 2 Oct 2022 11:06:47 -0700 Subject: [PATCH 078/306] Support sphinx-autobuild --- doc/Makefile | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index 0f1e4ba258..2167f27cd5 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,15 +2,16 @@ # # You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXOPTS ?= -n +SPHINXBUILD ?= sphinx-build PAPER = BUILDDIR = build +SOURCEDIR = source # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest @@ -32,11 +33,17 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " livehtml to create livehtml of docs that watches for changes" clean: -rm -rf $(BUILDDIR)/* +livehtml: + sphinx-autobuild -a "$(SOURCEDIR)" "$(BUILDDIR)/html" --watch doc/source/_static --open-browser + html: + # this will not detect changes in theme files, static files and + # source code used with auto-doc $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." From 3ce21c2e7d35d170856a3a3064cad4b0f37cc826 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 13:47:41 +0000 Subject: [PATCH 079/306] Bump sphinx from 5.2.2 to 5.2.3 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.2 to 5.2.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.2...v5.2.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 8b25313547..533fb6aecc 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -sphinx==5.2.2 +sphinx==5.2.3 PyQt6==6.3.1 pydata-sphinx-theme sphinx-qt-documentation From 6fd0a4af2b7b81dbc80d7b3365dc6216891b7a6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 13:47:45 +0000 Subject: [PATCH 080/306] Bump pyqt6 from 6.3.1 to 6.4.0 in /doc Bumps [pyqt6](https://www.riverbankcomputing.com/software/pyqt/) from 6.3.1 to 6.4.0. --- updated-dependencies: - dependency-name: pyqt6 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 8b25313547..56efb32205 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ sphinx==5.2.2 -PyQt6==6.3.1 +PyQt6==6.4.0 pydata-sphinx-theme sphinx-qt-documentation sphinx-design From 3188e3c2902d7d916a88f34967031421dbaff0aa Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Mon, 3 Oct 2022 23:24:07 +0800 Subject: [PATCH 081/306] unify win32 and unix mmap codepaths --- pyqtgraph/widgets/RemoteGraphicsView.py | 60 +++++++++---------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index fea33392c0..6c4bbbdbc0 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -4,7 +4,6 @@ import enum import mmap import os -import random import sys import tempfile @@ -169,10 +168,11 @@ def __init__(self, parent=None, *args, **kwds): self.setMouseTracking(True) self.shm = None shmFileName = self._view.shmFileName() - if sys.platform.startswith('win'): - self.shmtag = shmFileName + if sys.platform == 'win32': + opener = lambda path, flags: os.open(path, flags | os.O_TEMPORARY) else: - self.shmFile = open(shmFileName, 'r') + opener = None + self.shmFile = open(shmFileName, 'rb', opener=opener) self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off')) ## Note: we need synchronous signals @@ -192,16 +192,11 @@ def sizeHint(self): return QtCore.QSize(*self._sizeHint) def remoteSceneChanged(self, data): - w, h, size, newfile = data - #self._sizeHint = (whint, hhint) + w, h, size = data if self.shm is None or self.shm.size != size: if self.shm is not None: self.shm.close() - if sys.platform.startswith('win'): - self.shmtag = newfile ## on windows, we create a new tag for every resize - self.shm = mmap.mmap(-1, size, self.shmtag) ## can't use tmpfile on windows because the file can only be opened once. - else: - self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) + self.shm = mmap.mmap(self.shmFile.fileno(), size, access=mmap.ACCESS_READ) self._img = QtGui.QImage(self.shm, w, h, QtGui.QImage.Format.Format_RGB32).copy() self.update() @@ -257,16 +252,11 @@ class Renderer(GraphicsView): def __init__(self, *args, **kwds): ## Create shared memory for rendered image - #pg.dbg(namespace={'r': self}) - if sys.platform.startswith('win'): - self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) - self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows - else: - self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') - self.shmFile.write(b'\x00' * (mmap.PAGESIZE+1)) - self.shmFile.flush() - fd = self.shmFile.fileno() - self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) + self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') + size = mmap.PAGESIZE + self.shmFile.write(b'\x00' * size) + self.shmFile.flush() + self.shm = mmap.mmap(self.shmFile.fileno(), size, access=mmap.ACCESS_WRITE) atexit.register(self.close) GraphicsView.__init__(self, *args, **kwds) @@ -278,14 +268,10 @@ def __init__(self, *args, **kwds): def close(self): self.shm.close() - if not sys.platform.startswith('win'): - self.shmFile.close() + self.shmFile.close() def shmFileName(self): - if sys.platform.startswith('win'): - return self.shmtag - else: - return self.shmFile.name + return self.shmFile.name def update(self): self.img = None @@ -307,19 +293,15 @@ def renderView(self): iheight = int(self.height() * dpr) size = iwidth * iheight * 4 if size > self.shm.size(): - if sys.platform.startswith('win'): - ## windows says "WindowsError: [Error 87] the parameter is incorrect" if we try to resize the mmap - self.shm.close() - ## it also says (sometimes) 'access is denied' if we try to reuse the tag. - self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) - self.shm = mmap.mmap(-1, size, self.shmtag) - elif sys.platform == 'darwin' or sys.platform.startswith('freebsd'): + try: + self.shm.resize(size) + except SystemError: + # actually, the platforms on which resize() _does_ work + # can also take this codepath self.shm.close() fd = self.shmFile.fileno() - os.ftruncate(fd, size + 1) - self.shm = mmap.mmap(fd, size, mmap.MAP_SHARED, mmap.PROT_WRITE) - else: - self.shm.resize(size) + os.ftruncate(fd, size) + self.shm = mmap.mmap(fd, size, access=mmap.ACCESS_WRITE) ## render the scene directly to shared memory @@ -338,4 +320,4 @@ def renderView(self): p = QtGui.QPainter(self.img) self.render(p, self.viewRect(), self.rect()) p.end() - self.sceneRendered.emit((iwidth, iheight, self.shm.size(), self.shmFileName())) + self.sceneRendered.emit((iwidth, iheight, self.shm.size())) From 227a4efa887ecd8f8655cfc50737004ecb9cbcae Mon Sep 17 00:00:00 2001 From: Gianfranco Costamagna Date: Mon, 3 Oct 2022 20:27:07 +0200 Subject: [PATCH 082/306] Update setup.py, import distutils after setuptools This makes the package build again with newer setuptools, fixing this build failure: make[2]: Leaving directory '/<>/doc' dh_auto_build I: pybuild base:240: /usr/bin/python3 setup.py build /<>/setup.py:38: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives import distutils.dir_util /usr/lib/python3/dist-packages/_distutils_hack/__init__.py:18: UserWarning: Distutils was imported before Setuptools, but importing Setuptools also replaces the `distutils` module in `sys.modules`. This may lead to undesirable behaviors or errors. To avoid these issues, avoid using distutils directly, ensure that setuptools is installed in the traditional way (e.g. not an editable install), and/or make sure that setuptools is always imported before distutils. warnings.warn( /usr/lib/python3/dist-packages/_distutils_hack/__init__.py:33: UserWarning: Setuptools is replacing distutils. warnings.warn("Setuptools is replacing distutils.") Traceback (most recent call last): File "/<>/setup.py", line 112, in setup( File "/usr/lib/python3/dist-packages/setuptools/__init__.py", line 87, in setup return distutils.core.setup(**attrs) File "/usr/lib/python3/dist-packages/setuptools/_distutils/core.py", line 172, in setup ok = dist.parse_command_line() File "/usr/lib/python3/dist-packages/setuptools/_distutils/dist.py", line 479, in parse_command_line args = self._parse_command_opts(parser, args) File "/usr/lib/python3/dist-packages/setuptools/dist.py", line 1107, in _parse_command_opts nargs = _Distribution._parse_command_opts(self, parser, args) File "/usr/lib/python3/dist-packages/setuptools/_distutils/dist.py", line 545, in _parse_command_opts raise DistutilsClassError( distutils.errors.DistutilsClassError: command class must subclass Command E: pybuild pybuild:379: build: plugin distutils failed with: exit code=1: /usr/bin/python3 setup.py build dh_auto_build: error: pybuild --build -i python{version} -p 3.10 returned exit code 13 make[1]: *** [debian/rules:17: override_dh_auto_build] Error 25 make[1]: Leaving directory '/<>' make: *** [debian/rules:6: binary-indep] Error 2 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index de875f6ee0..782363b34c 100644 --- a/setup.py +++ b/setup.py @@ -39,11 +39,11 @@ import os import re import sys -from distutils.command import build - from setuptools import find_namespace_packages, setup from setuptools.command import install +from distutils.command import build + path = os.path.split(__file__)[0] import tools.setupHelpers as helpers From 6b61962ae3ec0be5dfa5fdd191bd03c52afd03a6 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Sat, 1 Oct 2022 23:34:19 -0400 Subject: [PATCH 083/306] Allow plotting multiple data items at once --- pyqtgraph/examples/MultiDataPlot.py | 83 +++++++++++++++++++ pyqtgraph/examples/utils.py | 1 + pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 66 +++++++++++++++ tests/graphicsItems/PlotItem/test_PlotItem.py | 47 ++++++++++- 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 pyqtgraph/examples/MultiDataPlot.py diff --git a/pyqtgraph/examples/MultiDataPlot.py b/pyqtgraph/examples/MultiDataPlot.py new file mode 100644 index 0000000000..c8db14621e --- /dev/null +++ b/pyqtgraph/examples/MultiDataPlot.py @@ -0,0 +1,83 @@ +import traceback +import numpy as np + +import pyqtgraph as pg +from pyqtgraph.graphicsItems.ScatterPlotItem import name_list +from pyqtgraph.Qt import QtWidgets, QtCore +from pyqtgraph.parametertree import interact, ParameterTree, Parameter +import random + +pg.mkQApp() + +rng = np.random.default_rng(10) +random.seed(10) + + +def sortedRandint(low, high, size): + return np.sort(rng.integers(low, high, size)) + + +def isNoneOrScalar(value): + return value is None or np.isscalar(value[0]) + + +values = { + # Convention 1 + "None (replaced by integer indices)": None, + # Convention 2 + "Single curve values": sortedRandint(0, 20, 15), + # Convention 3 list form + "container of (optionally) mixed-size curve values": [ + sortedRandint(0, 20, 15), + *[sortedRandint(0, 20, 15) for _ in range(4)], + ], + # Convention 3 array form + "2D matrix": np.row_stack([sortedRandint(20, 40, 15) for _ in range(6)]), +} + + +def next_plot(xtype="random", ytype="random", symbol="o", symbolBrush="#f00"): + constKwargs = locals() + x = y = None + if xtype == "random": + xtype = random.choice(list(values)) + if ytype == "random": + ytype = random.choice(list(values)) + x = values[xtype] + y = values[ytype] + textbox.setValue(f"x={xtype}\ny={ytype}") + pltItem.clear() + try: + pltItem.multiDataPlot( + x=x, y=y, pen=cmap.getLookupTable(nPts=6), constKwargs=constKwargs + ) + except Exception as e: + QtWidgets.QMessageBox.critical(widget, "Error", traceback.format_exc()) + + +cmap = pg.colormap.get("viridis") +widget = pg.PlotWidget() +pltItem: pg.PlotItem = widget.plotItem + +xytype = dict(type="list", values=list(values)) +topParam = interact( + next_plot, + symbolBrush=dict(type="color"), + symbol=dict(type="list", values=name_list), + xtype=xytype, + ytype=xytype, +) +tree = ParameterTree() +tree.setMinimumWidth(150) +tree.addParameters(topParam, showTop=True) + +textbox = Parameter.create(name="text", type="text", readonly=True) +tree.addParameters(textbox) + +win = QtWidgets.QWidget() +win.setLayout(lay := QtWidgets.QHBoxLayout()) +lay.addWidget(widget) +lay.addWidget(tree) +if __name__ == "__main__": + win.show() + pg.exec() diff --git a/pyqtgraph/examples/utils.py b/pyqtgraph/examples/utils.py index 2fa4e3ed6c..1a624e7203 100644 --- a/pyqtgraph/examples/utils.py +++ b/pyqtgraph/examples/utils.py @@ -5,6 +5,7 @@ examples_ = OrderedDict([ ('Command-line usage', 'CLIexample.py'), ('Basic Plotting', Namespace(filename='Plotting.py', recommended=True)), + ('Plotting Datasets', 'MultiDataPlot.py'), ('ImageView', 'ImageView.py'), ('ParameterTree', 'parametertree.py'), ('Parameter-Function Interaction', 'InteractiveParameter.py'), diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 49db63ef3a..1a04a914ca 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -669,6 +669,72 @@ def addColorBar(self, image, **kargs): bar.setImageItem( image, insert_in=self ) return bar + def multiDataPlot(self, *, x=None, y=None, constKwargs=None, **kwargs): + """ + Allow plotting multiple curves on the same plot, changing some kwargs + per curve. + + Parameters + ---------- + x, y: array-like + can be in the following formats: + - {x or y} = [n1, n2, n3, ...]: The named argument iterates through + ``n`` curves, while the unspecified argument is range(len(n)) for + each curve. + - x, [y1, y2, y3, ...] + - [x1, x2, x3, ...], [y1, y2, y3, ...] + - [x1, x2, x3, ...], y + + where ``x_n`` and ``y_n`` are ``ndarray`` data for each curve. Since + ``x`` and ``y`` values are matched using ``zip``, unequal lengths mean + the longer array will be truncated. Note that 2D matrices for either x + or y are considered lists of curve + data. + constKwargs: dict, optional + A dict of {str: value} passed to each curve during ``plot()``. + kwargs: dict, optional + A dict of {str: iterable} where the str is the name of a kwarg and the + iterable is a list of values, one for each plotted curve. + """ + if (x is not None and not len(x)) or (y is not None and not len(y)): + # Nothing to plot -- either x or y array will bail out early from + # zip() below. + return [] + def scalarOrNone(val): + return val is None or (len(val) and np.isscalar(val[0])) + + if scalarOrNone(x) and scalarOrNone(y): + raise ValueError( + "If both `x` and `y` represent single curves, use `plot` instead " + "of `multiPlot`." + ) + curves = [] + constKwargs = constKwargs or {} + xy: 'dict[str, list | None]' = dict(x=x, y=y) + for key, oppositeVal in zip(('x', 'y'), [y, x]): + oppositeVal: 'Iterable | None' + val = xy[key] + if val is None: + # Other curve has all data, make range that supports longest chain + val = range(max(len(curveN) for curveN in oppositeVal)) + if np.isscalar(val[0]): + # x, [y1, y2, y3, ...] or [x1, x2, x3, ...], y + # Repeat the single curve to match length of opposite list + val = [val] * len(oppositeVal) + xy[key] = val + for ii, (xi, yi) in enumerate(zip(xy['x'], xy['y'])): + for kk in kwargs: + if len(kwargs[kk]) <= ii: + raise ValueError( + f"Not enough values for kwarg `{kk}`. " + f"Expected {ii + 1:d} (number of curves to plot), got" + f" {len(kwargs[kk]):d}" + ) + kwargsi = {kk: kwargs[kk][ii] for kk in kwargs} + constKwargs.update(kwargsi) + curves.append(self.plot(xi, yi, **constKwargs)) + return curves + def scatterPlot(self, *args, **kargs): if 'pen' in kargs: kargs['symbolPen'] = kargs['pen'] diff --git a/tests/graphicsItems/PlotItem/test_PlotItem.py b/tests/graphicsItems/PlotItem/test_PlotItem.py index 9727deab59..f0c52d0301 100644 --- a/tests/graphicsItems/PlotItem/test_PlotItem.py +++ b/tests/graphicsItems/PlotItem/test_PlotItem.py @@ -4,9 +4,29 @@ import pyqtgraph as pg app = pg.mkQApp() +rng = np.random.default_rng(1001) -@pytest.mark.parametrize('orientation', ['left', 'right', 'top', 'bottom']) +def sorted_randint(low, high, size): + return np.sort(rng.integers(low, high, size)) + + +def is_none_or_scalar(value): + return value is None or np.isscalar(value[0]) + + +multi_data_plot_values = [ + None, + sorted_randint(0, 20, 15), + [ + sorted_randint(0, 20, 15), + *[sorted_randint(0, 20, 15) for _ in range(4)], + ], + np.row_stack([sorted_randint(20, 40, 15) for _ in range(6)]), +] + + +@pytest.mark.parametrize("orientation", ["left", "right", "top", "bottom"]) def test_PlotItem_shared_axis_items(orientation): """Adding an AxisItem to multiple plots raises RuntimeError""" ax1 = pg.AxisItem(orientation) @@ -53,6 +73,31 @@ def test_PlotItem_maxTraces(): assert curve1 not in item.curves, "curve1 should not be in the item's curves" +@pytest.mark.parametrize("xvalues", multi_data_plot_values) +@pytest.mark.parametrize("yvalues", multi_data_plot_values) +def test_PlotItem_multi_data_plot(xvalues, yvalues): + item = pg.PlotItem() + if is_none_or_scalar(xvalues) and is_none_or_scalar(yvalues): + with pytest.raises(ValueError): + item.multiDataPlot(x=xvalues, y=yvalues) + return + else: + curves = item.multiDataPlot(x=xvalues, y=yvalues, constKwargs={"pen": "r"}) + check_idx = None + if xvalues is None: + check_idx = 0 + elif yvalues is None: + check_idx = 1 + if check_idx is not None: + for curve in curves: + data = curve.getData() + opposite_idx = 1 - check_idx + assert np.array_equal( + data[check_idx], np.arange(len(data[opposite_idx])) + ) + assert curve.opts["pen"] == "r" + + def test_PlotItem_preserve_external_visibility_control(): item = pg.PlotItem() curve1 = pg.PlotDataItem(np.random.normal(size=10)) From 6716df608c56d632b82e15d8862448683edf1b8b Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 5 Oct 2022 02:01:35 +0800 Subject: [PATCH 084/306] re-enable hmac authentication for win32 tested working on Windows 7, Python 3.8.10 --- pyqtgraph/multiprocess/processes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 520f31f011..7f8e343fd6 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -85,11 +85,6 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de ## random authentication key authkey = os.urandom(20) - ## Windows seems to have a hard time with hmac - if sys.platform.startswith('win'): - authkey = None - - #print "key:", ' '.join([str(ord(x)) for x in authkey]) ## Listen for connection from remote process (and find free port number) l = multiprocessing.connection.Listener(('localhost', 0), authkey=authkey) port = l.address[1] From 2320ac63aa545dbccc20f00b8c59db157a0acfe3 Mon Sep 17 00:00:00 2001 From: Jaime R <38530589+Jaime02@users.noreply.github.com> Date: Thu, 6 Oct 2022 05:05:54 +0200 Subject: [PATCH 085/306] Remove all import * (#2468) * Remove all import * I have removed all the import * code, this makes all imports implicit. Co-authored-by: Jaime Resano --- pyqtgraph/SRTTransform.py | 2 +- pyqtgraph/SRTTransform3D.py | 2 +- pyqtgraph/examples/optics_demos.py | 3 ++- pyqtgraph/flowchart/library/Display.py | 2 +- pyqtgraph/graphicsItems/GradientEditorItem.py | 2 +- pyqtgraph/graphicsItems/GradientLegend.py | 2 +- pyqtgraph/graphicsItems/GridItem.py | 2 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 10 +++++----- pyqtgraph/graphicsItems/IsocurveItem.py | 2 +- pyqtgraph/graphicsItems/ScaleBar.py | 4 ++-- pyqtgraph/imageview/ImageView.py | 10 +++++----- pyqtgraph/widgets/PlotWidget.py | 4 ++-- 12 files changed, 23 insertions(+), 22 deletions(-) diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index 07e3c14cde..aa63145013 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -169,7 +169,7 @@ def matrix(self): import GraphicsView from . import widgets - from .functions import * + from .functions import mkPen app = pg.mkQApp() # noqa: qapp stored to avoid gc win = QtWidgets.QMainWindow() win.show() diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 0dadaa488b..01f4f35f9f 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -229,7 +229,7 @@ def matrix(self, nd=3): import GraphicsView from . import widgets - from .functions import * + from .functions import mkPen app = pg.mkQApp() # noqa: qapp must be stored to avoid gc win = QtWidgets.QMainWindow() win.show() diff --git a/pyqtgraph/examples/optics_demos.py b/pyqtgraph/examples/optics_demos.py index 2b80ca0bac..f551ec02e8 100644 --- a/pyqtgraph/examples/optics_demos.py +++ b/pyqtgraph/examples/optics_demos.py @@ -3,10 +3,11 @@ """ import numpy as np -from optics import * +from optics import Mirror, Ray, Tracer, Lens import pyqtgraph as pg from pyqtgraph import Point +from pyqtgraph.Qt import QtCore app = pg.mkQApp("Optics Demo") diff --git a/pyqtgraph/flowchart/library/Display.py b/pyqtgraph/flowchart/library/Display.py index 779bfed2c1..d54d2f589f 100644 --- a/pyqtgraph/flowchart/library/Display.py +++ b/pyqtgraph/flowchart/library/Display.py @@ -4,7 +4,7 @@ from ...graphicsItems.ScatterPlotItem import ScatterPlotItem from ...Qt import QtCore, QtGui, QtWidgets from ..Node import Node -from .common import * +from .common import CtrlNode class PlotWidgetNode(Node): diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index bdf80ac66b..0da7f268ef 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -12,7 +12,7 @@ translate = QtCore.QCoreApplication.translate -__all__ = ['TickSliderItem', 'GradientEditorItem'] +__all__ = ['TickSliderItem', 'GradientEditorItem', 'addGradientListToDocstring'] Gradients = OrderedDict([ ('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}), diff --git a/pyqtgraph/graphicsItems/GradientLegend.py b/pyqtgraph/graphicsItems/GradientLegend.py index 9d65c3175c..50343ed9a6 100644 --- a/pyqtgraph/graphicsItems/GradientLegend.py +++ b/pyqtgraph/graphicsItems/GradientLegend.py @@ -1,6 +1,6 @@ from .. import functions as fn from ..Qt import QtCore, QtGui -from .UIGraphicsItem import * +from .UIGraphicsItem import UIGraphicsItem __all__ = ['GradientLegend'] diff --git a/pyqtgraph/graphicsItems/GridItem.py b/pyqtgraph/graphicsItems/GridItem.py index 5e72ea6b7d..95715557a2 100644 --- a/pyqtgraph/graphicsItems/GridItem.py +++ b/pyqtgraph/graphicsItems/GridItem.py @@ -4,7 +4,7 @@ from .. import getConfigOption from ..Point import Point from ..Qt import QtCore, QtGui -from .UIGraphicsItem import * +from .UIGraphicsItem import UIGraphicsItem __all__ = ['GridItem'] class GridItem(UIGraphicsItem): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 11d581d233..e9b83e3552 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -12,12 +12,12 @@ from .. import functions as fn from ..Point import Point from ..Qt import QtCore, QtGui, QtWidgets -from .AxisItem import * -from .GradientEditorItem import * +from .AxisItem import AxisItem +from .GradientEditorItem import GradientEditorItem from .GraphicsWidget import GraphicsWidget -from .LinearRegionItem import * -from .PlotCurveItem import * -from .ViewBox import * +from .LinearRegionItem import LinearRegionItem +from .PlotCurveItem import PlotCurveItem +from .ViewBox import ViewBox __all__ = ['HistogramLUTItem'] diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index ce9aa64124..0dec0430c5 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -1,7 +1,7 @@ from .. import functions as fn from .. import getConfigOption from ..Qt import QtCore, QtGui -from .GraphicsObject import * +from .GraphicsObject import GraphicsObject __all__ = ['IsocurveItem'] diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py index 7ad913eb87..d4942a1b78 100644 --- a/pyqtgraph/graphicsItems/ScaleBar.py +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -2,8 +2,8 @@ from .. import getConfigOption from ..Point import Point from ..Qt import QtCore, QtWidgets -from .GraphicsObject import * -from .GraphicsWidgetAnchor import * +from .GraphicsObject import GraphicsObject +from .GraphicsWidgetAnchor import GraphicsWidgetAnchor from .TextItem import TextItem __all__ = ['ScaleBar'] diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 47b2d01ae1..7960ba2c62 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -21,11 +21,11 @@ from .. import functions as fn from .. import getConfigOption from ..graphicsItems.GradientEditorItem import addGradientListToDocstring -from ..graphicsItems.ImageItem import * -from ..graphicsItems.InfiniteLine import * -from ..graphicsItems.LinearRegionItem import * -from ..graphicsItems.ROI import * -from ..graphicsItems.ViewBox import * +from ..graphicsItems.ImageItem import ImageItem +from ..graphicsItems.InfiniteLine import InfiniteLine +from ..graphicsItems.LinearRegionItem import LinearRegionItem +from ..graphicsItems.ROI import ROI +from ..graphicsItems.ViewBox import ViewBox from ..graphicsItems.VTickGroup import VTickGroup from ..Qt import QtCore, QtGui, QtWidgets from ..SignalProxy import SignalProxy diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index ad3c25681a..932a30fdef 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -4,9 +4,9 @@ Distributed under MIT/X11 license. See license.txt for more information. """ -from ..graphicsItems.PlotItem import * +from ..graphicsItems.PlotItem import PlotItem from ..Qt import QtCore, QtWidgets -from .GraphicsView import * +from .GraphicsView import GraphicsView __all__ = ['PlotWidget'] class PlotWidget(GraphicsView): From b504c199c7f267833dbbcacec15645f52cbea3ef Mon Sep 17 00:00:00 2001 From: Jaime R <38530589+Jaime02@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:30:41 +0200 Subject: [PATCH 086/306] Fix Qt crash course example (#2470) * Fix Qt crash course example Updated to work with PyQt6. Fully compatible with PySide6. Can work with PyQt5 / PySide2 changing exec to exec_ * Fix import order Co-authored-by: Jaime Resano --- doc/source/getting_started/qtcrashcourse.rst | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/doc/source/getting_started/qtcrashcourse.rst b/doc/source/getting_started/qtcrashcourse.rst index bc993974c9..66b63717e8 100644 --- a/doc/source/getting_started/qtcrashcourse.rst +++ b/doc/source/getting_started/qtcrashcourse.rst @@ -16,37 +16,38 @@ PyQtGraph fits into this scheme by providing its own QWidget subclasses to be in Example:: - - from PyQt5 import QtGui # (the example applies equally well to PySide2) + + from PyQt6 import QtWidgets # Should work with PyQt5 / PySide2 / PySide6 as well import pyqtgraph as pg - + ## Always start by initializing Qt (only once per application) - app = QtGui.QApplication([]) + app = QtWidgets.QApplication([]) ## Define a top-level widget to hold everything - w = QtGui.QWidget() + w = QtWidgets.QWidget() + w.setWindowTitle('PyQtGraph example') ## Create some widgets to be placed inside - btn = QtGui.QPushButton('press me') - text = QtGui.QLineEdit('enter text') - listw = QtGui.QListWidget() + btn = QtWidgets.QPushButton('press me') + text = QtWidgets.QLineEdit('enter text') + listw = QtWidgets.QListWidget() plot = pg.PlotWidget() ## Create a grid layout to manage the widgets size and position - layout = QtGui.QGridLayout() + layout = QtWidgets.QGridLayout() w.setLayout(layout) ## Add widgets to the layout in their proper positions - layout.addWidget(btn, 0, 0) # button goes in upper-left - layout.addWidget(text, 1, 0) # text edit goes in middle-left + layout.addWidget(btn, 0, 0) # button goes in upper-left + layout.addWidget(text, 1, 0) # text edit goes in middle-left layout.addWidget(listw, 2, 0) # list widget goes in bottom-left layout.addWidget(plot, 0, 1, 3, 1) # plot goes on right side, spanning 3 rows - ## Display the widget as a new window w.show() ## Start the Qt event loop - app.exec_() + app.exec() # or app.exec_() for PyQt5 / PySide2 + More complex interfaces may be designed graphically using Qt Designer, which allows you to simply drag widgets into your window to define its appearance. From d7f09271434bdac53a868473f47fc9ee9d8d7b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20E=2E=20S=C3=B8rensen?= Date: Thu, 6 Oct 2022 23:11:35 +0200 Subject: [PATCH 087/306] Add keyword argument in PColorMeshItem to enable consistent colormap scaling during animation (#2463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add keyword argument in PColorMeshItem to enable consistent colormap scaling during animation. * Update PColorMeshItem example to illustrate usage of keyword arguments `colorMap` and `limit` * Rename limits and cmaplim to levels in order to conform with naming convention in ImageItem and ColorBarItem * Add autoLevels parameters to PColorMeshItem Add enableAutoLevels parameter to PColorMeshItem and autoLevels parameter to PColorMeshItem.setData() in order to conform with known APIs from ViewBox and ImageItem * Update PColorMeshItem.py Fix typos and improve code quality Co-authored-by: Simen E. Sørensen --- pyqtgraph/examples/PColorMeshItem.py | 15 +++++++--- pyqtgraph/graphicsItems/PColorMeshItem.py | 34 +++++++++++++++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/examples/PColorMeshItem.py b/pyqtgraph/examples/PColorMeshItem.py index cf64158664..a1e2cc7396 100644 --- a/pyqtgraph/examples/PColorMeshItem.py +++ b/pyqtgraph/examples/PColorMeshItem.py @@ -44,9 +44,14 @@ ## Create image item edgecolors = None antialiasing = False -# edgecolors = {'color':'w', 'width':2} # May be uncommened to see edgecolor effect -# antialiasing = True # May be uncommened to see antialiasing effect -pcmi = pg.PColorMeshItem(edgecolors=edgecolors, antialiasing=antialiasing) +cmap = pg.colormap.get('viridis') +levels = (-2,2) # Will be overwritten unless enableAutoLevels is set to False +enableAutoLevels = True +# edgecolors = {'color':'w', 'width':2} # May be uncommented to see edgecolor effect +# antialiasing = True # May be uncommented to see antialiasing effect +# cmap = pg.colormap.get('plasma') # May be uncommented to see a different colormap than the default 'viridis' +# enableAutoLevels = False # may be uncommented to see changes in the absolute value of z (color_noise) which is hidden by the autoscaling colormap when using the default `levels=None` +pcmi = pg.PColorMeshItem(edgecolors=edgecolors, antialiasing=antialiasing, colorMap=cmap, levels=levels, enableAutoLevels=enableAutoLevels) view.addItem(pcmi) textitem = pg.TextItem(anchor=(1, 0)) view.addItem(textitem) @@ -60,6 +65,7 @@ wave_speed = 0.3 wave_length = 10 color_speed = 0.3 +color_noise_freq = 0.05 timer = QtCore.QTimer() timer.setSingleShot(True) @@ -73,9 +79,10 @@ def updateData(): ## Display the new data set t0 = time.perf_counter() + color_noise = np.sin(i * 2*np.pi*color_noise_freq) new_x = x new_y = y+wave_amplitude*np.cos(x/wave_length+i) - new_z = np.exp(-(x-np.cos(i*color_speed)*xn)**2/1000)[:-1,:-1] + new_z = np.exp(-(x-np.cos(i*color_speed)*xn)**2/1000)[:-1,:-1] + color_noise t1 = time.perf_counter() pcmi.setData(new_x, new_y, diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index d1ddbb27c6..401073a5ec 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -88,6 +88,17 @@ def __init__(self, *args, **kwargs): colorMap : pyqtgraph.ColorMap Colormap used to map the z value to colors. default ``pyqtgraph.colormap.get('viridis')`` + levels: tuple, optional, default None + Sets the minimum and maximum values to be represented by the colormap (min, max). + Values outside this range will be clipped to the colors representing min or max. + ``None`` disables the limits, meaning that the colormap will autoscale + each time ``setData()`` is called - unless ``enableAutoLevels=False``. + enableAutoLevels: bool, optional, default True + Causes the colormap levels to autoscale whenever ``setData()`` is called. + When enableAutoLevels is set to True, it is still possible to disable autoscaling + on a per-change-basis by using ``autoLevels=False`` when calling ``setData()``. + If ``enableAutoLevels==False`` and ``levels==None``, autoscaling will be + performed once when the first z data is supplied. edgecolors : dict, optional The color of the edges of the polygons. Default None means no edges. @@ -107,6 +118,8 @@ def __init__(self, *args, **kwargs): self.edgecolors = kwargs.get('edgecolors', None) self.antialiasing = kwargs.get('antialiasing', False) + self.levels = kwargs.get('levels', None) + self.enableAutoLevels = kwargs.get('enableAutoLevels', True) if 'colorMap' in kwargs: cmap = kwargs.get('colorMap') @@ -178,7 +191,7 @@ def _prepareData(self, args): ValueError('Data must been sent as (z) or (x, y, z)') - def setData(self, *args): + def setData(self, *args, **kwargs): """ Set the data to be drawn. @@ -200,7 +213,13 @@ def setData(self, *args): "ASCII from: ". + autoLevels: bool, optional, default True + When set to True, PColorMeshItem will automatically select levels + based on the minimum and maximum values encountered in the data along the z axis. + The minimum and maximum levels are mapped to the lowest and highest colors + in the colormap. The autoLevels parameter is ignored if ``enableAutoLevels is False`` """ + autoLevels = kwargs.get('autoLevels', True) # Has the view bounds changed shapeChanged = False @@ -233,8 +252,17 @@ def setData(self, *args): lut = self.lut_qbrush # Second we associate each z value, that we normalize, to the lut scale = len(lut) - 1 - z_min = self.z.min() - z_max = self.z.max() + # Decide whether to autoscale the colormap or use the same levels as before + if (self.levels is None) or (self.enableAutoLevels and autoLevels): + # Autoscale colormap + z_min = self.z.min() + z_max = self.z.max() + if not self.enableAutoLevels: + self.levels = (z_min, z_max) + else: + # Use consistent colormap scaling + z_min = self.levels[0] + z_max = self.levels[1] rng = z_max - z_min if rng == 0: rng = 1 From 8b6a6c69eb2a7568cda7825da29611fd9cbfd157 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 6 Oct 2022 20:34:26 -0700 Subject: [PATCH 088/306] Remove duplicate entries in point docs --- doc/source/api_reference/point.rst | 12 ------------ pyqtgraph/Point.py | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/doc/source/api_reference/point.rst b/doc/source/api_reference/point.rst index 26e5e24616..a28c03facb 100644 --- a/doc/source/api_reference/point.rst +++ b/doc/source/api_reference/point.rst @@ -3,15 +3,3 @@ Point .. automodule:: pyqtgraph.Point :members: - - .. automethod:: pyqtgraph.Point.length - - .. automethod:: pyqtgraph.Point.norm - - .. automethod:: pyqtgraph.Point.angle - - .. automethod:: pyqtgraph.Point.dot - - .. automethod:: pyqtgraph.Point.cross - - .. automethod:: pyqtgraph.Point.proj diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index c90487168f..ac02198763 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -134,6 +134,7 @@ def dot(self, a): return Point.dotProduct(self, a) def cross(self, a): + """Returns the cross product of a and this Point""" if not isinstance(a, QtCore.QPointF): a = Point(a) return self.x() * a.y() - self.y() * a.x() From 5d4065a5ae8d0931cebc343ef19bb8c51b41a5e2 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 7 Oct 2022 17:42:33 +0800 Subject: [PATCH 089/306] fix: use connect='finite' if finite-ness of data is unknown --- pyqtgraph/graphicsItems/PlotDataItem.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index d9843864ac..0c2737f80c 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -271,8 +271,8 @@ def __init__(self, *args, **kargs): values exist, unpredictable behavior will occur. The data may not be displayed or the plot may take a significant performance hit. - In the default 'auto' connect mode, `PlotDataItem` will apply this - setting automatically. + In the default 'auto' connect mode, `PlotDataItem` will automatically + override this setting. ================= ======================================================================= **Meta-info keyword arguments:** @@ -877,14 +877,17 @@ def updateItems(self, styleUpdate=True): ): # draw if visible... # auto-switch to indicate non-finite values as interruptions in the curve: if isinstance(curveArgs['connect'], str) and curveArgs['connect'] == 'auto': # connect can also take a boolean array - if dataset.containsNonfinite is None: - curveArgs['connect'] = 'all' # this is faster, but silently connects the curve across any non-finite values - else: - if dataset.containsNonfinite: - curveArgs['connect'] = 'finite' - else: - curveArgs['connect'] = 'all' # all points can be connected, and no further check is needed. - curveArgs['skipFiniteCheck'] = True + if dataset.containsNonfinite is False: + # all points can be connected, and no further check is needed. + curveArgs['connect'] = 'all' + curveArgs['skipFiniteCheck'] = True + else: # True or None + # True: (we checked and found non-finites) + # don't connect non-finites + # None: (we haven't performed a check for non-finites yet) + # use connect='finite' in case there are non-finites. + curveArgs['connect'] = 'finite' + curveArgs['skipFiniteCheck'] = False self.curve.setData(x=x, y=y, **curveArgs) self.curve.show() else: # ...hide if not. From cb3e90fcfac86db5dc98c93a4d993e99dd4c5262 Mon Sep 17 00:00:00 2001 From: bbc131 <36670201+bbc131@users.noreply.github.com> Date: Fri, 7 Oct 2022 14:15:00 +0200 Subject: [PATCH 090/306] Fix ButtonItem hover event * Fix method name `mouseHoverEvent` -> `hoverEvent`. Therefore, it has never been called. * Fix changing opacity while hovering. Change it back to "non-hover-state" only after hover event with "is-exit" occurs. (Note, that `hoverEvent` is called multiple times while mouse cursor is on the item.) --- pyqtgraph/graphicsItems/ButtonItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ButtonItem.py b/pyqtgraph/graphicsItems/ButtonItem.py index be38f7ccec..acacdde922 100644 --- a/pyqtgraph/graphicsItems/ButtonItem.py +++ b/pyqtgraph/graphicsItems/ButtonItem.py @@ -33,12 +33,12 @@ def mouseClickEvent(self, ev): if self.enabled: self.clicked.emit(self) - def mouseHoverEvent(self, ev): + def hoverEvent(self, ev): if not self.enabled: return if ev.isEnter(): self.setOpacity(1.0) - else: + elif ev.isExit(): self.setOpacity(0.7) def disable(self): From b2e6c729e6f89ca42b704c8f4f24ea32b6492906 Mon Sep 17 00:00:00 2001 From: Soham Biswas Date: Sun, 9 Oct 2022 03:38:13 +0530 Subject: [PATCH 091/306] Raise appropriate Exceptions in place of generic exceptions (#2474) --- pyqtgraph/graphicsItems/PlotDataItem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 0c2737f80c..94c19a6baa 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -707,7 +707,7 @@ def setData(self, *args, **kargs): x = np.array(data['x']) if 'y' in data: y = np.array(data['y']) - elif dt == 'listOfDicts': + elif dt == 'listOfDicts': if 'x' in data[0]: x = np.array([d.get('x',None) for d in data]) if 'y' in data[0]: @@ -719,13 +719,13 @@ def setData(self, *args, **kargs): y = data.view(np.ndarray) x = data.xvals(0).view(np.ndarray) else: - raise Exception('Invalid data type %s' % type(data)) + raise TypeError('Invalid data type %s' % type(data)) elif len(args) == 2: seq = ('listOfValues', 'MetaArray', 'empty') dtyp = dataType(args[0]), dataType(args[1]) if dtyp[0] not in seq or dtyp[1] not in seq: - raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) + raise TypeError('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) if not isinstance(args[0], np.ndarray): #x = np.array(args[0]) if dtyp[0] == 'MetaArray': @@ -1217,7 +1217,7 @@ def dataType(obj): elif obj.ndim == 2 and obj.dtype.names is None and obj.shape[1] == 2: return 'Nx2array' else: - raise Exception('array shape must be (N,) or (N,2); got %s instead' % str(obj.shape)) + raise ValueError('array shape must be (N,) or (N,2); got %s instead' % str(obj.shape)) elif isinstance(first, dict): return 'listOfDicts' else: From 6e07e48f4dc70df7ce132ee90266b6e61731b5ba Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 8 Oct 2022 20:37:05 -0700 Subject: [PATCH 092/306] Use SPDX marker for BSD3 license in custom.css --- doc/source/_static/custom.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css index 6d9f25e891..e63b96aed4 100644 --- a/doc/source/_static/custom.css +++ b/doc/source/_static/custom.css @@ -1,6 +1,6 @@ -/* Contents/Template taken from the SciPy project */ -/* https://github.com/scipy/scipy/blob/9ae8fd0f4341d7d8785777d460cca4f7b6a93edd/doc/source/_static/scipy.css */ -/* PyQtGraph specifics */ +/* Template Inherited from SciPy and modified for PyQtGraph purposes + https://github.com/scipy/scipy/blob/9ae8fd0f4341d7d8785777d460cca4f7b6a93edd/doc/source/_static/scipy.css + SPDX-License-Identifier: BSD-3-Clause */ /* Remove parenthesis around module using fictive font and add them back. This is needed for better wrapping in the sidebar. */ From a5013e6babcdcf15d3d66cb21080a5b5ecb7e654 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 8 Oct 2022 20:38:54 -0700 Subject: [PATCH 093/306] Pin versions of pydata-sphinx-theme and sphinx-design --- doc/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index d2190c86a9..23d38d308b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,8 @@ PyQt6==6.4.0 sphinx==5.2.3 -pydata-sphinx-theme +pydata-sphinx-theme==0.11.0 +sphinx-design==0.3.0 sphinx-qt-documentation -sphinx-design sphinxext-rediraffe numpy pyopengl From 55a099af626c65a85123a1002ecad1698a799d13 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 8 Oct 2022 23:24:54 -0700 Subject: [PATCH 094/306] Unset added background in dark mode to svgs --- doc/source/_static/custom.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css index e63b96aed4..9adf36c46b 100644 --- a/doc/source/_static/custom.css +++ b/doc/source/_static/custom.css @@ -134,9 +134,9 @@ Matplotlib images are in png and inverted while other output types are assumed to be normal images. */ -html[data-theme=dark] img[src*='.svg']:not(.only-dark) { +html[data-theme=dark] img[src*='.svg']:not(.only-dark):not(.dark-light) { filter: brightness(0.8) invert(0.82) contrast(1.2); - background: none; + background: unset } html[data-theme=dark] .MathJax_SVG * { From 9a3e4dbc962deda0bfb4b6f66b4fc02b9e45b5c6 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 8 Oct 2022 23:27:55 -0700 Subject: [PATCH 095/306] Add sphinxcontrib-images dependency for better image dislpay --- doc/requirements.txt | 1 + doc/source/api_reference/colormap.rst | 6 ++++-- doc/source/api_reference/graphicsItems/imageitem.rst | 4 ++-- doc/source/conf.py | 8 ++++++-- doc/source/getting_started/plotting.rst | 5 +++-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 23d38d308b..cd7a06bf46 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,6 +2,7 @@ PyQt6==6.4.0 sphinx==5.2.3 pydata-sphinx-theme==0.11.0 sphinx-design==0.3.0 +sphinxcontrib-images==0.9.4 sphinx-qt-documentation sphinxext-rediraffe numpy diff --git a/doc/source/api_reference/colormap.rst b/doc/source/api_reference/colormap.rst index 4e40fa3fdc..5a91e6fbb2 100644 --- a/doc/source/api_reference/colormap.rst +++ b/doc/source/api_reference/colormap.rst @@ -55,15 +55,17 @@ Using QtGui.QPen and QtGui.QBrush to color plots according to the plotted value: :lines: 16-33 :dedent: 8 -.. image:: +.. thumbnail:: /images/example_false_color_image.png :width: 49% :alt: Example of a false color image + :title: False color image -.. image:: +.. thumbnail:: /images/example_gradient_plot.png :width: 49% :alt: Example of drawing and filling plots with gradients + :title: Drawing and filling plots with gradients The use of color maps is also demonstrated in the `ImageView`, `Color Gradient Plots` and `ColorBarItem` :ref:`examples`. diff --git a/doc/source/api_reference/graphicsItems/imageitem.rst b/doc/source/api_reference/graphicsItems/imageitem.rst index d33c02f6c7..1d16c1e653 100644 --- a/doc/source/api_reference/graphicsItems/imageitem.rst +++ b/doc/source/api_reference/graphicsItems/imageitem.rst @@ -54,11 +54,11 @@ Examples :lines: 19-28 :dedent: 8 -.. image:: +.. thumbnail:: /images/example_imageitem_transform.png :width: 49% :alt: Example of transformed image display - + :title: Transformed Image Display .. autoclass:: pyqtgraph.ImageItem diff --git a/doc/source/conf.py b/doc/source/conf.py index f7b702ab6b..8101f398d0 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -39,7 +39,8 @@ "sphinx.ext.intersphinx", "sphinx_qt_documentation", "sphinx_design", - "sphinxext.rediraffe" + "sphinxext.rediraffe", + "sphinxcontrib.images" ] # Add any paths that contain templates here, relative to this directory. @@ -309,7 +310,10 @@ #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +html_sidebars = { + "**": ["sidebar-nav-bs.html", "sidebar-ethical-ads.html"], + 'index': [] # don't show sidebar on main landing page +} # Additional templates that should be rendered to pages, maps page names to # template names. diff --git a/doc/source/getting_started/plotting.rst b/doc/source/getting_started/plotting.rst index c9a489e266..4e1ca89aa1 100644 --- a/doc/source/getting_started/plotting.rst +++ b/doc/source/getting_started/plotting.rst @@ -43,8 +43,9 @@ There are several classes invloved in displaying plot data. Most of these classe * :class:`PlotWidget ` - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. * :class:`GraphicsLayoutWidget ` - QWidget subclass displaying a single :class:`~pyqtgraph.GraphicsLayout`. Most of the methods provided by :class:`~pyqtgraph.GraphicsLayout` are also available through GraphicsLayoutWidget. -.. image:: /images/plottingClasses.png - +.. thumbnail:: + /images/plottingClasses.png + :title: Elements of PlotClasses See the :ref:`UML class diagram ` page for a more detailed figure of the most important classes and their relations. From e07fcd82eebe93c772ac8f99aaa0aeade0ce9f2d Mon Sep 17 00:00:00 2001 From: Martin Chase Date: Mon, 10 Oct 2022 16:52:04 -0700 Subject: [PATCH 096/306] test and fix for ChecklistParameter.show bug --- .../parametertree/parameterTypes/checklist.py | 1 + tests/parametertree/test_parametertypes.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/checklist.py b/pyqtgraph/parametertree/parameterTypes/checklist.py index e7257648d3..4106d59939 100644 --- a/pyqtgraph/parametertree/parameterTypes/checklist.py +++ b/pyqtgraph/parametertree/parameterTypes/checklist.py @@ -66,6 +66,7 @@ def takeChild(self, i): self.btnGrp.removeButton(child.widget) def optsChanged(self, param, opts): + super().optsChanged(param, opts) if 'expanded' in opts: for btn in self.metaBtns.values(): btn.setVisible(opts['expanded']) diff --git a/tests/parametertree/test_parametertypes.py b/tests/parametertree/test_parametertypes.py index 53fd2cd950..d07c575502 100644 --- a/tests/parametertree/test_parametertypes.py +++ b/tests/parametertree/test_parametertypes.py @@ -1,10 +1,11 @@ -import sys +from unittest.mock import MagicMock import numpy as np import pyqtgraph as pg import pyqtgraph.parametertree as pt from pyqtgraph.functions import eq +from pyqtgraph.parametertree.parameterTypes import ChecklistParameterItem from pyqtgraph.Qt import QtCore, QtGui app = pg.mkQApp() @@ -163,6 +164,17 @@ def override(): pi.widget.setValue(2) assert p.value() == pi.widget.value() == 1 + +def test_checklist_show_hide(): + p = pt.Parameter.create(name='checklist', type='checklist', limits=["a", "b", "c"]) + pi = ChecklistParameterItem(p, 0) + pi.setHidden = MagicMock() + p.hide() + pi.setHidden.assert_called_with(True) + p.show() + pi.setHidden.assert_called_with(False) + + def test_pen_settings(): # Option from constructor p = pt.Parameter.create(name='test', type='pen', width=5, additionalname='test') @@ -180,4 +192,4 @@ def test_recreate_from_savestate(): created = _buildParamTypes.makeAllParamTypes() state = created.saveState() created2 = pt.Parameter.create(**state) - assert pg.eq(state, created2.saveState()) \ No newline at end of file + assert pg.eq(state, created2.saveState()) From 914e81453e7d6980db746c660d51a5fa06f62aea Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 11 Oct 2022 12:58:17 +0800 Subject: [PATCH 097/306] bugfix: need to return a tuple --- pyqtgraph/graphicsItems/PlotCurveItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index a762cd02fb..0a50c706fd 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -73,7 +73,7 @@ def get(self, size): def arrayToLineSegments(self, x, y, connect, finiteCheck): # analogue of arrayToQPath taking the same parameters if len(x) < 2: - return [] + return [], connect_array = None if isinstance(connect, np.ndarray): From 4593bc28c14f85bed1a34498ed6bdac9c956a2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20G=C3=B6ries?= <43136580+dgoeries@users.noreply.github.com> Date: Tue, 11 Oct 2022 13:46:26 +0200 Subject: [PATCH 098/306] ROI: Add test with mouseDrag event and check the snapping (#2476) --- tests/graphicsItems/test_ROI.py | 228 ++++++++++++++++++++++---------- 1 file changed, 157 insertions(+), 71 deletions(-) diff --git a/tests/graphicsItems/test_ROI.py b/tests/graphicsItems/test_ROI.py index 2889220f03..ce2c1b4a3e 100644 --- a/tests/graphicsItems/test_ROI.py +++ b/tests/graphicsItems/test_ROI.py @@ -12,6 +12,7 @@ app = pg.mkQApp() pg.setConfigOption("mouseRateLimit", 0) + def test_getArrayRegion(transpose=False): pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr.setPos(1, 1) @@ -24,38 +25,40 @@ def test_getArrayRegion(transpose=False): for roi, name in rois: # For some ROIs, resize should not be used. testResize = not isinstance(roi, pg.PolyLineROI) - + origMode = pg.getConfigOption('imageAxisOrder') try: if transpose: pg.setConfigOptions(imageAxisOrder='row-major') - check_getArrayRegion(roi, 'roi/'+name, testResize, transpose=True) + check_getArrayRegion(roi, 'roi/' + name, testResize, + transpose=True) else: pg.setConfigOptions(imageAxisOrder='col-major') - check_getArrayRegion(roi, 'roi/'+name, testResize) + check_getArrayRegion(roi, 'roi/' + name, testResize) finally: pg.setConfigOptions(imageAxisOrder=origMode) - + + def test_getArrayRegion_axisorder(): test_getArrayRegion(transpose=True) - + def check_getArrayRegion(roi, name, testResize=True, transpose=False): - # on windows, edges corner pixels seem to be slightly different from other platforms - # giving a pxCount=2 for a fudge factor - if isinstance(roi, (pg.ROI, pg.RectROI)) and platform.system() == "Windows": + # on windows, edges corner pixels seem to be slightly different from + # other platforms giving a pxCount=2 for a fudge factor + if (isinstance(roi, (pg.ROI, pg.RectROI)) + and platform.system() == "Windows"): pxCount = 2 else: - pxCount=-1 - + pxCount = -1 initState = roi.getState() - + win = pg.GraphicsView() win.show() resizeWindow(win, 200, 400) - # Don't use Qt's layouts for testing--these generate unpredictable results. - # Instead, place the viewboxes manually + # Don't use Qt's layouts for testing--these generate unpredictable results. + # Instead, place the viewboxes manually vb1 = pg.ViewBox() win.scene().addItem(vb1) vb1.setPos(6, 6) @@ -65,44 +68,47 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): win.scene().addItem(vb2) vb2.setPos(6, 203) vb2.resize(188, 191) - + img1 = pg.ImageItem(border='w') img2 = pg.ImageItem(border='w') vb1.addItem(img1) vb2.addItem(img2) - + np.random.seed(0) data = np.random.normal(size=(7, 30, 31, 5)) data[0, :, :, :] += 10 data[:, 1, :, :] += 10 data[:, :, 2, :] += 10 data[:, :, :, 3] += 10 - + if transpose: data = data.transpose(0, 2, 1, 3) - + img1.setImage(data[0, ..., 0]) vb1.setAspectLocked() vb1.enableAutoRange(True, True) - + roi.setZValue(10) vb1.addItem(roi) if isinstance(roi, pg.RectROI): if transpose: - assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([28.0, 27.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ( + [28.0, 27.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) else: - assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([27.0, 28.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ( + [27.0, 28.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) - #assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) + # assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) img2.setImage(rgn[0, ..., 0]) vb2.setAspectLocked() vb2.enableAutoRange(True, True) - + app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.', pxCount=pxCount) + assertImageApproved(win, name + '/roi_getarrayregion', + 'Simple ROI region selection.', pxCount=pxCount) with pytest.raises(TypeError): roi.setPos(0, False) @@ -111,34 +117,44 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_halfpx', 'Simple ROI region selection, 0.5 pixel shift.', pxCount=pxCount) + assertImageApproved(win, name + '/roi_getarrayregion_halfpx', + 'Simple ROI region selection, 0.5 pixel shift.', + pxCount=pxCount) roi.setAngle(45) roi.setPos([3, 0]) rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.', pxCount=pxCount) + assertImageApproved(win, name + '/roi_getarrayregion_rotate', + 'Simple ROI region selection, rotation.', + pxCount=pxCount) if testResize: roi.setSize([60, 60]) rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.', pxCount=pxCount) + assertImageApproved(win, name + '/roi_getarrayregion_resize', + 'Simple ROI region selection, resized.', + pxCount=pxCount) img1.setPos(0, img1.height()) img1.setTransform(QtGui.QTransform().scale(1, -1).rotate(20), True) rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_img_trans', 'Simple ROI region selection, image transformed.', pxCount=pxCount) + assertImageApproved(win, name + '/roi_getarrayregion_img_trans', + 'Simple ROI region selection, image transformed.', + pxCount=pxCount) vb1.invertY() rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.', pxCount=pxCount) + assertImageApproved(win, name + '/roi_getarrayregion_inverty', + 'Simple ROI region selection, view inverted.', + pxCount=pxCount) roi.setState(initState) img1.setPos(0, 0) @@ -146,8 +162,11 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.', pxCount=pxCount) - + assertImageApproved( + win, name + '/roi_getarrayregion_anisotropic', + 'Simple ROI region selection, image scaled anisotropically.', + pxCount=pxCount) + # allow the roi to be re-used roi.scene().removeItem(roi) @@ -168,90 +187,156 @@ def test_mouseClickEvent(): vb.addItem(roi) app.processEvents() - mouseClick(plt, roi.mapToScene(pg.Point(2, 2)), QtCore.Qt.MouseButton.LeftButton) + mouseClick(plt, roi.mapToScene(pg.Point(2, 2)), + QtCore.Qt.MouseButton.LeftButton) + + +def test_mouseDragEventSnap(): + plt = pg.GraphicsView() + plt.show() + resizeWindow(plt, 200, 200) + vb = pg.ViewBox() + plt.scene().addItem(vb) + vb.resize(200, 200) + QtTest.QTest.qWaitForWindowExposed(plt) + QtTest.QTest.qWait(100) + + # A Rectangular roi with scaleSnap enabled + initial_x = 20 + initial_y = 20 + roi = pg.RectROI((initial_x, initial_y), (20, 20), scaleSnap=True, + translateSnap=True, snapSize=1.0, movable=True) + vb.addItem(roi) + app.processEvents() + + # Snap size roundtrip + assert roi.snapSize == 1.0 + roi.snapSize = 0.2 + assert roi.snapSize == 0.2 + roi.snapSize = 1.0 + assert roi.snapSize == 1.0 + + # Snap position check + snapped = roi.getSnapPosition(pg.Point(2.5, 3.5), snap=True) + assert snapped == pg.Point(2.0, 4.0) + + # Only drag in y direction + roi_position = roi.mapToView(pg.Point(initial_x, initial_y)) + mouseDrag(plt, roi_position, roi_position + pg.Point(0, 10), + QtCore.Qt.MouseButton.LeftButton) + assert roi.pos() == pg.Point(initial_x, 19) + + mouseDrag(plt, roi_position, roi_position + pg.Point(0, 10), + QtCore.Qt.MouseButton.LeftButton) + assert roi.pos() == pg.Point(initial_x, 18) + + # Only drag in x direction + mouseDrag(plt, roi_position, roi_position + pg.Point(10, 0), + QtCore.Qt.MouseButton.LeftButton) + assert roi.pos() == pg.Point(21, 18) def test_PolyLineROI(): rois = [ - (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=True, pen=0.3), 'closed'), - (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=False, pen=0.3), 'open') + (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=True, pen=0.3), + 'closed'), + (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=False, pen=0.3), + 'open') ] - - #plt = pg.plot() + + # plt = pg.plot() plt = pg.GraphicsView() plt.show() resizeWindow(plt, 200, 200) vb = pg.ViewBox() plt.scene().addItem(vb) vb.resize(200, 200) - #plt.plotItem = pg.PlotItem() - #plt.scene().addItem(plt.plotItem) - #plt.plotItem.resize(200, 200) - + # plt.plotItem = pg.PlotItem() + # plt.scene().addItem(plt.plotItem) + # plt.plotItem.resize(200, 200) plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. - # seemingly arbitrary requirements; might need longer wait time for some platforms.. + # seemingly arbitrary requirements; might need longer wait time for some + # platforms.. QtTest.QTest.qWaitForWindowExposed(plt) QtTest.QTest.qWait(100) - + for r, name in rois: vb.clear() vb.addItem(r) vb.autoRange() app.processEvents() - - assertImageApproved(plt, 'roi/polylineroi/'+name+'_init', 'Init %s polyline.' % name) + + assertImageApproved(plt, 'roi/polylineroi/' + name + '_init', + 'Init %s polyline.' % name) initState = r.getState() assert len(r.getState()['points']) == 3 - + # hover over center center = r.mapToScene(pg.Point(3, 3)) mouseMove(plt, center) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_roi', 'Hover mouse over center of ROI.') - + assertImageApproved(plt, 'roi/polylineroi/' + name + '_hover_roi', + 'Hover mouse over center of ROI.') + # drag ROI - mouseDrag(plt, center, center + pg.Point(10, -10), QtCore.Qt.MouseButton.LeftButton) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_roi', 'Drag mouse over center of ROI.') - + mouseDrag(plt, center, center + pg.Point(10, -10), + QtCore.Qt.MouseButton.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/' + name + '_drag_roi', + 'Drag mouse over center of ROI.') + # hover over handle pt = r.mapToScene(pg.Point(r.getState()['points'][2])) mouseMove(plt, pt) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_handle', 'Hover mouse over handle.') - + assertImageApproved(plt, 'roi/polylineroi/' + name + '_hover_handle', + 'Hover mouse over handle.') + # drag handle - mouseDrag(plt, pt, pt + pg.Point(5, 20), QtCore.Qt.MouseButton.LeftButton) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_handle', 'Drag mouse over handle.') - - # hover over segment - pt = r.mapToScene((pg.Point(r.getState()['points'][2]) + pg.Point(r.getState()['points'][1])) * 0.5) - mouseMove(plt, pt+pg.Point(0, 2)) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_segment', 'Hover mouse over diagonal segment.') - + mouseDrag(plt, pt, pt + pg.Point(5, 20), + QtCore.Qt.MouseButton.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/' + name + '_drag_handle', + 'Drag mouse over handle.') + + # hover over segment + pt = r.mapToScene((pg.Point(r.getState()['points'][2]) + pg.Point( + r.getState()['points'][1])) * 0.5) + mouseMove(plt, pt + pg.Point(0, 2)) + assertImageApproved(plt, 'roi/polylineroi/' + name + '_hover_segment', + 'Hover mouse over diagonal segment.') + # click segment mouseClick(plt, pt, QtCore.Qt.MouseButton.LeftButton) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.') + assertImageApproved(plt, 'roi/polylineroi/' + name + '_click_segment', + 'Click mouse over segment.') # drag new handle - mouseMove(plt, pt+pg.Point(10, -10)) # pg bug: have to move the mouse off/on again to register hover - mouseDrag(plt, pt, pt + pg.Point(10, -10), QtCore.Qt.MouseButton.LeftButton) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_new_handle', 'Drag mouse over created handle.') - + mouseMove(plt, pt + pg.Point(10, + -10)) + # pg bug: have to move the mouse off/on again to register hover + mouseDrag(plt, pt, pt + pg.Point(10, -10), + QtCore.Qt.MouseButton.LeftButton) + assertImageApproved(plt, + 'roi/polylineroi/' + name + '_drag_new_handle', + 'Drag mouse over created handle.') + # clear all points r.clearPoints() - assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.') + assertImageApproved(plt, 'roi/polylineroi/' + name + '_clear', + 'All points cleared.') assert len(r.getState()['points']) == 0 - + # call setPoints r.setPoints(initState['points']) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.') + assertImageApproved(plt, 'roi/polylineroi/' + name + '_setpoints', + 'Reset points to initial state.') assert len(r.getState()['points']) == 3 - + # call setState r.setState(initState) - assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.') + assertImageApproved(plt, 'roi/polylineroi/' + name + '_setstate', + 'Reset ROI to initial state.') assert len(r.getState()['points']) == 3 - + plt.hide() @@ -268,7 +353,8 @@ def test_LineROI_coords(p1, p2): pw.addItem(lineroi) # first two handles are the scale-rotate handles positioned by pos1, pos2 - for expected, (name, scenepos) in zip([p1, p2], lineroi.getSceneHandlePositions()): + for expected, (name, scenepos) in zip([p1, p2], + lineroi.getSceneHandlePositions()): got = lineroi.mapSceneToParent(scenepos) assert math.isclose(got.x(), expected[0]) assert math.isclose(got.y(), expected[1]) From e8325f65435e1434801afeda52bb4a8b1938f23d Mon Sep 17 00:00:00 2001 From: Martin Chase Date: Tue, 11 Oct 2022 04:49:06 -0700 Subject: [PATCH 099/306] also assert the visible option is set --- tests/parametertree/test_parametertypes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/parametertree/test_parametertypes.py b/tests/parametertree/test_parametertypes.py index d07c575502..382ac2e913 100644 --- a/tests/parametertree/test_parametertypes.py +++ b/tests/parametertree/test_parametertypes.py @@ -171,8 +171,10 @@ def test_checklist_show_hide(): pi.setHidden = MagicMock() p.hide() pi.setHidden.assert_called_with(True) + assert not p.opts["visible"] p.show() pi.setHidden.assert_called_with(False) + assert p.opts["visible"] def test_pen_settings(): From 2d24a336931220528cf462762809737e55d33527 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 11 Oct 2022 10:02:31 -0400 Subject: [PATCH 100/306] Fix #2482 argparse inputs were ignored --- pyqtgraph/examples/PlotSpeedTest.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/examples/PlotSpeedTest.py b/pyqtgraph/examples/PlotSpeedTest.py index fe6c60b8b9..edcbd621dc 100644 --- a/pyqtgraph/examples/PlotSpeedTest.py +++ b/pyqtgraph/examples/PlotSpeedTest.py @@ -95,7 +95,14 @@ def resetTimings(*args): fsample={'units': 'Hz'}, frequency={'units': 'Hz'} ) -def makeData(noise=True, nsamples=5000, frames=50, fsample=1000.0, frequency=0.0, amplitude=5.0): +def makeData( + noise=args.noise, + nsamples=args.nsamples, + frames=args.frames, + fsample=args.fsample, + frequency=args.frequency, + amplitude=args.amplitude, +): global data, connect_array, ptr ttt = np.arange(frames * nsamples, dtype=np.float64) / fsample data = amplitude*np.sin(2*np.pi*frequency*ttt).reshape((frames, nsamples)) From b9553d839f7b0ec63711cca34fb9759e6d32ea5c Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 11 Oct 2022 22:43:22 +0800 Subject: [PATCH 101/306] add test for LineSegments single data point --- tests/graphicsItems/test_PlotCurveItem.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/graphicsItems/test_PlotCurveItem.py b/tests/graphicsItems/test_PlotCurveItem.py index d88887cb20..57e6a082e7 100644 --- a/tests/graphicsItems/test_PlotCurveItem.py +++ b/tests/graphicsItems/test_PlotCurveItem.py @@ -1,6 +1,7 @@ import numpy as np import pyqtgraph as pg +from pyqtgraph.graphicsItems.PlotCurveItem import LineSegments from tests.image_testing import assertImageApproved @@ -35,3 +36,13 @@ def test_PlotCurveItem(): assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.") p.close() + + +def test_LineSegments(): + ls = LineSegments() + + # test the boundary case where the dataset consists of a single point + xy = np.array([0.]) + segs = ls.arrayToLineSegments(xy, xy, connect='all', finiteCheck=True) + assert isinstance(segs, tuple) and len(segs) in [1, 2] + assert len(segs[0]) == 0 From c54b5709f79720032fc41c7c53b1d16cdd36fca6 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 11 Oct 2022 16:47:31 -0400 Subject: [PATCH 102/306] also fix flickering `ActionGroup` --- .../parameterTypes/actiongroup.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/actiongroup.py b/pyqtgraph/parametertree/parameterTypes/actiongroup.py index 95b3052213..595ffb5983 100644 --- a/pyqtgraph/parametertree/parameterTypes/actiongroup.py +++ b/pyqtgraph/parametertree/parameterTypes/actiongroup.py @@ -2,26 +2,32 @@ from .action import ParameterControlledButton from .basetypes import GroupParameter, GroupParameterItem from ..ParameterItem import ParameterItem -from ...Qt import QtCore +from ...Qt import QtCore, QtWidgets class ActionGroupParameterItem(GroupParameterItem): """ - Wraps a :class:`GroupParameterItem` to manage ``bool`` parameter children. Also provides convenience buttons to - select or clear all values at once. Note these conveniences are disabled when ``exclusive`` is *True*. + Wraps a :class:`GroupParameterItem` to manage function parameters created by + an interactor. Provies a button widget which mimics an ``action`` parameter. """ def __init__(self, param, depth): - self.button = ParameterControlledButton() - super().__init__(param, depth) + self.itemWidget = QtWidgets.QWidget() + self.button = ParameterControlledButton(parent=self.itemWidget) self.button.clicked.connect(param.activate) + self.itemWidget.setLayout(layout := QtWidgets.QHBoxLayout()) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.button) + + super().__init__(param, depth) + def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) tw = self.treeWidget() if tw is None: return - tw.setItemWidget(self, 1, self.button) + tw.setItemWidget(self, 1, self.itemWidget) def optsChanged(self, param, opts): if "button" in opts: From b335acad171173a350013617c7f983ac548af53e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 12 Oct 2022 20:28:04 +0800 Subject: [PATCH 103/306] make DockDrop be a non-mixin --- pyqtgraph/dockarea/Dock.py | 33 +++++++++++++------------------- pyqtgraph/dockarea/DockArea.py | 31 +++++++++++++----------------- pyqtgraph/dockarea/DockDrop.py | 35 +++++++++++++++++++--------------- 3 files changed, 46 insertions(+), 53 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 783c51ce8d..99449e1b71 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -1,22 +1,18 @@ import warnings -from ..Qt import QT_LIB, QtCore, QtGui, QtWidgets +from ..Qt import QtCore, QtGui, QtWidgets from ..widgets.VerticalLabel import VerticalLabel from .DockDrop import DockDrop -class Dock(QtWidgets.QWidget, DockDrop): +class Dock(QtWidgets.QWidget): sigStretchChanged = QtCore.Signal() sigClosed = QtCore.Signal(object) def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, label=None, **kargs): - allowedAreas = None - if QT_LIB.startswith('PyQt'): - QtWidgets.QWidget.__init__(self, allowedAreas=allowedAreas) - else: - QtWidgets.QWidget.__init__(self) - DockDrop.__init__(self, allowedAreas=allowedAreas) + QtWidgets.QWidget.__init__(self) + self.dockdrop = DockDrop(self) self._container = None self._name = name self.area = area @@ -46,7 +42,7 @@ def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, self.widgets = [] self.currentRow = 0 #self.titlePos = 'top' - self.raiseOverlay() + self.dockdrop.raiseOverlay() self.hStyle = """ Dock > QWidget { border: 1px solid #000; @@ -113,8 +109,7 @@ def hideTitleBar(self): """ self.label.hide() self.labelHidden = True - if 'center' in self.allowedAreas: - self.allowedAreas.remove('center') + self.dockdrop.removeAllowedArea('center') self.updateStyle() def showTitleBar(self): @@ -123,7 +118,7 @@ def showTitleBar(self): """ self.label.show() self.labelHidden = False - self.allowedAreas.add('center') + self.dockdrop.addAllowedArea('center') self.updateStyle() def title(self): @@ -180,7 +175,7 @@ def updateStyle(self): def resizeEvent(self, ev): self.setOrientation() - self.resizeOverlay(self.size()) + self.dockdrop.resizeOverlay(self.size()) def name(self): return self._name @@ -195,7 +190,7 @@ def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): self.currentRow = max(row+1, self.currentRow) self.widgets.append(widget) self.layout.addWidget(widget, row, col, rowspan, colspan) - self.raiseOverlay() + self.dockdrop.raiseOverlay() def startDrag(self): self.drag = QtGui.QDrag(self) @@ -249,19 +244,17 @@ def close(self): def __repr__(self): return "" % (self.name(), self.stretch()) - ## PySide bug: We need to explicitly redefine these methods - ## or else drag/drop events will not be delivered. def dragEnterEvent(self, *args): - DockDrop.dragEnterEvent(self, *args) + self.dockdrop.dragEnterEvent(*args) def dragMoveEvent(self, *args): - DockDrop.dragMoveEvent(self, *args) + self.dockdrop.dragMoveEvent(*args) def dragLeaveEvent(self, *args): - DockDrop.dragLeaveEvent(self, *args) + self.dockdrop.dragLeaveEvent(*args) def dropEvent(self, *args): - DockDrop.dropEvent(self, *args) + self.dockdrop.dropEvent(*args) class DockLabel(VerticalLabel): diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index e97ce048de..ea98d2c88a 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -1,27 +1,24 @@ import weakref -from ..Qt import QT_LIB, QtWidgets +from ..Qt import QtWidgets from .Container import Container, HContainer, TContainer, VContainer from .Dock import Dock from .DockDrop import DockDrop -class DockArea(Container, QtWidgets.QWidget, DockDrop): +class DockArea(Container, QtWidgets.QWidget): def __init__(self, parent=None, temporary=False, home=None): Container.__init__(self, self) - allowedAreas=['left', 'right', 'top', 'bottom'] - if QT_LIB.startswith('PyQt'): - QtWidgets.QWidget.__init__(self, parent=parent, allowedAreas=allowedAreas) - else: - QtWidgets.QWidget.__init__(self, parent=parent) - DockDrop.__init__(self, allowedAreas=allowedAreas) + QtWidgets.QWidget.__init__(self, parent=parent) + self.dockdrop = DockDrop(self) + self.dockdrop.removeAllowedArea('center') self.layout = QtWidgets.QVBoxLayout() self.layout.setContentsMargins(0,0,0,0) self.layout.setSpacing(0) self.setLayout(self.layout) self.docks = weakref.WeakValueDictionary() self.topContainer = None - self.raiseOverlay() + self.dockdrop.raiseOverlay() self.temporary = temporary self.tempAreas = [] self.home = home @@ -146,7 +143,7 @@ def addContainer(self, typ, obj): #print "Add container:", new, " -> ", container if obj is not None: new.insert(obj) - self.raiseOverlay() + self.dockdrop.raiseOverlay() return new def insert(self, new, pos=None, neighbor=None): @@ -157,7 +154,7 @@ def insert(self, new, pos=None, neighbor=None): self.layout.addWidget(new) new.containerChanged(self) self.topContainer = new - self.raiseOverlay() + self.dockdrop.raiseOverlay() def count(self): if self.topContainer is None: @@ -165,7 +162,7 @@ def count(self): return 1 def resizeEvent(self, ev): - self.resizeOverlay(self.size()) + self.dockdrop.resizeOverlay(self.size()) def addTempArea(self): if self.home is None: @@ -330,19 +327,17 @@ def clear(self): for dock in docks.values(): dock.close() - ## PySide bug: We need to explicitly redefine these methods - ## or else drag/drop events will not be delivered. def dragEnterEvent(self, *args): - DockDrop.dragEnterEvent(self, *args) + self.dockdrop.dragEnterEvent(*args) def dragMoveEvent(self, *args): - DockDrop.dragMoveEvent(self, *args) + self.dockdrop.dragMoveEvent(*args) def dragLeaveEvent(self, *args): - DockDrop.dragLeaveEvent(self, *args) + self.dockdrop.dragLeaveEvent(*args) def dropEvent(self, *args): - DockDrop.dropEvent(self, *args) + self.dockdrop.dropEvent(*args) def printState(self, state=None, name='Main'): # for debugging diff --git a/pyqtgraph/dockarea/DockDrop.py b/pyqtgraph/dockarea/DockDrop.py index f93822f15b..74cd786855 100644 --- a/pyqtgraph/dockarea/DockDrop.py +++ b/pyqtgraph/dockarea/DockDrop.py @@ -3,18 +3,22 @@ from ..Qt import QtCore, QtGui, QtWidgets -class DockDrop(object): +class DockDrop: """Provides dock-dropping methods""" - def __init__(self, allowedAreas=None): - object.__init__(self) - if allowedAreas is None: - allowedAreas = ['center', 'right', 'left', 'top', 'bottom'] - self.allowedAreas = set(allowedAreas) - self.setAcceptDrops(True) + def __init__(self, dndWidget): + self.dndWidget = dndWidget + self.allowedAreas = {'center', 'right', 'left', 'top', 'bottom'} + self.dndWidget.setAcceptDrops(True) self.dropArea = None - self.overlay = DropAreaOverlay(self) + self.overlay = DropAreaOverlay(dndWidget) self.overlay.raise_() - + + def addAllowedArea(self, area): + self.allowedAreas.update(area) + + def removeAllowedArea(self, area): + self.allowedAreas.discard(area) + def resizeOverlay(self, size): self.overlay.resize(size) @@ -34,18 +38,19 @@ def dragMoveEvent(self, ev): #print "drag move" # QDragMoveEvent inherits QDropEvent which provides posF() # PyQt6 provides only position() + width, height = self.dndWidget.width(), self.dndWidget.height() posF = ev.posF() if hasattr(ev, 'posF') else ev.position() ld = posF.x() - rd = self.width() - ld + rd = width - ld td = posF.y() - bd = self.height() - td + bd = height - td mn = min(ld, rd, td, bd) if mn > 30: self.dropArea = "center" - elif (ld == mn or td == mn) and mn > self.height()/3.: + elif (ld == mn or td == mn) and mn > height/3: self.dropArea = "center" - elif (rd == mn or ld == mn) and mn > self.width()/3.: + elif (rd == mn or ld == mn) and mn > width/3: self.dropArea = "center" elif rd == mn: @@ -57,7 +62,7 @@ def dragMoveEvent(self, ev): elif bd == mn: self.dropArea = "bottom" - if ev.source() is self and self.dropArea == 'center': + if ev.source() is self.dndWidget and self.dropArea == 'center': #print " no self-center" self.dropArea = None ev.ignore() @@ -80,7 +85,7 @@ def dropEvent(self, ev): return if area == 'center': area = 'above' - self.area.moveDock(ev.source(), area, self) + self.dndWidget.area.moveDock(ev.source(), area, self.dndWidget) self.dropArea = None self.overlay.setDropArea(self.dropArea) From 12987dfdd04dbb4e678ad6540b7efa21f06acf0f Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 12 Oct 2022 14:29:38 -0400 Subject: [PATCH 104/306] Reflect new host for s3a repo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 096b09c65d..7b592e6ff2 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,6 @@ Here is a partial listing of some of the applications that make use of PyQtGraph * [PyMeasure](https://github.com/pymeasure/pymeasure) * [PySpectra](http://hasyweb.desy.de/services/computing/Spock/node138.html) * [rapidtide](https://rapidtide.readthedocs.io/en/latest/) -* [Semi-Supervised Semantic Annotator](https://gitlab.com/ficsresearch/s3ah) +* [Semi-Supervised Semantic Annotator](https://gitlab.com/s3a/s3a) Do you use PyQtGraph in your own project, and want to add it to the list? Submit a pull request to update this listing! From 1c8ca39c41250b9fd4c757d2e97d835de1044643 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 12 Oct 2022 14:32:51 -0400 Subject: [PATCH 105/306] Let binder URL point to full notebook rather than single page view Suggested by StackOverflow user https://stackoverflow.com/users/8508004/wayne Relevant SO post: https://stackoverflow.com/a/74028478/9463643 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b592e6ff2..62bb4b3192 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Through 3rd part libraries, additional functionality may be added to PyQtGraph, | [`matplotlib`] |
  • Export of PlotItem in matplotlib figure
  • Add matplotlib collection of colormaps
| | [`cupy`] |
  • CUDA-enhanced image processing
  • Note: On Windows, CUDA toolkit must be >= 11.1
| | [`numba`] |
  • Faster image processing
| -| [`jupyter_rfb`]|
  • Jupyter Notebook support
  • [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pyqtgraph/pyqtgraph/HEAD?labpath=pyqtgraph%2Fexamples%2Fnotebooks)
| +| [`jupyter_rfb`]|
  • Jupyter Notebook support
  • [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pyqtgraph/pyqtgraph/HEAD?urlpath=%2Flab%2Ftree%2Fpyqtgraph%2Fexamples%2Fnotebooks)
| [`scipy`]: https://github.com/scipy/scipy [`ndimage`]: https://docs.scipy.org/doc/scipy/reference/ndimage.html From 00276660566c15c9e663c780a4ed55aa9873d438 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Mon, 10 Oct 2022 10:06:54 -0700 Subject: [PATCH 106/306] Add doc2dash build target --- .gitignore | 1 + doc/Makefile | 16 ++++++++++- doc/source/_static/custom.css | 2 -- doc/source/_static/dash.css | 24 +++++++++++++++++ .../peegee_03_square_no_bg_32_cleaned.png | Bin 0 -> 651 bytes doc/source/conf.py | 25 +++++++++++++++--- 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 doc/source/_static/dash.css create mode 100644 doc/source/_static/peegee_03_square_no_bg_32_cleaned.png diff --git a/.gitignore b/.gitignore index 7309261d14..19b29428e2 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,7 @@ coverage.xml *.tiff *.tif *.png +!doc/source/**/*.png *.dat *.DAT diff --git a/doc/Makefile b/doc/Makefile index 2167f27cd5..e4eb7f8b56 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -13,7 +13,7 @@ PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest livehtml dash help: @echo "Please use \`make ' where is one of" @@ -48,6 +48,20 @@ html: @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dash: clean + # this will create a docset of the documentation, used in documentation + # viewers such as dash or zeal, requires doc2dash to be installed + export BUILD_DASH_DOCSET=1;\ + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/dash + doc2dash -n pyqtgraph\ + --online-redirect-url https://pyqtgraph.readthedocs.io/en/latest\ + --icon $(SOURCEDIR)/_static/peegee_03_square_no_bg_32_cleaned.png\ + --force\ + -A\ + $(BUILDDIR)/dash + @echo + dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css index 9adf36c46b..4a7219c7ae 100644 --- a/doc/source/_static/custom.css +++ b/doc/source/_static/custom.css @@ -74,7 +74,6 @@ Nat Methods 8, 441 (2011). https://doi.org/10.1038/nmeth.1618 margin: 0px; } - .sd-card .sd-card-img-top { height: 52px; width: 52px; @@ -127,7 +126,6 @@ Nat Methods 8, 441 (2011). https://doi.org/10.1038/nmeth.1618 color: #484848; } - /* Dark theme tweaking Matplotlib images are in png and inverted while other output diff --git a/doc/source/_static/dash.css b/doc/source/_static/dash.css new file mode 100644 index 0000000000..5ddb6f5786 --- /dev/null +++ b/doc/source/_static/dash.css @@ -0,0 +1,24 @@ +/* Slight Modification When Building Dash Docset + +Dash windows can be of a variety of sizes, no need to restrict to fixed +width. To be used with sphinx-pydata-theme + +*/ + +.bd-header { + display: none; +} + +.bd-sidebar-primary { + display: none; +} + +.bd-main .bd-content .bd-article-container { + flex-grow: 1; + max-width: 100%; +} + +.bd-container .bd-container__inner { + flex-grow: 1; + max-width: 100%; +} diff --git a/doc/source/_static/peegee_03_square_no_bg_32_cleaned.png b/doc/source/_static/peegee_03_square_no_bg_32_cleaned.png new file mode 100644 index 0000000000000000000000000000000000000000..0683368e083335f9f9525d480d25ecf2012b10da GIT binary patch literal 651 zcmV;60(AX}P)Px%LrFwIR9HvFmc2^@K@i1fF@<1fpH6I4P_Rm8DVRz@!9q~bf5Cu7Dr0A16;TmE zP*716EClTYK`>%$L?n>jP7pt`@;1&gx85apXHK`tUAWu#oA+jxQ}mBZm&djG4}A!; z?wJtZmxYk5&nJf;N#2g4Co)`ecKRz|3+J4q2 zt4{YqWWE z`@?#B5(40)dQBtgL5d3k2;R^S#%(-L1ORaHqky;$Kukh361P(zU5Mg_6o(KN#2KQQ zc;FZ^Q1-0Qus68zzJ)zZ zqnDm3fG~s1>}Py8cwo17dOI+&tnp zQd~VUXOPGam=`7sn$o>Q)iTj`uBnaA_?hS91F$%w0|F}mL{`nO`282w2}SeA^(KTc zT;OZ`%KPn{Z8&$#;#OZj2?4;?!wpm}q2&Ei))S9k6k#j3=YYvSMDFg!zp-8J)wJQ{ z!yTt&&y1Jh)_l-ffc66EwtjD&QUv0kC8F0{YlLkCyfKSI1WVrIItg*pgwj@5M7q){ l%iUg5ERvK!v#Iz3rKF$0jnwqB00000NkvXXu0mjf003$MDlGs2 literal 0 HcmV?d00001 diff --git a/doc/source/conf.py b/doc/source/conf.py index 8101f398d0..b99610ef75 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -147,6 +147,15 @@ twitter_url="https://twitter.com/pyqtgraph", ) +if os.getenv("BUILD_DASH_DOCSET"): + html_theme_options |= { + 'page_sidebar_items': [], + "show_prev_next": False, + "collapse_navigation": True, + "use_edit_page_button": False + } + + # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -176,6 +185,9 @@ "custom.css", ] +if os.getenv("BUILD_DASH_DOCSET"): + html_css_files.append("dash.css") + # Redirects for pages that were moved to new locations rediraffe_redirects = { "3dgraphics/glaxisitem.rst": "api_reference/3dgraphics/glaxisitem.rst", @@ -310,10 +322,15 @@ #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -html_sidebars = { - "**": ["sidebar-nav-bs.html", "sidebar-ethical-ads.html"], - 'index': [] # don't show sidebar on main landing page -} +if os.getenv("BUILD_DASH_DOCSET"): # used for building dash docsets + html_sidebars = { + "**": [] + } +else: + html_sidebars = { + "**": ["sidebar-nav-bs.html", "sidebar-ethical-ads.html"], + 'index': [] # don't show sidebar on main landing page + } # Additional templates that should be rendered to pages, maps page names to # template names. From 3d29133b100626df5bed3f5adfb8bc781b74ecfc Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Mon, 10 Oct 2022 14:20:50 -0700 Subject: [PATCH 107/306] Setup favicon --- .../peegee_03_square_no_bg_32_cleaned.ico | Bin 0 -> 4286 bytes .../peegee_04_square_no_bg_180_cleaned.png | Bin 0 -> 4425 bytes doc/source/conf.py | 36 +++++++++++++----- doc/source/index.rst | 2 + 4 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 doc/source/_static/peegee_03_square_no_bg_32_cleaned.ico create mode 100644 doc/source/_static/peegee_04_square_no_bg_180_cleaned.png diff --git a/doc/source/_static/peegee_03_square_no_bg_32_cleaned.ico b/doc/source/_static/peegee_03_square_no_bg_32_cleaned.ico new file mode 100644 index 0000000000000000000000000000000000000000..2e084f022017574440d507b33cc94c2d654ff668 GIT binary patch literal 4286 zcmd6oy-piZ5QS&u!bpjLiZZ4Oyg^EnB2Rz%6dGt~Q+ z-5a@%mQ=1@=}5h-0@yTY@fWrIPkrd^If%oY&vJ>Yy5q( z{45xcGlNO@?AeUD_fQLUpw4GQ4}MsiBu`_K{qe3lv+qQfF7ncR)(o=xPI51e&H6kw z>^wY`@*XfR?04t>ap?E6%o$L_?vHpxLmBUYXQsdBMbPl_JI!|hU*;SJ@5AXRo4+U; zLLEFS^YAzab^EWqY#&lny}h&Kw`#Mex_Mex*|^rq-!nb#{L=W%p1%IJr%|`2QEx@t wt5tnEjIPx`0ZBwbRCr$PoxiVTHxbA0>yVZvZ0ZyV1ww%m38c`aKnhBcVj~KKLIV;tN)akp zi9a9`p#af^Eh2;x2?-<$B7uaUfheMY{R^^e!!~IynzL{2o8xDD#^bS%ea^gQ-@fZ# zGydGUUypr!uTK8s9@vowu6AULn{qr4obNt!XR-Rx<0lKt4)pOL*o(zyFyv=H`qJsG zw;wvtdzsaP+OfcV{^;If{% zH6Vm}(abMCIDw9K4Si53W;GjO{^Y|~E+#zu&%aM!`Od#CF#Do#*4aJSxB+IA7}WGo zZaB~f17kLGV-f>xfav_ftAAg~4F`I*89vZ>V-ka!ACnsn^nn1G$<&m@pwapFfBSTm z-N4s;V`Zk9-uYN_5+jVxF}dLpdgp-5WF99mByz)no@vH+JXTI(gfOS%h6BB0NM
  • 48>JnF3uJ zogHWgR^?TP^gM9& z9_gkqgRwQ44cE}UaT&Y6&0x0F&gf##a*PdI^U?L#{Yx2Pshu6@=BSL{!Im&vYi9>~ zeBPRla0HmaK)3}7esWK8mpH;DM~Sy~QnvRFVCXiK8djMNUli^HOpO^^!yGOIEu%BE zcAzH(Q#tHS({ngxivvhG`#nM{>6on=MCaaH+9~a9wTTj&0|$CG zGIirU!Gn$>8v5Okmi zgwJ|NqrlucO&9vjHMBKJW3)F!MuWoKav|sudJO1Vjbb>Mdq@p*rbp;jSZ&nq2)Ki4$vs;0!@}Gv=j;)BN63$q(HKE$ zcpiFgxX&>B^GElFumV$bTS^TFdI%KC2Qg|!17U`LtsZzS_fBWQt{I@$x$(egFmIir za}7Q5yJKXSd!E(crqHt-oQDQ@ePozNks22beSYJ;^VKh3bFc>hH#US3VIEOxtbks! z-wyR~NRJNlNK(Ur9uD92fJTRTG^ue5Xphpv5WegHK6?CQm^?FjCd@Z)ouZ3oIY2{S z=yut$CDj*@x4v>9<%F%yIG0Sy6$RR(bcyTi%HlRQw)ocD4^J}ePNo7kB zm=|0sn>DZQK%2X?ticdw3N`#b0@yQ1ji`q92weuFMd=*S@I8SLt>I_OYrIQ6Duz zZ*^E7qcx=WmN|~xT&5uRd!jTHEiruv`_JluqHfH~_uNsU`TOJoNZQ6;cS%9Louk;`f}Owb+* z;_^7w`SM`V>DkYHRtz<^6Q11r>#x3Wab53A$y?6ZtmeaC0nRS%>zf&qlO2;wk|i3JQ-9RlbjLPbvG zp%E=~ywpLZ`_4ffN)9GEmzpzO`6+-#;3x5JvE&HlMhG<6oy$j4$>Bh6 zNC|h=nnvweqO>+bFINUJ8TW6_z5 z(6rBM{k(jrZ8)`?b-lUKlKVTbOa;)FgAjPQC*^VY_!hvh96G|E+6PoQpt#|H`borJVL{8BpWRp)U_Y`&EL71 z4swe^X>^9>k1jWch|oCC9I&a^r3dIC3c_Xwo-xMXOE*ATfN!zo?Q-Mk%nT8srH}yB zR7cZ{Likh%Z4(dTTaB4KCrm`TfR7NLLf+Mk!q z!*}2NTG1F#dLY!otc27bVYyW{Bi9Jrv=3*C5n%fGlr#&Hi_%wDSC_}H!uGJ<-cHVG zCanl2O$T8e1UdYBDcBZcv2G*WRKch#{aT6e(*{3ba?cUE)L74ASD;fcfp_y@c)!-p+`<YOOL!H|44x=tS$M^gTpRb-zhdJ&-7HA4$ zO$3br4ef=0uj!+j5@iFqBcoIj-~tpR$o)4yRfA36B(9qaB!`vWO~H&q*#Xq&*sVk{ zeH&&~N+LF0Xf|F9E8p7NS4&1{P9l^`nslF8_VWzWfh^dK!Y1X0869u3{d9| zLKQ^O)dDiig2Xmq3fi8Z$Vv+?K<8kluu4Fclu3Lkg<2TmwJsqRCr3b<`>jLMhZ-J26i-iqb21VRNG7mLmii z&sj)7M%$#ILb*^!yM~s(*OEX>A>^Qzwi6E73h3HLv*^Kn4ai`Z6y|FmoL_(StEYGG zKeuwpk#9qi<9s?rf!081y?tG?I4p%Y_p}Q=^;Qy*bCDTd%SBy@)-IXkBb6Qca8PNK zMv%kH3+aT$;HA*gvbE%l z9q1ha4W>{%{lh=Lys}P{eUQ0_FU3V=O{&i$H1GFRiSE%%Kh&pCBg|VuOe1M*XR&lk zJoJeJt*2xvp!H_j*UV)xG*LO!;^EyJE;(d#SO@f*@85i1FZTD-Z`QRb8~3KAYEp8N zfqR6Ocqv{+4_^#s+Q&}8&ZX4AdKtfrpvY=-#v}BoK*P9p5~+K^ z3@(fH1$Ri#l4jN8SYNYDa+E~p9YyNi5+JtwS(3-D$70?VPh^l=Jo3q9Q_RhV0tSuOy>{Bvrf#wCXgavo?6 zoGl`?MDkd?VQ&0(>$(B@FssbH@=~cSbl*^s`;cEXYhcFyt)P~GY`g%CEy}il8O(B^ zM*$kfQS*k#)#q%ih}6Avz}(>`mm1t;uU}6~j#?NwAUWtH5#C~qxBZp8*{IC5`O>)~ zrx=;xUkAF(>urJ7LM?%r+G(^<45MYMkNaFyrZNM5x7bQALt^gIaiFaupA=AOxtPyw zpuR-smN2Kj6|45x>D(yVjQ%#z)-X#JiJUE*JjwMTtVek%ZXW2NtHfKB{OTo~Wh&pPYzpi5w;X0={$xzy+j(MY|if!4x|#;wNajvF{O5mHaR|?at|y((`q>gECH0fIiQRLO36-O zxof<}j2^fJbZ7(s#epmZFXbSvea)Lp@DmMuaDz_C2hxj+{bivOA|`RgJW@+Th7WHZAbIa zBYd2fKI!`x#g=ILW-ps5=v+z#P3J P002ovPDHLkV1fVuOyg|- literal 0 HcmV?d00001 diff --git a/doc/source/conf.py b/doc/source/conf.py index b99610ef75..e5e6aee365 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -40,7 +40,7 @@ "sphinx_qt_documentation", "sphinx_design", "sphinxext.rediraffe", - "sphinxcontrib.images" + "sphinxcontrib.images", ] # Add any paths that contain templates here, relative to this directory. @@ -141,18 +141,36 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = dict( - github_url="https://github.com/pyqtgraph/pyqtgraph", - navbar_end=["theme-switcher", "navbar-icon-links"], - twitter_url="https://twitter.com/pyqtgraph", -) +html_theme_options = { + "favicons": [ + { + "rel": "icon", + "sizes": "32x32", + "href": "peegee_03_square_no_bg_32_cleaned.png" + }, + { + "rel": "icon", + "href": "peegee_03_square_no_bg_32_cleaned.ico", + "sizes": "any" + }, + { + "rel": "apple-touch-icon", + "href": "peegee_04_square_no_bg_180_cleaned.png" + }, + ], + "github_url": "https://github.com/pyqtgraph/pyqtgraph", + "navbar_end": ["theme-switcher", "navbar-icon-links"], + "twitter_url": "https://twitter.com/pyqtgraph", + "use_edit_page_button": False, + "page_sidebar_items": ["page-toc"] +} + if os.getenv("BUILD_DASH_DOCSET"): html_theme_options |= { 'page_sidebar_items': [], "show_prev_next": False, "collapse_navigation": True, - "use_edit_page_button": False } @@ -173,7 +191,7 @@ # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = "_static/peegee_03_square_no_bg_32_cleaned.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -346,7 +364,7 @@ #html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True diff --git a/doc/source/index.rst b/doc/source/index.rst index 37b8069553..6d2ece9a94 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,3 +1,5 @@ +:html_theme.sidebar_secondary.remove: + PyQtGraph ========= From fd08a9e4c3cefb366276db675344e2cb6eb2522f Mon Sep 17 00:00:00 2001 From: bbc131 <36670201+bbc131@users.noreply.github.com> Date: Thu, 13 Oct 2022 08:29:57 +0200 Subject: [PATCH 108/306] Fix `ViewBox.autoRange()` for case of data with constant y-value --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0b6961bf4b..6ad9d56952 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -593,7 +593,9 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru # If we requested 0 range, try to preserve previous scale. # Otherwise just pick an arbitrary scale. + preserve = False if mn == mx: + preserve = True dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0] if dy == 0: dy = 1 @@ -613,13 +615,14 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru raise Exception("Cannot set range [%s, %s]" % (str(mn), str(mx))) # Apply padding - if padding is None: - xpad = self.suggestPadding(ax) - else: - xpad = padding - p = (mx-mn) * xpad - mn -= p - mx += p + if not preserve: + if padding is None: + xpad = self.suggestPadding(ax) + else: + xpad = padding + p = (mx-mn) * xpad + mn -= p + mx += p # max range cannot be larger than bounds, if they are given if limits[ax][0] is not None and limits[ax][1] is not None: From a1182d9fe9c60db3d6341c4634f09cc78d568f1e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 12 Oct 2022 22:57:56 +0800 Subject: [PATCH 109/306] PlotSpeedTest: reflect initial use_opengl state --- pyqtgraph/examples/PlotSpeedTest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/examples/PlotSpeedTest.py b/pyqtgraph/examples/PlotSpeedTest.py index edcbd621dc..2d7d09e3e2 100644 --- a/pyqtgraph/examples/PlotSpeedTest.py +++ b/pyqtgraph/examples/PlotSpeedTest.py @@ -35,6 +35,7 @@ if args.use_opengl is not None: pg.setConfigOption('useOpenGL', args.use_opengl) pg.setConfigOption('enableExperimental', args.use_opengl) +use_opengl = pg.getConfigOption('useOpenGL') # don't limit frame rate to vsync sfmt = QtGui.QSurfaceFormat() @@ -151,11 +152,11 @@ def updateOptions( curvePen=pg.mkPen(), plotMethod='pyqtgraph', fillLevel=False, - enableExperimental=False, - useOpenGL=False, + enableExperimental=use_opengl, + useOpenGL=use_opengl, ): pg.setConfigOption('enableExperimental', enableExperimental) - pg.setConfigOption('useOpenGL', useOpenGL) + pw.useOpenGL(useOpenGL) curve.setPen(curvePen) curve.setFillLevel(0.0 if fillLevel else None) curve.setMethod(plotMethod) From c4255c3f0827a9947bf855d6f7067f4bb2a14fe5 Mon Sep 17 00:00:00 2001 From: Zhengyu Peng Date: Fri, 14 Oct 2022 09:05:42 -0400 Subject: [PATCH 110/306] Add link of Antenna Array Analysis tool --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 62bb4b3192..c7e10a3629 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Used By Here is a partial listing of some of the applications that make use of PyQtGraph! * [ACQ4](https://github.com/acq4/acq4) +* [Antenna Array Analysis](https://github.com/rookiepeng/antenna-array-analysis) * [argos](https://github.com/titusjan/argos) * [Atomize](https://github.com/Anatoly1010/Atomize) * [EnMAP-Box](https://enmap-box.readthedocs.io) From 965e2e5358039c8714605936cb7c378149d9870e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 15 Oct 2022 08:46:28 +0800 Subject: [PATCH 111/306] fix: ndarray_from_qimage does not hold a reference to qimage --- tests/parametertree/test_Parameter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index fe478ec684..7dbb9b246f 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -467,7 +467,9 @@ def a(): groupItem = parent.child("a").itemClass(parent.child("a"), 1) buttonPixmap = groupItem.button.icon().pixmap(randomPixmap.size()) - imageBytes = [ - fn.ndarray_from_qimage(pix.toImage()) for pix in (randomPixmap, buttonPixmap) - ] + + # hold references to the QImages + images = [ pix.toImage() for pix in (randomPixmap, buttonPixmap) ] + + imageBytes = [ fn.ndarray_from_qimage(img) for img in images ] assert np.array_equal(*imageBytes) From 2ae77165aea17d517aee0abe920d62cdd86ad299 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 26 Oct 2022 19:35:41 +0800 Subject: [PATCH 112/306] instantiate QMenu with a parent menu.addMenu(submenu) does not take ownership of "submenu" --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 49db63ef3a..edb0c84692 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -201,7 +201,7 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None self.ctrlMenu.setTitle(translate("PlotItem", 'Plot Options')) self.subMenus = [] for name, grp in menuItems: - sm = QtWidgets.QMenu(name) + sm = QtWidgets.QMenu(name, self.ctrlMenu) act = QtWidgets.QWidgetAction(self) act.setDefaultWidget(grp) sm.addAction(act) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index a0804d4b26..976c6eb4e9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -24,7 +24,7 @@ def __init__(self, view): self.widgetGroups = [] self.dv = QtGui.QDoubleValidator(self) for axis in 'XY': - m = QtWidgets.QMenu() + m = QtWidgets.QMenu(self) m.setTitle(f"{axis} {translate('ViewBox', 'axis')}") w = QtWidgets.QWidget() ui = ui_template.Ui_Form() @@ -60,7 +60,7 @@ def __init__(self, view): #self.setExportMethods(view.exportMethods) #self.addMenu(self.export) - self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode")) + self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode"), self) group = QtGui.QActionGroup(self) # This does not work! QAction _must_ be initialized with a permanent From e4688251d9a2510a05b1012c9e1e9ff5d4518b2f Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 26 Oct 2022 19:38:46 +0800 Subject: [PATCH 113/306] delete ExportMethods getter and setter self.export and self.exportMethods are not assigned to, so calling either of those methods would have raised an exception. --- pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 976c6eb4e9..cbee671f89 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -55,10 +55,6 @@ def __init__(self, view): self.ctrl[0].invertCheck.toggled.connect(self.xInvertToggled) self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled) - ## exporting is handled by GraphicsScene now - #self.export = QtWidgets.QMenu("Export") - #self.setExportMethods(view.exportMethods) - #self.addMenu(self.export) self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode"), self) group = QtGui.QActionGroup(self) @@ -85,13 +81,6 @@ def __init__(self, view): self.updateState() - def setExportMethods(self, methods): - self.exportMethods = methods - self.export.clear() - for opt, fn in methods.items(): - self.export.addAction(opt, self.exportMethod) - - def viewStateChanged(self): self.valid = False if self.ctrl[0].minText.isVisible() or self.ctrl[1].minText.isVisible(): @@ -210,10 +199,6 @@ def yInvertToggled(self, b): def xInvertToggled(self, b): self.view().invertX(b) - def exportMethod(self): - act = self.sender() - self.exportMethods[str(act.text())]() - def set3ButtonMode(self): self.view().setLeftButtonAction('pan') From ae0afa5da2be9acf977ec4b7db674fb749a4488a Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 26 Oct 2022 21:08:18 +0800 Subject: [PATCH 114/306] pass if underlying C++ QObject is no longer alive --- tests/test_ref_cycles.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_ref_cycles.py b/tests/test_ref_cycles.py index f602116e01..228e4587db 100644 --- a/tests/test_ref_cycles.py +++ b/tests/test_ref_cycles.py @@ -13,7 +13,10 @@ def assert_alldead(refs): for ref in refs: - assert ref() is None + obj = ref() + assert obj is None or not ( + isinstance(obj, pg.QtCore.QObject) and pg.Qt.isQObjectAlive(obj) + ) def qObjectTree(root): """Return root and its entire tree of qobject children""" From 8acacfdce2b85eef5a54fe1e1db0aa8d5ec75eab Mon Sep 17 00:00:00 2001 From: Jaime R <38530589+Jaime02@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:55:19 +0200 Subject: [PATCH 115/306] Remove old unused mains (#2490) * Remove old unused mains This main entry points are old and worked for testing purposes. These should be removed, they are no longer needed * Remove unused imports --- pyqtgraph/configfile.py | 22 ---- pyqtgraph/metaarray/MetaArray.py | 166 ---------------------------- pyqtgraph/widgets/FeedbackButton.py | 21 ---- pyqtgraph/widgets/JoystickButton.py | 19 +--- pyqtgraph/widgets/VerticalLabel.py | 19 ---- 5 files changed, 1 insertion(+), 246 deletions(-) diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 89004d9e3e..9f33d42b88 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -12,7 +12,6 @@ import os import re import sys -import tempfile from collections import OrderedDict import numpy @@ -193,24 +192,3 @@ def measureIndent(s): while n < len(s) and s[n] == ' ': n += 1 return n - -if __name__ == '__main__': - cf = """ -key: 'value' -key2: ##comment - ##comment - key21: 'value' ## comment - ##comment - key22: [1,2,3] - key23: 234 #comment - """ - with tempfile.NamedTemporaryFile(encoding="utf-8") as tf: - tf.write(cf.encode("utf-8")) - print("=== Test:===") - for num, line in enumerate(cf.split('\n'), start=1): - print("%02d %s" % (num, line)) - print(cf) - print("============") - data = readConfigFile(tf.name) - print(data) - diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 000b55c6fd..fd6a0bcd11 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -1199,169 +1199,3 @@ def writeCsv(self, fileName=None): file.close() else: return ret - - -if __name__ == '__main__': - ## Create an array with every option possible - - arr = np.zeros((2, 5, 3, 5), dtype=int) - for i in range(arr.shape[0]): - for j in range(arr.shape[1]): - for k in range(arr.shape[2]): - for l in range(arr.shape[3]): - arr[i,j,k,l] = (i+1)*1000 + (j+1)*100 + (k+1)*10 + (l+1) - - info = [ - axis('Axis1'), - axis('Axis2', values=[1,2,3,4,5]), - axis('Axis3', cols=[ - ('Ax3Col1'), - ('Ax3Col2', 'mV', 'Axis3 Column2'), - (('Ax3','Col3'), 'A', 'Axis3 Column3')]), - {'name': 'Axis4', 'values': np.array([1.1, 1.2, 1.3, 1.4, 1.5]), 'units': 's'}, - {'extra': 'info'} - ] - - ma = MetaArray(arr, info=info) - - print("==== Original Array =======") - print(ma) - print("\n\n") - - #### Tests follow: - - - #### Index/slice tests: check that all values and meta info are correct after slice - print("\n -- normal integer indexing\n") - - print("\n ma[1]") - print(ma[1]) - - print("\n ma[1, 2:4]") - print(ma[1, 2:4]) - - print("\n ma[1, 1:5:2]") - print(ma[1, 1:5:2]) - - print("\n -- named axis indexing\n") - - print("\n ma['Axis2':3]") - print(ma['Axis2':3]) - - print("\n ma['Axis2':3:5]") - print(ma['Axis2':3:5]) - - print("\n ma[1, 'Axis2':3]") - print(ma[1, 'Axis2':3]) - - print("\n ma[:, 'Axis2':3]") - print(ma[:, 'Axis2':3]) - - print("\n ma['Axis2':3, 'Axis4':0:2]") - print(ma['Axis2':3, 'Axis4':0:2]) - - - print("\n -- column name indexing\n") - - print("\n ma['Axis3':'Ax3Col1']") - print(ma['Axis3':'Ax3Col1']) - - print("\n ma['Axis3':('Ax3','Col3')]") - print(ma['Axis3':('Ax3','Col3')]) - - print("\n ma[:, :, 'Ax3Col2']") - print(ma[:, :, 'Ax3Col2']) - - print("\n ma[:, :, ('Ax3','Col3')]") - print(ma[:, :, ('Ax3','Col3')]) - - - print("\n -- axis value range indexing\n") - - print("\n ma['Axis2':1.5:4.5]") - print(ma['Axis2':1.5:4.5]) - - print("\n ma['Axis4':1.15:1.45]") - print(ma['Axis4':1.15:1.45]) - - print("\n ma['Axis4':1.15:1.25]") - print(ma['Axis4':1.15:1.25]) - - - - print("\n -- list indexing\n") - - print("\n ma[:, [0,2,4]]") - print(ma[:, [0,2,4]]) - - print("\n ma['Axis4':[0,2,4]]") - print(ma['Axis4':[0,2,4]]) - - print("\n ma['Axis3':[0, ('Ax3','Col3')]]") - print(ma['Axis3':[0, ('Ax3','Col3')]]) - - - - print("\n -- boolean indexing\n") - - print("\n ma[:, array([True, True, False, True, False])]") - print(ma[:, np.array([True, True, False, True, False])]) - - print("\n ma['Axis4':array([True, False, False, False])]") - print(ma['Axis4':np.array([True, False, False, False])]) - - - - - - #### Array operations - # - Concatenate - # - Append - # - Extend - # - Rowsort - - - - - #### File I/O tests - - print("\n================ File I/O Tests ===================\n") - tf = 'test.ma' - # write whole array - - print("\n -- write/read test") - ma.write(tf) - ma2 = MetaArray(file=tf) - - #print ma2 - print("\nArrays are equivalent:", (ma == ma2).all()) - #print "Meta info is equivalent:", ma.infoCopy() == ma2.infoCopy() - os.remove(tf) - - # CSV write - - # append mode - - - print("\n================append test (%s)===============" % tf) - ma['Axis2':0:2].write(tf, appendAxis='Axis2') - for i in range(2,ma.shape[1]): - ma['Axis2':[i]].write(tf, appendAxis='Axis2') - - ma2 = MetaArray(file=tf) - - #print ma2 - print("\nArrays are equivalent:", (ma == ma2).all()) - #print "Meta info is equivalent:", ma.infoCopy() == ma2.infoCopy() - - os.remove(tf) - - - - ## memmap test - print("\n==========Memmap test============") - ma.write(tf, mappable=True) - ma2 = MetaArray(file=tf, mmap=True) - print("\nArrays are equivalent:", (ma == ma2).all()) - os.remove(tf) - diff --git a/pyqtgraph/widgets/FeedbackButton.py b/pyqtgraph/widgets/FeedbackButton.py index 7e100f7edd..9e7e6d2aa8 100644 --- a/pyqtgraph/widgets/FeedbackButton.py +++ b/pyqtgraph/widgets/FeedbackButton.py @@ -139,24 +139,3 @@ def setStyleSheet(self, style=None, temporary=False): QtWidgets.QPushButton.setStyleSheet(self, style) if not temporary: self.origStyle = style - - -if __name__ == '__main__': - import time - app = QtWidgets.QApplication([]) # noqa: qapp stored to avoid gc - win = QtWidgets.QMainWindow() - btn = FeedbackButton("Button") - fail = True - def click(): - btn.processing("Hold on..") - time.sleep(2.0) - - global fail - fail = not fail - if fail: - btn.failure(message="FAIL.", tip="There was a failure. Get over it.") - else: - btn.success(message="Bueno!") - btn.clicked.connect(click) - win.setCentralWidget(btn) - win.show() diff --git a/pyqtgraph/widgets/JoystickButton.py b/pyqtgraph/widgets/JoystickButton.py index 3ae78770ce..5f79f8bdce 100644 --- a/pyqtgraph/widgets/JoystickButton.py +++ b/pyqtgraph/widgets/JoystickButton.py @@ -1,6 +1,6 @@ from math import hypot -from ..Qt import QtCore, QtGui, QtWidgets, mkQApp +from ..Qt import QtCore, QtGui, QtWidgets __all__ = ['JoystickButton'] @@ -84,20 +84,3 @@ def paintEvent(self, ev): def resizeEvent(self, ev): self.setState(*self.state) super().resizeEvent(ev) - - - -if __name__ == '__main__': - app = mkQApp() - w = QtWidgets.QMainWindow() - b = JoystickButton() - w.setCentralWidget(b) - w.show() - w.resize(100, 100) - - def fn(b, s): - print("state changed:", s) - - b.sigStateChanged.connect(fn) - - app.exec() if hasattr(app, 'exec') else app.exec_() diff --git a/pyqtgraph/widgets/VerticalLabel.py b/pyqtgraph/widgets/VerticalLabel.py index c1ac9110cf..9ac41dee8b 100644 --- a/pyqtgraph/widgets/VerticalLabel.py +++ b/pyqtgraph/widgets/VerticalLabel.py @@ -80,22 +80,3 @@ def sizeHint(self): return QtCore.QSize(self.hint.width(), self.hint.height()) else: return QtCore.QSize(50, 19) - - -if __name__ == '__main__': - app = QtWidgets.QApplication([]) # noqa: qapplication must be stored to variable to avoid gc - win = QtWidgets.QMainWindow() - w = QtWidgets.QWidget() - l = QtWidgets.QGridLayout() - w.setLayout(l) - - l1 = VerticalLabel("text 1", orientation='horizontal') - l2 = VerticalLabel("text 2") - l3 = VerticalLabel("text 3") - l4 = VerticalLabel("text 4", orientation='horizontal') - l.addWidget(l1, 0, 0) - l.addWidget(l2, 1, 1) - l.addWidget(l3, 2, 2) - l.addWidget(l4, 3, 3) - win.setCentralWidget(w) - win.show() From 47e9b149ac91db6a48ccb1b385959b3ec1ecaca3 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 27 Oct 2022 03:26:25 +0800 Subject: [PATCH 116/306] fix wrong logic for assert_alldead the previous logic would have wrongly passed non-QObjects. --- tests/test_ref_cycles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ref_cycles.py b/tests/test_ref_cycles.py index 228e4587db..8d09bf00ad 100644 --- a/tests/test_ref_cycles.py +++ b/tests/test_ref_cycles.py @@ -14,8 +14,8 @@ def assert_alldead(refs): for ref in refs: obj = ref() - assert obj is None or not ( - isinstance(obj, pg.QtCore.QObject) and pg.Qt.isQObjectAlive(obj) + assert obj is None or ( + isinstance(obj, pg.QtCore.QObject) and not pg.Qt.isQObjectAlive(obj) ) def qObjectTree(root): From 46f89a0232887c8e7fa9263a6d1f39ebeaba5039 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 12 Oct 2022 20:40:51 -0700 Subject: [PATCH 117/306] Fixup UML diagram for light and dark mode --- doc/source/api_reference/uml_overview.rst | 26 +- ...graphml => overview_uml-dark_mode.graphml} | 533 ++++++- doc/source/images/overview_uml-dark_mode.svg | 1226 +++++++++++++++ .../images/overview_uml-light_mode.graphml | 1396 +++++++++++++++++ ...ew_uml.svg => overview_uml-light_mode.svg} | 708 +++++---- 5 files changed, 3468 insertions(+), 421 deletions(-) rename doc/source/images/{overview_uml.graphml => overview_uml-dark_mode.graphml} (53%) create mode 100644 doc/source/images/overview_uml-dark_mode.svg create mode 100644 doc/source/images/overview_uml-light_mode.graphml rename doc/source/images/{overview_uml.svg => overview_uml-light_mode.svg} (62%) diff --git a/doc/source/api_reference/uml_overview.rst b/doc/source/api_reference/uml_overview.rst index 18168ee8a0..40dd14e956 100644 --- a/doc/source/api_reference/uml_overview.rst +++ b/doc/source/api_reference/uml_overview.rst @@ -1,3 +1,5 @@ +:html_theme.sidebar_secondary.remove: + UML class diagram ================= @@ -11,4 +13,26 @@ The black arrows indicate inheritance between two classes (with the parent class The gray lines with the diamonds indicate an aggregation relation. For example the :class:`PlotDataItem ` class has a ``curve`` attribute that is a reference to a :class:`PlotCurveItem ` object. -.. image:: /images/overview_uml.svg + +.. If it's stupid, and it works, it's not stupid +.. Inlining SVG code, not using tags so nodes can act as links and be clicked + +.. raw:: html + +
    + +.. raw:: html + :file: ../images/overview_uml-dark_mode.svg + +.. raw:: html + +
    + +.. raw:: html + :file: ../images/overview_uml-light_mode.svg + +.. raw:: html + +
    + +.. end of not stupid stupidity diff --git a/doc/source/images/overview_uml.graphml b/doc/source/images/overview_uml-dark_mode.graphml similarity index 53% rename from doc/source/images/overview_uml.graphml rename to doc/source/images/overview_uml-dark_mode.graphml index d36dca5fcd..5aa011f52a 100644 --- a/doc/source/images/overview_uml.graphml +++ b/doc/source/images/overview_uml-dark_mode.graphml @@ -1,6 +1,6 @@ - + @@ -20,7 +20,7 @@ - + ImageView @@ -40,7 +40,7 @@ displaying the contents of a QGraphicsScene.]]> - + QGraphicsView @@ -57,7 +57,7 @@ managing a large number of 2D graphical items.]]> - + QGraphicsScene @@ -75,7 +75,7 @@ graphical items in a QGraphicsScene.]]> - + QGraphicsItem @@ -92,7 +92,7 @@ graphics items that require signals, slots and properties.]]> - + QGraphicsObject @@ -112,7 +112,7 @@ that is automatically scaled to the full view geometry.]]> - + GraphicsView @@ -129,7 +129,7 @@ methods (provided by GraphicsItem)]]> - + GraphicsObject @@ -152,7 +152,7 @@ PlotDataItem; these rarely need to be instantiated directly.]]> - + ScatterPlotItem @@ -175,7 +175,7 @@ PlotDataItem; these rarely need to be instantiated directly.]]> - + PlotCurveItem @@ -195,7 +195,7 @@ view - + ImageItem @@ -218,7 +218,7 @@ individually, this class provides a unified interface to both.]]> - + PlotDataItem @@ -239,7 +239,7 @@ also available through GraphicsLayoutWidget.]]> - + GraphicsLayoutWidget @@ -257,7 +257,7 @@ have multiple inheritance with QObject subclasses.)]]> - + GraphicsItem @@ -275,7 +275,7 @@ items to be managed by layouts.]]> - + QGraphicsLayoutItem @@ -292,7 +292,7 @@ all widget items in a QGraphicsScene. ]]> - + QGraphicsWidget @@ -310,7 +310,7 @@ Graphics View.]]> - + QGraphicsLayout @@ -328,7 +328,7 @@ Graphics View.]]> - + QGraphicsGridLayout @@ -347,7 +347,7 @@ through PlotWidget.]]> - + PlotWidget @@ -364,7 +364,7 @@ complete, parallel mouse event system. ]]> - + GraphicsScene @@ -387,7 +387,7 @@ Most of the extra functionality is inherited from GraphicsItem.]]> - + GraphicsWidget @@ -412,7 +412,7 @@ Canvas or with GraphicsLayout.addViewBox().]]> - + ViewBox @@ -433,7 +433,7 @@ GraphicsLayoutWidget.]]> - + GraphicsLayout @@ -453,7 +453,7 @@ the graphics item to the scene.]]> - + PlotItem @@ -472,7 +472,7 @@ can be painted on with QPainter. ]]> - + QPaintDevice @@ -488,7 +488,7 @@ can be painted on with QPainter. ]]> - + QWidget @@ -504,7 +504,7 @@ can be painted on with QPainter. ]]> - + QObject @@ -519,8 +519,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -530,8 +539,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -541,8 +559,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -552,8 +579,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -563,8 +599,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -577,8 +622,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -590,8 +644,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -600,8 +663,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -611,8 +683,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -624,8 +705,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -634,8 +724,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -645,8 +744,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -656,8 +764,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -667,8 +784,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -678,8 +804,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -691,8 +826,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -703,8 +847,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -713,8 +866,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -724,8 +886,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -735,8 +906,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -746,8 +926,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -757,8 +946,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -768,8 +966,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -779,8 +986,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -790,8 +1006,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -803,8 +1028,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -813,8 +1047,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -824,8 +1067,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -838,8 +1090,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -851,8 +1112,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -864,8 +1134,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -877,8 +1156,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -887,8 +1175,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -898,8 +1195,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -915,8 +1221,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -930,8 +1245,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -944,8 +1268,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -954,8 +1287,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + @@ -965,24 +1307,22 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + - - - - - - - - - - - - + @@ -990,12 +1330,21 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + - + @@ -1003,12 +1352,21 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + - + @@ -1017,8 +1375,17 @@ can be painted on with QPainter. ]]> - + + + + + + + + + + diff --git a/doc/source/images/overview_uml-dark_mode.svg b/doc/source/images/overview_uml-dark_mode.svg new file mode 100644 index 0000000000..79eae99823 --- /dev/null +++ b/doc/source/images/overview_uml-dark_mode.svg @@ -0,0 +1,1226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ImageView + + + + + graphicsView + imageItem + view + + + + + + + + + + + + + + + QGraphicsView + + + + + + scene() + + + + + + + + + + + + + + QGraphicsScene + + + + + + items() + views() + + + + + + + + + + + + + + QGraphicsItem + + + + + + scene() + + + + + + + + + + + + + + QGraphicsObject + + + + + + + + + + + + + + + GraphicsView + + + + + sceneObj + + + + + + + + + + + + + + + GraphicsObject + + + + + + + + + + + + + + + ScatterPlotItem + + + + + + + + + + + + + + + PlotCurveItem + + + + + + + + + + + + + + + ImageItem + + + + + + + + + + + + + + + PlotDataItem + + + + + curve + scatter + + + + + + + + + + + + + + + GraphicsLayoutWidget + + + + + graphicsLayout + + + + + + + + + + + + + + + GraphicsItem + + + + + + + + + + + + + + + QGraphicsLayoutItem + + + + + + + + + + + + + + + QGraphicsWidget + + + + + + + + + + + + + + + QGraphicsLayout + + + + + + + + + + + + + + + QGraphicsGridLayout + + + + + + + + + + + + + + + PlotWidget + + + + + plotItem + + + + + + + + + + + + + + + GraphicsScene + + + + + + + + + + + + + + + GraphicsWidget + + + + + + + + + + + + + + + ViewBox + + + + + + addItem(item) + + + + + + + + + + + + + + GraphicsLayout + + + + + layout + + + + + + + + + + + + + + + PlotItem + + + + + items + layout + vb + + addItem(item) + + + + + + + + + + + + + + QPaintDevice + + + + + + + + + + + + + + + QWidget + + + + + + + + + + + + + + + QObject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Widget used for display and analysis of image data. + + + + + + The QGraphicsView class provides a widget for + + displaying the contents of a QGraphicsScene. + + + + + + The QGraphicsScene class provides a surface for + + managing a large number of 2D graphical items. + + + + + + The QGraphicsItem class is the base class for all + + graphical items in a QGraphicsScene. + + + + + + The QGraphicsObject class provides a base class for all + + graphics items that require signals, slots and properties. + + + + + + Re-implementation of QGraphicsView that removes scrollbars and allows + + unambiguous control of the viewed coordinate range. + + + Also automatically creates a GraphicsScene and a central QGraphicsWidget + + that is automatically scaled to the full view geometry. + + + + + + Extension of QGraphicsObject with some useful + + methods (provided by GraphicsItem) + + + + + + Displays a set of x/y points. + + + Instances of this class are created automatically as part of + + PlotDataItem; these rarely need to be instantiated directly. + + + + + + Class representing a single plot curve. + + + Instances of this class are created automatically as part of + + PlotDataItem; these rarely need to be instantiated directly. + + + + + + GraphicsObject displaying an image. + + + + + + GraphicsItem for displaying plot curves, scatter plots, or both. + + + While it is possible to use PlotCurveItem or ScatterPlotItem + + individually, this class provides a unified interface to both. + + + + + + Convenience class consisting of a GraphicsView with a + + single GraphicsLayout as its central item. + + + Most of the methods provided by GraphicsLayout are + + also available through GraphicsLayoutWidget. + + + + + + Abstract class providing useful methods to GraphicsObject + + and GraphicsWidget. (This is required because we cannot + + have multiple inheritance with QObject subclasses.) + + + + + + The QGraphicsLayoutItem class can + + be inherited to allow your custom + + items to be managed by layouts. + + + + + + The QGraphicsWidget class is the base class for + + all widget items in a QGraphicsScene. + + + + + + The QGraphicsLayout class provides + + the base class for all layouts in + + Graphics View. + + + + + + The QGraphicsGridLayout class provides + + a grid layout for managing widgets in + + Graphics View. + + + + + + A subclass of GraphicsView with a single PlotItem displayed. + + + Most of the methods provided by PlotItem are also available + + through PlotWidget. + + + + + + Extension of QGraphicsScene that implements a + + complete, parallel mouse event system. + + + + + + Extends QGraphicsWidget with several helpful methods and + + workarounds for PyQt bugs. + + + Most of the extra functionality is inherited from GraphicsItem. + + + + + + Box that allows internal scaling/panning of children by mouse drag. + + + addItem() will add a graphics item (e.g. a PlotDataItem) to the scene. + + + This class is usually created automatically as part of a PlotItem or + + Canvas or with GraphicsLayout.addViewBox(). + + + + + + Used for laying out GraphicsWidgets + + in a grid. + + + This is usually created automatically + + as part of a GraphicsWindow or + + GraphicsLayoutWidget. + + + + + + GraphicsWidget implementing a standard 2D plotting + + area with axes. + + + addItem() will call ViewBox.addItem(), which will add + + the graphics item to the scene. + + + + + + The QPaintDevice class is the base class of objects that + + can be painted on with QPainter. + + + + + + The QWidget class is the base class of all user interface objects. + + + + + + The QObject class is the base class of all Qt objects. + + + + diff --git a/doc/source/images/overview_uml-light_mode.graphml b/doc/source/images/overview_uml-light_mode.graphml new file mode 100644 index 0000000000..98c7280635 --- /dev/null +++ b/doc/source/images/overview_uml-light_mode.graphml @@ -0,0 +1,1396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + ImageView + + graphicsView +imageItem +view + + + + + + + + + + + + + + + QGraphicsView + + + scene() + + + + + + + + + + + + + QGraphicsScene + + + items() +views() + + + + + + + + + + + + + QGraphicsItem + + + scene() + + + + + + + + + + + + + QGraphicsObject + + + + + + + + + + + + + + + + GraphicsView + + sceneObj + + + + + + + + + + + + + + GraphicsObject + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + ScatterPlotItem + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + PlotCurveItem + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + ImageItem + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + PlotDataItem + + curve +scatter + + + + + + + + + + + + + + GraphicsLayoutWidget + + graphicsLayout + + + + + + + + + + + + + + GraphicsItem + + + + + + + + + + + + + + + + QGraphicsLayoutItem + + + + + + + + + + + + + + + + QGraphicsWidget + + + + + + + + + + + + + + + + QGraphicsLayout + + + + + + + + + + + + + + + + QGraphicsGridLayout + + + + + + + + + + + + + + + + PlotWidget + + plotItem + + + + + + + + + + + + + + GraphicsScene + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + GraphicsWidget + + graphicsView +imageItem +scene +view + + + + + + + + + + + + + + + ViewBox + + + addItem(item) + + + + + + + + + + + + + GraphicsLayout + + layout + + + + + + + + + + + + + + PlotItem + + items +layout +vb + addItem(item) + + + + + + + + + + + + + QPaintDevice + + + + + + + + + + + + + + + + QWidget + + + + + + + + + + + + + + + + QObject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/images/overview_uml.svg b/doc/source/images/overview_uml-light_mode.svg similarity index 62% rename from doc/source/images/overview_uml.svg rename to doc/source/images/overview_uml-light_mode.svg index 99f7d8cec9..d60f07ab62 100644 --- a/doc/source/images/overview_uml.svg +++ b/doc/source/images/overview_uml-light_mode.svg @@ -1,91 +1,91 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -209,733 +209,767 @@ }; ]]> - - - - + - + - - ImageView + + ImageView - - - graphicsView - imageItem - view - + + + graphicsView + imageItem + view + - + - + - - QGraphicsView + + QGraphicsView - - - - scene() + + + + scene() - + - + - - QGraphicsScene + + QGraphicsScene - - - - items() - views() + + + + items() + views() - + - + - - QGraphicsItem + + QGraphicsItem - - - - scene() + + + + scene() - + - + - - QGraphicsObject + + QGraphicsObject - + - + - - GraphicsView + + GraphicsView - - - sceneObj - + + + sceneObj + - + - + - - GraphicsObject + + GraphicsObject - + - + - - ScatterPlotItem + + ScatterPlotItem - + - + - - PlotCurveItem + + PlotCurveItem - + - + - - ImageItem + + ImageItem - + - + - - PlotDataItem + + PlotDataItem - - - curve - scatter - + + + curve + scatter + - + - + - - GraphicsLayoutWidget + + GraphicsLayoutWidget - - - graphicsLayout - + + + graphicsLayout + - + - + - - GraphicsItem + + GraphicsItem - + - + - - QGraphicsLayoutItem + + QGraphicsLayoutItem - + - + - - QGraphicsWidget + + QGraphicsWidget - + - + - - QGraphicsLayout + + QGraphicsLayout - + - + - - QGraphicsGridLayout + + QGraphicsGridLayout - + - + - - PlotWidget + + PlotWidget - - - plotItem - + + + plotItem + - + - + - - GraphicsScene + + GraphicsScene - + - + - - GraphicsWidget + + GraphicsWidget - + - + - - ViewBox + + ViewBox - - - - addItem(item) + + + + addItem(item) - + - + - - GraphicsLayout + + GraphicsLayout - - - layout - + + + layout + - + - + - - PlotItem + + PlotItem - - - items - layout - vb - - addItem(item) + + + items + layout + vb + + addItem(item) - + - + - - QPaintDevice + + QPaintDevice - + - + - - QWidget + + QWidget - + - + - - QObject + + QObject - - - - + + + + + - - - - - + + + + + + - - - - + + + + + - - - - - + + + + + + - - - + + + + - - - - + + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - - + + + + + - - - + + + + - - - + + + + - - - - + + + + + - - - - + + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - - - - - - + + + + - - - + + + + - - - - + + + + + - - - + + + + - - - + + + + - - - + + + + - - - - + + + + + - - - + + + + - - - - + + + + + - - - - + + + + + - - - + + + + - - - - + + + + + - - - - + + + + + - - - - + + + + + - - - + + + + - - - - - + + + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + From 7718676f2ee8bd578275c08866a0397d681fa130 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Oct 2022 02:48:45 +0000 Subject: [PATCH 118/306] Bump sphinx from 5.2.3 to 5.3.0 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.3 to 5.3.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.3...v5.3.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index cd7a06bf46..ce3957f06e 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.4.0 -sphinx==5.2.3 +sphinx==5.3.0 pydata-sphinx-theme==0.11.0 sphinx-design==0.3.0 sphinxcontrib-images==0.9.4 From 035046ad3e27dcb385a0fc4d1e634558cbb9146c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 26 Oct 2022 20:13:22 -0700 Subject: [PATCH 119/306] One ethical ad on the sidebar is sufficient --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index e5e6aee365..872cd64fc2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -346,7 +346,7 @@ } else: html_sidebars = { - "**": ["sidebar-nav-bs.html", "sidebar-ethical-ads.html"], + "**": ["sidebar-nav-bs.html"], 'index': [] # don't show sidebar on main landing page } From 0cf9b352106ec226b8cdc088f03c90c3c1cf18a9 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 26 Oct 2022 20:16:33 -0700 Subject: [PATCH 120/306] Update copyright to pyqtgraph developers --- README.md | 2 +- doc/source/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7e10a3629..c96b36446e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ PyQtGraph [![Discord](https://img.shields.io/discord/946624673200893953.svg?label=PyQtGraph&logo=discord)](https://discord.gg/3Qxjz5BF) A pure-Python graphics library for PyQt5/PyQt6/PySide2/PySide6 -Copyright 2020 Luke Campagnola, University of North Carolina at Chapel Hill +Copyright 2022 PyQtGraph developers diff --git a/doc/source/conf.py b/doc/source/conf.py index 872cd64fc2..bfc97263a2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -80,7 +80,7 @@ now = datetime.utcfromtimestamp( int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) ) -copyright = '2011 - {}, Luke Campagnola'.format(now.year) +copyright = '2011 - {}, PyQtGraph developers'.format(now.year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 96468a480698a5bb2c55a071f127c617031ce357 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 26 Oct 2022 23:03:19 -0500 Subject: [PATCH 121/306] See if updating mpl fixes CI --- .github/workflows/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt index de7ad47a3e..bac65b8b83 100644 --- a/.github/workflows/requirements.txt +++ b/.github/workflows/requirements.txt @@ -13,7 +13,7 @@ numba==0.55.2; python_version == '3.9' pyopengl==3.1.6 # supplimental tools -matplotlib==3.5.2 +matplotlib==3.6.1 h5py==3.7.0 # testing From ec28395bdd39513a87f524a3ea3ead7e8c7a2d6f Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 26 Oct 2022 23:17:54 -0500 Subject: [PATCH 122/306] don't use qt5 mpl backend on qt6 --- pyqtgraph/widgets/MatplotlibWidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/MatplotlibWidget.py b/pyqtgraph/widgets/MatplotlibWidget.py index 49553179d5..643ec5e51c 100644 --- a/pyqtgraph/widgets/MatplotlibWidget.py +++ b/pyqtgraph/widgets/MatplotlibWidget.py @@ -1,5 +1,5 @@ -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure from ..Qt import QtWidgets From f381fae98901066f755bb8b9f31f9f68cc56f072 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 27 Oct 2022 21:11:41 -0700 Subject: [PATCH 123/306] Skip failing tests with pyside6 6.4+ and matplotlib <= 3.6.1 --- tests/exporters/test_matplotlib.py | 13 +++++++++++++ tests/widgets/test_matplotlibwidget.py | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/exporters/test_matplotlib.py b/tests/exporters/test_matplotlib.py index c734fb01f3..e740c3a118 100644 --- a/tests/exporters/test_matplotlib.py +++ b/tests/exporters/test_matplotlib.py @@ -1,3 +1,5 @@ +from importlib.metadata import version + import pytest import pyqtgraph as pg @@ -17,6 +19,17 @@ ) ) +# see https://github.com/matplotlib/matplotlib/pull/24172 +if ( + pg.Qt.QT_LIB == "PySide6" + and tuple(map(int, pg.Qt.PySide6.__version__.split("."))) > (6, 4) + and tuple(map(int, version("matplotlib").split("."))) < (3, 6, 2) +): + pytest.skip( + "matplotlib + PySide6 6.4 bug", + allow_module_level=True + ) + @skip_qt6 def test_MatplotlibExporter(): diff --git a/tests/widgets/test_matplotlibwidget.py b/tests/widgets/test_matplotlibwidget.py index 31e66f62ff..31ca2f62b6 100644 --- a/tests/widgets/test_matplotlibwidget.py +++ b/tests/widgets/test_matplotlibwidget.py @@ -4,13 +4,27 @@ Tests the creation of a MatplotlibWidget. """ +from importlib.metadata import version + +import numpy as np import pytest + import pyqtgraph as pg from pyqtgraph.Qt import QtWidgets -import numpy as np pytest.importorskip("matplotlib") +# see https://github.com/matplotlib/matplotlib/pull/24172 +if ( + pg.Qt.QT_LIB == "PySide6" + and tuple(map(int, pg.Qt.PySide6.__version__.split("."))) > (6, 4) + and tuple(map(int, version("matplotlib").split("."))) < (3, 6, 2) +): + pytest.skip( + "matplotlib + PySide6 6.4 bug", + allow_module_level=True + ) + from pyqtgraph.widgets.MatplotlibWidget import MatplotlibWidget pg.mkQApp() From 09189d20936ec4a87243f1b6a6f720d5fc7d6f0c Mon Sep 17 00:00:00 2001 From: aksy2512 Date: Fri, 28 Oct 2022 10:56:10 +0200 Subject: [PATCH 124/306] Changed exportDialog --- pyqtgraph/GraphicsScene/exportDialog.py | 7 +++---- pyqtgraph/GraphicsScene/exportDialogTemplate_generic.py | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 870123334b..e98acb32fe 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -49,13 +49,12 @@ def show(self, item=None): self.activateWindow() self.raise_() self.selectBox.setVisible(True) - if not self.shown: self.shown = True vcenter = self.scene.getViewWidget().geometry().center() - self.setGeometry(int(vcenter.x() - self.width() / 2), - int(vcenter.y() - self.height() / 2), - self.width(), self.height()) + x = max(0, int(vcenter.x() - self.width() / 2)) + y = max(0, int(vcenter.y() - self.height() / 2)) + self.move(x, y) def updateItemList(self, select=None): self.ui.itemTree.clear() diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_generic.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_generic.py index 48db23583f..a7b1806102 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_generic.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_generic.py @@ -5,7 +5,6 @@ # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. - from ..Qt import QtCore, QtGui, QtWidgets From d2c0834ccc04da60efd9341e215749e0894c649f Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 28 Oct 2022 21:09:40 +0800 Subject: [PATCH 125/306] Revert "instantiate QMenu with a parent" This reverts commit 2ae77165aea17d517aee0abe920d62cdd86ad299. --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index edb0c84692..49db63ef3a 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -201,7 +201,7 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None self.ctrlMenu.setTitle(translate("PlotItem", 'Plot Options')) self.subMenus = [] for name, grp in menuItems: - sm = QtWidgets.QMenu(name, self.ctrlMenu) + sm = QtWidgets.QMenu(name) act = QtWidgets.QWidgetAction(self) act.setDefaultWidget(grp) sm.addAction(act) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index cbee671f89..232f660b19 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -24,7 +24,7 @@ def __init__(self, view): self.widgetGroups = [] self.dv = QtGui.QDoubleValidator(self) for axis in 'XY': - m = QtWidgets.QMenu(self) + m = QtWidgets.QMenu() m.setTitle(f"{axis} {translate('ViewBox', 'axis')}") w = QtWidgets.QWidget() ui = ui_template.Ui_Form() @@ -56,7 +56,7 @@ def __init__(self, view): self.ctrl[0].invertCheck.toggled.connect(self.xInvertToggled) self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled) - self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode"), self) + self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode")) group = QtGui.QActionGroup(self) # This does not work! QAction _must_ be initialized with a permanent From 71612fd6099c7cef0c1a95e08824c36f934426ea Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 27 Oct 2022 21:40:25 -0700 Subject: [PATCH 126/306] Use non-deprecated QMouseEvent signatures In Qt 6.4, the QMouseEvent signature we were using has been deprecated. To work around it, we get the global mouse position by using widget.mapToGlobal(pos), which gives us the global_position. This feels a bit hacky, but if it is stupid and it works, it is not stupid. --- tests/ui_testing.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/ui_testing.py b/tests/ui_testing.py index 75cc51c65b..b56141a609 100644 --- a/tests/ui_testing.py +++ b/tests/ui_testing.py @@ -31,29 +31,54 @@ def resizeWindow(win, w, h, timeout=2.0): def mousePress(widget, pos, button, modifier=None): if isinstance(widget, QtWidgets.QGraphicsView): widget = widget.viewport() + global_pos = QtCore.QPointF(widget.mapToGlobal(pos.toPoint())) if modifier is None: modifier = QtCore.Qt.KeyboardModifier.NoModifier - event = QtGui.QMouseEvent(QtCore.QEvent.Type.MouseButtonPress, pos, button, QtCore.Qt.MouseButton.NoButton, modifier) + event = QtGui.QMouseEvent( + QtCore.QEvent.Type.MouseButtonPress, + pos, + global_pos, + button, + QtCore.Qt.MouseButton.NoButton, + modifier + ) QtWidgets.QApplication.sendEvent(widget, event) def mouseRelease(widget, pos, button, modifier=None): if isinstance(widget, QtWidgets.QGraphicsView): widget = widget.viewport() + global_pos = QtCore.QPointF(widget.mapToGlobal(pos.toPoint())) if modifier is None: modifier = QtCore.Qt.KeyboardModifier.NoModifier - event = QtGui.QMouseEvent(QtCore.QEvent.Type.MouseButtonRelease, pos, button, QtCore.Qt.MouseButton.NoButton, modifier) + event = QtGui.QMouseEvent( + QtCore.QEvent.Type.MouseButtonRelease, + pos, + global_pos, + button, + QtCore.Qt.MouseButton.NoButton, + modifier + ) QtWidgets.QApplication.sendEvent(widget, event) def mouseMove(widget, pos, buttons=None, modifier=None): if isinstance(widget, QtWidgets.QGraphicsView): widget = widget.viewport() + + global_pos = QtCore.QPointF(widget.mapToGlobal(pos.toPoint())) if modifier is None: modifier = QtCore.Qt.KeyboardModifier.NoModifier if buttons is None: buttons = QtCore.Qt.MouseButton.NoButton - event = QtGui.QMouseEvent(QtCore.QEvent.Type.MouseMove, pos, QtCore.Qt.MouseButton.NoButton, buttons, modifier) + event = QtGui.QMouseEvent( + QtCore.QEvent.Type.MouseMove, + pos, + global_pos, + QtCore.Qt.MouseButton.NoButton, + buttons, + modifier + ) QtWidgets.QApplication.sendEvent(widget, event) From 2ed25515f2072abd918eb14a7913114a6f50b551 Mon Sep 17 00:00:00 2001 From: Jaime R <38530589+Jaime02@users.noreply.github.com> Date: Sat, 29 Oct 2022 06:54:20 +0200 Subject: [PATCH 127/306] Remove STRTransform main (#2466) * Remove STRTransform main Why this file has a main function? It is not meant to be run independently, right? Shouldn't it be removed? * Remove SRTTransforms mains These files have main entry points that are pointless, therefore they should be removed * Fix imports Co-authored-by: Jaime Resano --- pyqtgraph/SRTTransform.py | 98 ++------------------------------- pyqtgraph/SRTTransform3D.py | 105 +++--------------------------------- 2 files changed, 10 insertions(+), 193 deletions(-) diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index aa63145013..3d0aac7ea2 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -3,7 +3,8 @@ import numpy as np from .Point import Point -from .Qt import QtCore, QtGui, QtWidgets +from .Qt import QtGui +from . import SRTTransform3D class SRTTransform(QtGui.QTransform): @@ -74,7 +75,7 @@ def setFromQTransform(self, tr): self.update() def setFromMatrix4x4(self, m): - m = SRTTransform3D(m) + m = SRTTransform3D.SRTTransform3D(m) angle, axis = m.getRotation() if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1): print("angle: %s axis: %s" % (str(angle), str(axis))) @@ -163,96 +164,3 @@ def __repr__(self): def matrix(self): return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) - - -if __name__ == '__main__': - import GraphicsView - - from . import widgets - from .functions import mkPen - app = pg.mkQApp() # noqa: qapp stored to avoid gc - win = QtWidgets.QMainWindow() - win.show() - cw = GraphicsView.GraphicsView() - #cw.enableMouse() - win.setCentralWidget(cw) - s = QtWidgets.QGraphicsScene() - cw.setScene(s) - win.resize(600,600) - cw.enableMouse() - cw.setRange(QtCore.QRectF(-100., -100., 200., 200.)) - - class Item(QtWidgets.QGraphicsItem): - def __init__(self): - QtWidgets.QGraphicsItem.__init__(self) - self.b = QtWidgets.QGraphicsRectItem(20, 20, 20, 20, self) - self.b.setPen(QtGui.QPen(mkPen('y'))) - self.t1 = QtWidgets.QGraphicsTextItem(self) - self.t1.setHtml('R') - self.t1.translate(20, 20) - self.l1 = QtWidgets.QGraphicsLineItem(10, 0, -10, 0, self) - self.l2 = QtWidgets.QGraphicsLineItem(0, 10, 0, -10, self) - self.l1.setPen(QtGui.QPen(mkPen('y'))) - self.l2.setPen(QtGui.QPen(mkPen('y'))) - def boundingRect(self): - return QtCore.QRectF() - def paint(self, *args): - pass - - #s.addItem(b) - #s.addItem(t1) - item = Item() - s.addItem(item) - l1 = QtWidgets.QGraphicsLineItem(10, 0, -10, 0) - l2 = QtWidgets.QGraphicsLineItem(0, 10, 0, -10) - l1.setPen(QtGui.QPen(mkPen('r'))) - l2.setPen(QtGui.QPen(mkPen('r'))) - s.addItem(l1) - s.addItem(l2) - - tr1 = SRTTransform() - tr2 = SRTTransform() - tr3 = QtGui.QTransform() - tr3.translate(20, 0) - tr3.rotate(45) - print("QTransform -> Transform:", SRTTransform(tr3)) - - print("tr1:", tr1) - - tr2.translate(20, 0) - tr2.rotate(45) - print("tr2:", tr2) - - dt = tr2/tr1 - print("tr2 / tr1 = ", dt) - - print("tr2 * tr1 = ", tr2*tr1) - - tr4 = SRTTransform() - tr4.scale(-1, 1) - tr4.rotate(30) - print("tr1 * tr4 = ", tr1*tr4) - - w1 = widgets.TestROI((19,19), (22, 22), invertible=True) - #w2 = widgets.TestROI((0,0), (150, 150)) - w1.setZValue(10) - s.addItem(w1) - #s.addItem(w2) - w1Base = w1.getState() - #w2Base = w2.getState() - def update(): - tr1 = w1.getGlobalTransform(w1Base) - #tr2 = w2.getGlobalTransform(w2Base) - item.setTransform(tr1) - - #def update2(): - #tr1 = w1.getGlobalTransform(w1Base) - #tr2 = w2.getGlobalTransform(w2Base) - #t1.setTransform(tr1) - #w1.setState(w1Base) - #w1.applyGlobalTransform(tr2) - - w1.sigRegionChanged.connect(update) - #w2.sigRegionChanged.connect(update2) - -from .SRTTransform3D import SRTTransform3D diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 01f4f35f9f..59dd78429c 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -2,9 +2,10 @@ import numpy as np -from .Qt import QtCore, QtGui, QtWidgets +from .Qt import QtGui from .Transform3D import Transform3D from .Vector import Vector +from . import SRTTransform class SRTTransform3D(Transform3D): @@ -17,7 +18,7 @@ def __init__(self, init=None): if init is None: return if init.__class__ is QtGui.QTransform: - init = SRTTransform(init) + init = SRTTransform.SRTTransform(init) if isinstance(init, dict): self.restoreState(init) @@ -29,7 +30,7 @@ def __init__(self, init=None): 'axis': Vector(init._state['axis']), } self.update() - elif isinstance(init, SRTTransform): + elif isinstance(init, SRTTransform.SRTTransform): self._state = { 'pos': Vector(init._state['pos']), 'scale': Vector(init._state['scale']), @@ -172,15 +173,15 @@ def setFromMatrix(self, m): def as2D(self): """Return a QTransform representing the x,y portion of this transform (if possible)""" - return SRTTransform(self) + return SRTTransform.SRTTransform(self) #def __div__(self, t): #"""A / B == B^-1 * A""" #dt = t.inverted()[0] * self - #return SRTTransform(dt) + #return SRTTransform.SRTTransform(dt) #def __mul__(self, t): - #return SRTTransform(QtGui.QTransform.__mul__(self, t)) + #return SRTTransform.SRTTransform(QtGui.QTransform.__mul__(self, t)) def saveState(self): p = self._state['pos'] @@ -224,95 +225,3 @@ def matrix(self, nd=3): return m[:3,:3] else: raise Exception("Argument 'nd' must be 2 or 3") - -if __name__ == '__main__': - import GraphicsView - - from . import widgets - from .functions import mkPen - app = pg.mkQApp() # noqa: qapp must be stored to avoid gc - win = QtWidgets.QMainWindow() - win.show() - cw = GraphicsView.GraphicsView() - #cw.enableMouse() - win.setCentralWidget(cw) - s = QtWidgets.QGraphicsScene() - cw.setScene(s) - win.resize(600,600) - cw.enableMouse() - cw.setRange(QtCore.QRectF(-100., -100., 200., 200.)) - - class Item(QtWidgets.QGraphicsItem): - def __init__(self): - QtWidgets.QGraphicsItem.__init__(self) - self.b = QtWidgets.QGraphicsRectItem(20, 20, 20, 20, self) - self.b.setPen(QtGui.QPen(mkPen('y'))) - self.t1 = QtWidgets.QGraphicsTextItem(self) - self.t1.setHtml('R') - self.t1.translate(20, 20) - self.l1 = QtWidgets.QGraphicsLineItem(10, 0, -10, 0, self) - self.l2 = QtWidgets.QGraphicsLineItem(0, 10, 0, -10, self) - self.l1.setPen(QtGui.QPen(mkPen('y'))) - self.l2.setPen(QtGui.QPen(mkPen('y'))) - def boundingRect(self): - return QtCore.QRectF() - def paint(self, *args): - pass - - #s.addItem(b) - #s.addItem(t1) - item = Item() - s.addItem(item) - l1 = QtWidgets.QGraphicsLineItem(10, 0, -10, 0) - l2 = QtWidgets.QGraphicsLineItem(0, 10, 0, -10) - l1.setPen(QtGui.QPen(mkPen('r'))) - l2.setPen(QtGui.QPen(mkPen('r'))) - s.addItem(l1) - s.addItem(l2) - - tr1 = SRTTransform() - tr2 = SRTTransform() - tr3 = QtGui.QTransform() - tr3.translate(20, 0) - tr3.rotate(45) - print("QTransform -> Transform: %s" % str(SRTTransform(tr3))) - - print("tr1: %s" % str(tr1)) - - tr2.translate(20, 0) - tr2.rotate(45) - print("tr2: %s" % str(tr2)) - - dt = tr2/tr1 - print("tr2 / tr1 = %s" % str(dt)) - - print("tr2 * tr1 = %s" % str(tr2*tr1)) - - tr4 = SRTTransform() - tr4.scale(-1, 1) - tr4.rotate(30) - print("tr1 * tr4 = %s" % str(tr1*tr4)) - - w1 = widgets.TestROI((19,19), (22, 22), invertible=True) - #w2 = widgets.TestROI((0,0), (150, 150)) - w1.setZValue(10) - s.addItem(w1) - #s.addItem(w2) - w1Base = w1.getState() - #w2Base = w2.getState() - def update(): - tr1 = w1.getGlobalTransform(w1Base) - #tr2 = w2.getGlobalTransform(w2Base) - item.setTransform(tr1) - - #def update2(): - #tr1 = w1.getGlobalTransform(w1Base) - #tr2 = w2.getGlobalTransform(w2Base) - #t1.setTransform(tr1) - #w1.setState(w1Base) - #w1.applyGlobalTransform(tr2) - - w1.sigRegionChanged.connect(update) - #w2.sigRegionChanged.connect(update2) - -from .SRTTransform import SRTTransform From 581c99b5fcb3dba82be4e83629a026c6907b329f Mon Sep 17 00:00:00 2001 From: ntjess Date: Tue, 1 Nov 2022 17:57:46 -0400 Subject: [PATCH 128/306] Make `ActionGroup` compatible with existing Parameter conventions (#2505) * Make `ActionGroup` compatible with existing Parameter conventions. - The name didn't end with `Parameter` which is unlike every other class - `sigActivated` should emit `self` instead of nothing - `emitStateChanged` should be fired during `activate` * Provide deprecated `ActionGroup` definition rather than eliminating entirely --- pyqtgraph/parametertree/interactive.py | 4 +-- .../parametertree/parameterTypes/__init__.py | 6 ++--- .../parameterTypes/actiongroup.py | 26 ++++++++++++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index b46d4264fc..c2754d4a6a 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -4,7 +4,7 @@ import pydoc from . import Parameter -from .parameterTypes import ActionGroup +from .parameterTypes import ActionGroupParameter from .. import functions as fn @@ -494,7 +494,7 @@ def resolveAndHookupParameterChild( return child def _resolveRunAction(self, interactiveFunction, functionGroup, functionTip): - if isinstance(functionGroup, ActionGroup): + if isinstance(functionGroup, ActionGroupParameter): functionGroup.setButtonOpts(visible=True) child = None else: diff --git a/pyqtgraph/parametertree/parameterTypes/__init__.py b/pyqtgraph/parametertree/parameterTypes/__init__.py index 42e1dbd7b7..2fe0336e04 100644 --- a/pyqtgraph/parametertree/parameterTypes/__init__.py +++ b/pyqtgraph/parametertree/parameterTypes/__init__.py @@ -1,6 +1,6 @@ from ..Parameter import registerParameterItemType, registerParameterType from .action import ActionParameter, ActionParameterItem -from .actiongroup import ActionGroup, ActionGroupParameterItem +from .actiongroup import ActionGroup, ActionGroupParameter, ActionGroupParameterItem from .basetypes import ( GroupParameter, GroupParameterItem, @@ -28,9 +28,9 @@ registerParameterItemType('int', NumericParameterItem, SimpleParameter, override=True) registerParameterItemType('str', StrParameterItem, SimpleParameter, override=True) -registerParameterType('group', GroupParameter, override=True) +registerParameterType('group', GroupParameter, override=True) # Keep actiongroup private for now, mainly useful for Interactor but not externally -registerParameterType('_actiongroup', ActionGroup, override=True) +registerParameterType('_actiongroup', ActionGroupParameter, override=True) registerParameterType('action', ActionParameter, override=True) registerParameterType('calendar', CalendarParameter, override=True) diff --git a/pyqtgraph/parametertree/parameterTypes/actiongroup.py b/pyqtgraph/parametertree/parameterTypes/actiongroup.py index 595ffb5983..58844d86c4 100644 --- a/pyqtgraph/parametertree/parameterTypes/actiongroup.py +++ b/pyqtgraph/parametertree/parameterTypes/actiongroup.py @@ -1,3 +1,5 @@ +import warnings + from ...Qt import QtCore from .action import ParameterControlledButton from .basetypes import GroupParameter, GroupParameterItem @@ -37,17 +39,18 @@ def optsChanged(self, param, opts): super().optsChanged(param, opts) -class ActionGroup(GroupParameter): +class ActionGroupParameter(GroupParameter): itemClass = ActionGroupParameterItem - sigActivated = QtCore.Signal() + sigActivated = QtCore.Signal(object) def __init__(self, **opts): opts.setdefault("button", {}) super().__init__(**opts) def activate(self): - self.sigActivated.emit() + self.sigActivated.emit(self) + self.emitStateChanged('activated', None) def setButtonOpts(self, **opts): """ @@ -57,3 +60,20 @@ def setButtonOpts(self, **opts): buttonOpts = self.opts.get("button", {}).copy() buttonOpts.update(opts) self.setOpts(button=buttonOpts) + + +class ActionGroup(ActionGroupParameter): + sigActivated = QtCore.Signal() + + def __init__(self, **opts): + warnings.warn( + "`ActionGroup` is deprecated and will be removed in the first release after " + "January 2023. Use `ActionGroupParameter` instead. See " + "https://github.com/pyqtgraph/pyqtgraph/pull/2505 for details.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(**opts) + + def activate(self): + self.sigActivated.emit() From b3689b13787b5325985e416d041b2065fbc1bf0c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Tue, 1 Nov 2022 22:35:36 -0700 Subject: [PATCH 129/306] Fix vertical alignment of spans for highlighted code block --- doc/source/_static/dash.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/source/_static/dash.css b/doc/source/_static/dash.css index 5ddb6f5786..d85c613b32 100644 --- a/doc/source/_static/dash.css +++ b/doc/source/_static/dash.css @@ -22,3 +22,8 @@ width. To be used with sphinx-pydata-theme flex-grow: 1; max-width: 100%; } + +/* patching issue from pydata-sphinx-theme */ +div.viewcode-block:target { + display: block; +} From b13df0e72734cc354e458336e8162ace7f8ba254 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 2 Nov 2022 21:37:21 +0800 Subject: [PATCH 130/306] PlotItem: unset submenu parent --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 49db63ef3a..713b4d4588 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -207,6 +207,7 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None sm.addAction(act) self.subMenus.append(sm) self.ctrlMenu.addMenu(sm) + sm.setParent(None) # workaround PySide bug (present at least up to 6.4.0) self.stateGroup = WidgetGroup() for name, w in menuItems: From 85a37b3739eb8076a234e09883644439ef7b0847 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 2 Nov 2022 21:37:13 +0800 Subject: [PATCH 131/306] ViewBoxMenu: unset submenu parent --- pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 232f660b19..81b87c836c 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -33,6 +33,7 @@ def __init__(self, view): a.setDefaultWidget(w) m.addAction(a) self.addMenu(m) + m.setParent(None) # workaround PySide bug (present at least up to 6.4.0) self.axes.append(m) self.ctrl.append(ui) wg = WidgetGroup(w) From 1db3b9e5e390b5bd7b7d372a8ec29b99c38d1e1e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 2 Nov 2022 22:46:37 +0800 Subject: [PATCH 132/306] add test_qmenu_leak_workaround.py --- tests/test_qmenu_leak_workaround.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/test_qmenu_leak_workaround.py diff --git a/tests/test_qmenu_leak_workaround.py b/tests/test_qmenu_leak_workaround.py new file mode 100644 index 0000000000..71bec3074f --- /dev/null +++ b/tests/test_qmenu_leak_workaround.py @@ -0,0 +1,25 @@ +import sys +import pyqtgraph as pg +from pyqtgraph.Qt import QtWidgets + +def test_qmenu_leak_workaround(): + # refer to https://github.com/pyqtgraph/pyqtgraph/pull/2518 + pg.mkQApp() + topmenu = QtWidgets.QMenu() + submenu = QtWidgets.QMenu() + + refcnt1 = sys.getrefcount(submenu) + + # check that after the workaround, + # submenu has no change in refcnt + topmenu.addMenu(submenu) + submenu.setParent(None) # this is the workaround for PySide{2,6}, + # and should have no effect on bindings + # where it is not needed. + refcnt2 = sys.getrefcount(submenu) + assert refcnt2 == refcnt1 + + # check that topmenu is not a C++ parent of submenu. + # i.e. deleting topmenu leaves submenu alive + del topmenu + assert pg.Qt.isQObjectAlive(submenu) \ No newline at end of file From 66a45837391a4c0b5fa4ebab4ae6c6d148e92f75 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 3 Nov 2022 06:11:32 +0800 Subject: [PATCH 133/306] revert to old assert_alldead implementation the existing implementation was to workaround the previous (reverted) QMenu leak implementation. the new QMenu leak workaround implementation does not need it. --- tests/test_ref_cycles.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_ref_cycles.py b/tests/test_ref_cycles.py index 8d09bf00ad..f602116e01 100644 --- a/tests/test_ref_cycles.py +++ b/tests/test_ref_cycles.py @@ -13,10 +13,7 @@ def assert_alldead(refs): for ref in refs: - obj = ref() - assert obj is None or ( - isinstance(obj, pg.QtCore.QObject) and not pg.Qt.isQObjectAlive(obj) - ) + assert ref() is None def qObjectTree(root): """Return root and its entire tree of qobject children""" From 41c8c245bda59d8760999effea6d2092b11f5353 Mon Sep 17 00:00:00 2001 From: Petras Jokubauskas Date: Fri, 4 Nov 2022 17:21:08 +0100 Subject: [PATCH 134/306] fix SpinBox.py in method of Qt 5.15.6 SpinBox setMaximumHeight does not accept float. changing 1e6 (which produces 1million in float) to integer 1000000 --- pyqtgraph/widgets/SpinBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 76722fca89..67a7e09e62 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -572,7 +572,7 @@ def _updateHeight(self): # SpinBox has very large margins on some platforms; this is a hack to remove those # margins and allow more compact packing of controls. if not self.opts['compactHeight']: - self.setMaximumHeight(1e6) + self.setMaximumHeight(1000000) return h = QtGui.QFontMetrics(self.font()).height() if self._lastFontHeight != h: From 7ef9c8ff6c528840c25cde2b7282b51c29e1477b Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 4 Nov 2022 22:03:27 -0400 Subject: [PATCH 135/306] Play nice with InteractiveFunction subclasses `__str__` --- pyqtgraph/parametertree/interactive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index c2754d4a6a..d3c0e5de4a 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -192,7 +192,7 @@ def reconnect(self): return oldDisconnect def __str__(self): - return f"InteractiveFunction(`<{self.function.__name__}>`) at {hex(id(self))}" + return f"{type(self).__name__}(`<{self.function.__name__}>`) at {hex(id(self))}" def __repr__(self): return ( From 0b2149ffb3e08230c3602dcfd0a4c73e242f2c41 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 4 Nov 2022 22:07:13 -0400 Subject: [PATCH 136/306] `params` was renamed to `parameters` a while ago, but `__repr__` wasn't changed --- pyqtgraph/parametertree/interactive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index d3c0e5de4a..a8dc54075d 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -197,7 +197,7 @@ def __str__(self): def __repr__(self): return ( str(self) + " with keys:\n" - f"params={list(self.parameters)}, " + f"parameters={list(self.parameters)}, " f"extra={list(self.extra)}, " f"closures={list(self.closures)}" ) From dbd50de104bd2d07a941403e0407ea82de745086 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 4 Nov 2022 22:07:37 -0400 Subject: [PATCH 137/306] Black formatting --- pyqtgraph/parametertree/interactive.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index a8dc54075d..71fcb40f1b 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -342,9 +342,7 @@ def interact( # Get every overridden default locs = locals() # Everything until action template - opts = { - kk: locs[kk] for kk in self._optionNames if locs[kk] is not PARAM_UNSET - } + opts = {kk: locs[kk] for kk in self._optionNames if locs[kk] is not PARAM_UNSET} oldOpts = self.setOpts(**opts) # Delete explicitly since correct values are now ``self`` attributes del runOptions, titleFormat, nest, existOk, parent, runActionTemplate From cc504fed5dae8648375cc7578f8f6e7fb7ee9945 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 4 Nov 2022 22:21:52 -0400 Subject: [PATCH 138/306] Allow `Interactor` subclasses to ignore some children --- pyqtgraph/parametertree/interactive.py | 3 ++- tests/parametertree/test_Parameter.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index 71fcb40f1b..ab7c03f3c2 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -388,7 +388,8 @@ def interact( for name in checkNames: childOpts = children[chNames.index(name)] child = self.resolveAndHookupParameterChild(funcGroup, childOpts, function) - useParams.append(child) + if child is not None: + useParams.append(child) function.hookupParameters(useParams) if RunOptions.ON_ACTION in self.runOptions: diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 7dbb9b246f..c02e814efa 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -11,6 +11,7 @@ RunOptions, ) from pyqtgraph.parametertree import Parameter +from pyqtgraph.parametertree.Parameter import PARAM_TYPES from pyqtgraph.parametertree.parameterTypes import GroupParameter as GP from pyqtgraph.Qt import QtGui @@ -473,3 +474,20 @@ def a(): imageBytes = [ fn.ndarray_from_qimage(img) for img in images ] assert np.array_equal(*imageBytes) + + +def test_interact_ignore_none_child(): + class InteractorSubclass(Interactor): + def resolveAndHookupParameterChild( + self, functionGroup, childOpts, interactiveFunction + ): + if childOpts["type"] not in PARAM_TYPES: + # Optionally add to `extra` instead + return None + return super().resolveAndHookupParameterChild( + functionGroup, childOpts, interactiveFunction + ) + + interactor = InteractorSubclass() + out = interactor(lambda a=None: a, runOptions=[]) + assert "a" not in out.names From 69dc6eaa9a7ac7040cc59abaeb45956dffd8de58 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Mon, 7 Nov 2022 20:52:14 +0100 Subject: [PATCH 139/306] Handle interacting when ActionGroup already exists in `parent` --- pyqtgraph/parametertree/interactive.py | 4 ++-- tests/parametertree/test_Parameter.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/interactive.py b/pyqtgraph/parametertree/interactive.py index ab7c03f3c2..05d3eb5939 100644 --- a/pyqtgraph/parametertree/interactive.py +++ b/pyqtgraph/parametertree/interactive.py @@ -457,9 +457,9 @@ def _resolveFunctionGroup(self, functionDict, interactiveFunction): funcGroup = self.parent if self.nest: funcGroup = Parameter.create(**functionDict) - funcGroup.sigActivated.connect(interactiveFunction.runFromAction) if self.parent: - self.parent.addChild(funcGroup, existOk=self.existOk) + funcGroup = self.parent.addChild(funcGroup, existOk=self.existOk) + funcGroup.sigActivated.connect(interactiveFunction.runFromAction) return funcGroup @staticmethod diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index c02e814efa..29a3021b68 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -491,3 +491,17 @@ def resolveAndHookupParameterChild( interactor = InteractorSubclass() out = interactor(lambda a=None: a, runOptions=[]) assert "a" not in out.names + + +def test_interact_existing_parent(): + lastValue = None + + def a(): + nonlocal lastValue + lastValue = 5 + + parent = Parameter.create(name="parent", type="group") + outParam = interact(a, parent=parent) + assert outParam in parent.names.values() + outParam.activate() + assert lastValue == 5 From dfe8bc8d65bc4b44b9b09a0d5ca652a34bbf1265 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 9 Nov 2022 21:53:46 +0100 Subject: [PATCH 140/306] No longer a need to call `treeWidgetChanged` when setting button options. This is a hold-over from when a button was either added or removed from a tree depending on button options. Now, the button is always present and simply hidden/not hidden based on button options. Thus, the tree widget will never change as a result of button opts. --- pyqtgraph/parametertree/parameterTypes/actiongroup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes/actiongroup.py b/pyqtgraph/parametertree/parameterTypes/actiongroup.py index 58844d86c4..b69ab96943 100644 --- a/pyqtgraph/parametertree/parameterTypes/actiongroup.py +++ b/pyqtgraph/parametertree/parameterTypes/actiongroup.py @@ -35,7 +35,6 @@ def optsChanged(self, param, opts): if "button" in opts: buttonOpts = opts["button"] or dict(visible=False) self.button.updateOpts(param, buttonOpts) - self.treeWidgetChanged() super().optsChanged(param, opts) From 69bf2c786074ea2fbe950c31f1cfc7e57ddee42b Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Wed, 16 Nov 2022 11:04:06 -0500 Subject: [PATCH 141/306] bump dev version to correct value --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 59c64c3cd8..63eb4bbdfd 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.1.dev0' +__version__ = '0.13.2.dev0' ### import all the goodies and add some helper functions for easy CLI use From bf3820a2396224626ff63d4abd0e1916258692ab Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 5 Nov 2022 10:04:43 +0800 Subject: [PATCH 142/306] SignalProxy: add threadSafe=True option --- pyqtgraph/SignalProxy.py | 7 +++++-- pyqtgraph/imageview/ImageView.py | 6 +++++- pyqtgraph/parametertree/parameterTypes/checklist.py | 7 ++++++- pyqtgraph/parametertree/parameterTypes/pen.py | 7 ++++++- pyqtgraph/widgets/SpinBox.py | 7 ++++++- tests/test_signalproxy.py | 9 +++++---- 6 files changed, 33 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index ae0c8cb48e..75005b70fc 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -20,20 +20,23 @@ class SignalProxy(QtCore.QObject): sigDelayed = QtCore.Signal(object) - def __init__(self, signal, delay=0.3, rateLimit=0, slot=None): + def __init__(self, signal, delay=0.3, rateLimit=0, slot=None, *, threadSafe=True): """Initialization arguments: signal - a bound Signal or pyqtSignal instance delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s) slot - Optional function to connect sigDelayed to. rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a steady rate while they are being received. + threadSafe - Specify if thread-safety is required. For backwards compatibility, it + defaults to True. """ QtCore.QObject.__init__(self) self.delay = delay self.rateLimit = rateLimit self.args = None - self.timer = ThreadsafeTimer.ThreadsafeTimer() + Timer = ThreadsafeTimer if threadSafe else QtCore.QTimer + self.timer = Timer() self.timer.timeout.connect(self.flush) self.lastFlushTime = None self.signal = signal diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 7960ba2c62..06836e2181 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -219,7 +219,11 @@ def __init__( self.ui.normTimeRangeCheck.clicked.connect(self.updateNorm) self.playTimer.timeout.connect(self.timeout) - self.normProxy = SignalProxy(self.normRgn.sigRegionChanged, slot=self.updateNorm) + self.normProxy = SignalProxy( + self.normRgn.sigRegionChanged, + slot=self.updateNorm, + threadSafe=False, + ) self.normRoi.sigRegionChangeFinished.connect(self.updateNorm) self.ui.roiPlot.registerPlot(self.name + '_ROI') diff --git a/pyqtgraph/parametertree/parameterTypes/checklist.py b/pyqtgraph/parametertree/parameterTypes/checklist.py index 4106d59939..2232c19415 100644 --- a/pyqtgraph/parametertree/parameterTypes/checklist.py +++ b/pyqtgraph/parametertree/parameterTypes/checklist.py @@ -175,7 +175,12 @@ def __init__(self, **opts): # Also, value calculation will be incorrect until children are added, so make sure to recompute self.setValue(value) - self.valChangingProxy = SignalProxy(self.sigValueChanging, delay=opts.get('delay', 1.0), slot=self._finishChildChanges) + self.valChangingProxy = SignalProxy( + self.sigValueChanging, + delay=opts.get('delay', 1.0), + slot=self._finishChildChanges, + threadSafe=False, + ) def childrenValue(self): vals = [self.forward[p.name()] for p in self.children() if p.value()] diff --git a/pyqtgraph/parametertree/parameterTypes/pen.py b/pyqtgraph/parametertree/parameterTypes/pen.py index 87628f60ed..6a8d6aef12 100644 --- a/pyqtgraph/parametertree/parameterTypes/pen.py +++ b/pyqtgraph/parametertree/parameterTypes/pen.py @@ -46,7 +46,12 @@ def __init__(self, **opts): if 'children' in opts: raise KeyError('Cannot set "children" argument in Pen Parameter opts') super().__init__(**opts, children=list(children)) - self.valChangingProxy = SignalProxy(self.sigValueChanging, delay=1.0, slot=self._childrenFinishedChanging) + self.valChangingProxy = SignalProxy( + self.sigValueChanging, + delay=1.0, + slot=self._childrenFinishedChanging, + threadSafe=False, + ) def _childrenFinishedChanging(self, paramAndValue): self.sigValueChanged.emit(*paramAndValue) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 67a7e09e62..e893d0d8de 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -104,7 +104,12 @@ def __init__(self, parent=None, value=0.0, **kwargs): self.skipValidate = False self.setCorrectionMode(self.CorrectionMode.CorrectToPreviousValue) self.setKeyboardTracking(False) - self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) + self.proxy = SignalProxy( + self.sigValueChanging, + delay=self.opts['delay'], + slot=self.delayedChange, + threadSafe=False, + ) self.setOpts(**kwargs) self._updateHeight() diff --git a/tests/test_signalproxy.py b/tests/test_signalproxy.py index 68df12f661..b0bb2764d4 100644 --- a/tests/test_signalproxy.py +++ b/tests/test_signalproxy.py @@ -35,7 +35,7 @@ def test_signal_proxy_slot(qapp): sender = Sender(parent=qapp) receiver = Receiver(parent=qapp) proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6, - slot=receiver.slotReceive) + slot=receiver.slotReceive, threadSafe=False) assert proxy.blockSignal is False assert proxy is not None @@ -54,7 +54,7 @@ def test_signal_proxy_disconnect_slot(qapp): sender = Sender(parent=qapp) receiver = Receiver(parent=qapp) proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6, - slot=receiver.slotReceive) + slot=receiver.slotReceive, threadSafe=False) assert proxy.blockSignal is False assert proxy is not None @@ -77,7 +77,8 @@ def test_signal_proxy_no_slot_start(qapp): """Test the connect mode of SignalProxy without slot at start`""" sender = Sender(parent=qapp) receiver = Receiver(parent=qapp) - proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6) + proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6, + threadSafe=False) assert proxy.blockSignal is True assert proxy is not None @@ -107,7 +108,7 @@ def test_signal_proxy_slot_block(qapp): sender = Sender(parent=qapp) receiver = Receiver(parent=qapp) proxy = SignalProxy(sender.signalSend, delay=0.0, rateLimit=0.6, - slot=receiver.slotReceive) + slot=receiver.slotReceive, threadSafe=False) assert proxy.blockSignal is False assert proxy is not None From df4ffd35f2b577b7da136b50f4fed04b9d910395 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 4 Nov 2022 22:40:12 +0800 Subject: [PATCH 143/306] skip busycursor on newer PySide6 and linux --- tests/widgets/test_busycursor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/widgets/test_busycursor.py b/tests/widgets/test_busycursor.py index ef7de10349..b8d2d1ddef 100644 --- a/tests/widgets/test_busycursor.py +++ b/tests/widgets/test_busycursor.py @@ -1,8 +1,16 @@ +import pytest +import sys + import pyqtgraph as pg pg.mkQApp() - +@pytest.mark.skipif( + sys.platform.startswith("linux") + and pg.Qt.QT_LIB == "PySide6" + and pg.Qt.PySide6.__version_info__ > (6, 3), + reason="taking gui thread causes segfault" +) def test_nested_busy_cursors_clear_after_all_exit(): with pg.BusyCursor(): wait_cursor = pg.Qt.QtCore.Qt.CursorShape.WaitCursor From d94467fd0fed523370cd2725d0ff706c284295eb Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 5 Nov 2022 10:08:02 +0800 Subject: [PATCH 144/306] skip progressdialog on newer PySide6 and linux --- tests/widgets/test_progressdialog.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/widgets/test_progressdialog.py b/tests/widgets/test_progressdialog.py index 44673d974d..a449dc4773 100644 --- a/tests/widgets/test_progressdialog.py +++ b/tests/widgets/test_progressdialog.py @@ -1,8 +1,16 @@ -from pyqtgraph import ProgressDialog, mkQApp +import pytest +import sys -mkQApp() +import pyqtgraph as pg +pg.mkQApp() +@pytest.mark.skipif( + sys.platform.startswith("linux") + and pg.Qt.QT_LIB == "PySide6" + and pg.Qt.PySide6.__version_info__ > (6, 3), + reason="taking gui thread causes segfault" +) def test_progress_dialog(): - with ProgressDialog("test", 0, 1) as dlg: + with pg.ProgressDialog("test", 0, 1) as dlg: dlg += 1 From 6cb50969dd342694486d9e8b758987291b1c9543 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 4 Nov 2022 22:34:22 +0800 Subject: [PATCH 145/306] make leftMenu the child of topmenu --- .../graphicsItems/ViewBox/ViewBoxMenu.py | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 81b87c836c..b03f2bcc31 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -57,26 +57,18 @@ def __init__(self, view): self.ctrl[0].invertCheck.toggled.connect(self.xInvertToggled) self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled) - self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode")) + leftMenu = self.addMenu(translate("ViewBox", "Mouse Mode")) + group = QtGui.QActionGroup(self) - - # This does not work! QAction _must_ be initialized with a permanent - # object as the parent or else it may be collected prematurely. - #pan = self.leftMenu.addAction("3 button", self.set3ButtonMode) - #zoom = self.leftMenu.addAction("1 button", self.set1ButtonMode) - pan = QtGui.QAction(translate("ViewBox", "3 button"), self.leftMenu) - zoom = QtGui.QAction(translate("ViewBox", "1 button"), self.leftMenu) - self.leftMenu.addAction(pan) - self.leftMenu.addAction(zoom) - pan.triggered.connect(self.set3ButtonMode) - zoom.triggered.connect(self.set1ButtonMode) - + group.triggered.connect(self.setMouseMode) + pan = QtGui.QAction(translate("ViewBox", "3 button"), group) + zoom = QtGui.QAction(translate("ViewBox", "1 button"), group) pan.setCheckable(True) zoom.setCheckable(True) - pan.setActionGroup(group) - zoom.setActionGroup(group) + + leftMenu.addActions(group.actions()) + self.mouseModes = [pan, zoom] - self.addMenu(self.leftMenu) self.view().sigStateChanged.connect(self.viewStateChanged) @@ -200,11 +192,14 @@ def yInvertToggled(self, b): def xInvertToggled(self, b): self.view().invertX(b) - def set3ButtonMode(self): - self.view().setLeftButtonAction('pan') - - def set1ButtonMode(self): - self.view().setLeftButtonAction('rect') + def setMouseMode(self, action): + mode = None + if action == self.mouseModes[0]: + mode = 'pan' + elif action == self.mouseModes[1]: + mode = 'rect' + if mode is not None: + self.view().setLeftButtonAction(mode) def setViewList(self, views): names = [''] From 8a791819e74440adaa88097b658c1eba34ef6af0 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 5 Nov 2022 16:56:41 +0800 Subject: [PATCH 146/306] create axes submenu from topmenu --- pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index b03f2bcc31..617ad78054 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -19,22 +19,17 @@ def __init__(self, view): self.viewAll.triggered.connect(self.autoRange) self.addAction(self.viewAll) - self.axes = [] self.ctrl = [] self.widgetGroups = [] self.dv = QtGui.QDoubleValidator(self) for axis in 'XY': - m = QtWidgets.QMenu() - m.setTitle(f"{axis} {translate('ViewBox', 'axis')}") + m = self.addMenu(f"{axis} {translate('ViewBox', 'axis')}") w = QtWidgets.QWidget() ui = ui_template.Ui_Form() ui.setupUi(w) a = QtWidgets.QWidgetAction(self) a.setDefaultWidget(w) m.addAction(a) - self.addMenu(m) - m.setParent(None) # workaround PySide bug (present at least up to 6.4.0) - self.axes.append(m) self.ctrl.append(ui) wg = WidgetGroup(w) self.widgetGroups.append(wg) From b91991ea10a4dc9c572f1216ae0eaa387b28dd10 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Mon, 7 Nov 2022 21:04:55 +0800 Subject: [PATCH 147/306] remove test for live-ness of w.plotItem.getMenu() the test is actually bogus. the current implementation is such that it has no C++ children, thus qObjectTree() will return only one element. Yet if the menu did actually have any children, the test would fail on PyQt bindings; because the Python wrappers of the children are still alive (but the wrapped C++ objects are deleted) --- tests/test_ref_cycles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ref_cycles.py b/tests/test_ref_cycles.py index f602116e01..83d15a961c 100644 --- a/tests/test_ref_cycles.py +++ b/tests/test_ref_cycles.py @@ -48,7 +48,7 @@ def mkobjs(*args, **kwds): app.focusChanged.connect(w.plotItem.vb.invertY) # return weakrefs to a bunch of objects that should die when the scope exits. - return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getMenu(), w.plotItem.getAxis('left')) + return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getAxis('left')) for _ in range(5): assert_alldead(mkobjs()) From a6ddff1e72f981c100c65d54b4420a3f438cde7b Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 5 Nov 2022 17:06:11 +0800 Subject: [PATCH 148/306] PlotItem: create subMenus from ctrlMenu --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 713b4d4588..4bae23b982 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -196,18 +196,13 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None ] - self.ctrlMenu = QtWidgets.QMenu() - - self.ctrlMenu.setTitle(translate("PlotItem", 'Plot Options')) - self.subMenus = [] + self.ctrlMenu = QtWidgets.QMenu(translate("PlotItem", 'Plot Options')) + for name, grp in menuItems: - sm = QtWidgets.QMenu(name) + sm = self.ctrlMenu.addMenu(name) act = QtWidgets.QWidgetAction(self) act.setDefaultWidget(grp) sm.addAction(act) - self.subMenus.append(sm) - self.ctrlMenu.addMenu(sm) - sm.setParent(None) # workaround PySide bug (present at least up to 6.4.0) self.stateGroup = WidgetGroup() for name, w in menuItems: From c64a5e3ddbe08ac01df5a77e8be9cca4db3a4fa6 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 10 Nov 2022 21:57:07 +0800 Subject: [PATCH 149/306] crosshair.py: remove use of SignalProxy for mouse there is already a config option "mouseRateLimit" for such a purpose, thus using SignalProxy here would be a bad example. --- pyqtgraph/examples/crosshair.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/examples/crosshair.py b/pyqtgraph/examples/crosshair.py index 596aba85a2..ba35272964 100644 --- a/pyqtgraph/examples/crosshair.py +++ b/pyqtgraph/examples/crosshair.py @@ -67,7 +67,7 @@ def updateRegion(window, viewRange): vb = p1.vb def mouseMoved(evt): - pos = evt[0] ## using signal proxy turns original arguments into a tuple + pos = evt if p1.sceneBoundingRect().contains(pos): mousePoint = vb.mapSceneToView(pos) index = int(mousePoint.x()) @@ -78,8 +78,7 @@ def mouseMoved(evt): -proxy = pg.SignalProxy(p1.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved) -#p1.scene().sigMouseMoved.connect(mouseMoved) +p1.scene().sigMouseMoved.connect(mouseMoved) if __name__ == '__main__': From d9142ec637fd733b366c8533d61da4ff71f3a4b1 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 19 Nov 2022 13:46:13 +0800 Subject: [PATCH 150/306] skip busycursor and progressdialog for older pyside6 too --- tests/widgets/test_busycursor.py | 2 +- tests/widgets/test_progressdialog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/widgets/test_busycursor.py b/tests/widgets/test_busycursor.py index b8d2d1ddef..180a6e23d7 100644 --- a/tests/widgets/test_busycursor.py +++ b/tests/widgets/test_busycursor.py @@ -8,7 +8,7 @@ @pytest.mark.skipif( sys.platform.startswith("linux") and pg.Qt.QT_LIB == "PySide6" - and pg.Qt.PySide6.__version_info__ > (6, 3), + and pg.Qt.PySide6.__version_info__ > (6, 0), reason="taking gui thread causes segfault" ) def test_nested_busy_cursors_clear_after_all_exit(): diff --git a/tests/widgets/test_progressdialog.py b/tests/widgets/test_progressdialog.py index a449dc4773..50a580b467 100644 --- a/tests/widgets/test_progressdialog.py +++ b/tests/widgets/test_progressdialog.py @@ -8,7 +8,7 @@ @pytest.mark.skipif( sys.platform.startswith("linux") and pg.Qt.QT_LIB == "PySide6" - and pg.Qt.PySide6.__version_info__ > (6, 3), + and pg.Qt.PySide6.__version_info__ > (6, 0), reason="taking gui thread causes segfault" ) def test_progress_dialog(): From 8e4bab21c98750485fae74941f478efffb938b23 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 25 Nov 2022 17:12:15 -0500 Subject: [PATCH 151/306] Fix #2388 --- pyqtgraph/parametertree/parameterTypes/pen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes/pen.py b/pyqtgraph/parametertree/parameterTypes/pen.py index 87628f60ed..8bdcf100f1 100644 --- a/pyqtgraph/parametertree/parameterTypes/pen.py +++ b/pyqtgraph/parametertree/parameterTypes/pen.py @@ -49,7 +49,7 @@ def __init__(self, **opts): self.valChangingProxy = SignalProxy(self.sigValueChanging, delay=1.0, slot=self._childrenFinishedChanging) def _childrenFinishedChanging(self, paramAndValue): - self.sigValueChanged.emit(*paramAndValue) + self.setValue(self.pen) def saveState(self, filter=None): state = super().saveState(filter) From 66f5160c6f1513054193e829a368f524ba1388b0 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 25 Nov 2022 18:48:38 -0500 Subject: [PATCH 152/306] Add a default button to pen parameter --- pyqtgraph/parametertree/parameterTypes/pen.py | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/pen.py b/pyqtgraph/parametertree/parameterTypes/pen.py index 8bdcf100f1..2f0c3ad0b2 100644 --- a/pyqtgraph/parametertree/parameterTypes/pen.py +++ b/pyqtgraph/parametertree/parameterTypes/pen.py @@ -1,26 +1,52 @@ import re from contextlib import ExitStack -from . import GroupParameterItem +from . import GroupParameterItem, WidgetParameterItem from .basetypes import GroupParameter, Parameter, ParameterItem from .qtenum import QtEnumParameter from ... import functions as fn -from ...Qt import QtCore +from ...Qt import QtCore, QtWidgets from ...SignalProxy import SignalProxy from ...widgets.PenPreviewLabel import PenPreviewLabel class PenParameterItem(GroupParameterItem): def __init__(self, param, depth): + self.defaultBtn = self.makeDefaultButton() super().__init__(param, depth) + self.itemWidget = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + self.penLabel = PenPreviewLabel(param) + for child in self.penLabel, self.defaultBtn: + layout.addWidget(child) + self.itemWidget.setLayout(layout) + + def optsChanged(self, param, opts): + if "enabled" in opts or "readonly" in opts: + self.updateDefaultBtn() def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) tw = self.treeWidget() if tw is None: return - tw.setItemWidget(self, 1, self.penLabel - ) + tw.setItemWidget(self, 1, self.itemWidget) + + defaultClicked = WidgetParameterItem.defaultClicked + makeDefaultButton = WidgetParameterItem.makeDefaultButton + + def valueChanged(self, param, val): + self.updateDefaultBtn() + + def updateDefaultBtn(self): + self.defaultBtn.setEnabled( + not self.param.valueIsDefault() + and self.param.opts["enabled"] + and self.param.writable() + ) + class PenParameter(GroupParameter): """ @@ -51,6 +77,20 @@ def __init__(self, **opts): def _childrenFinishedChanging(self, paramAndValue): self.setValue(self.pen) + def setDefault(self, val): + pen = self._interpretValue(val) + with self.treeChangeBlocker(): + # Block changes until all are finalized + for opt in self.names: + # Booleans have different naming convention + if isinstance(self[opt], bool): + attrName = f'is{opt.title()}' + else: + attrName = opt + self.child(opt).setDefault(getattr(pen, attrName)()) + out = super().setDefault(val) + return out + def saveState(self, filter=None): state = super().saveState(filter) opts = state.pop('children') From 5f38340811dea3225cce86f43087d89529458d9b Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 26 Nov 2022 20:26:21 +0800 Subject: [PATCH 153/306] fix: instantiate QApplication for test_Parameter.py --- tests/parametertree/test_Parameter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/parametertree/test_Parameter.py b/tests/parametertree/test_Parameter.py index 29a3021b68..4ede448cdc 100644 --- a/tests/parametertree/test_Parameter.py +++ b/tests/parametertree/test_Parameter.py @@ -1,6 +1,7 @@ from functools import wraps import numpy as np +import pyqtgraph as pg import pytest from pyqtgraph import functions as fn @@ -15,6 +16,7 @@ from pyqtgraph.parametertree.parameterTypes import GroupParameter as GP from pyqtgraph.Qt import QtGui +pg.mkQApp() def test_parameter_hasdefault(): opts = {"name": "param", "type": int, "value": 1} From 844f5efb12ea362a90f08c0af8a9e6ea6dd4ae15 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Fri, 2 Dec 2022 18:07:28 -0500 Subject: [PATCH 154/306] Improve `ChecklistParameter.setValue` logic. Previously, calling `setValue` would properly fill in child checklist widgets, but wouldn't populate `self.opts["value"]` with the correct option. This was because setting `blockSigal=self._onChildChanging` meant `childrenValues()` wasn't notified that child values were updated. Simply creating and passing a `valueToSet` object prevents these issues. --- .../parametertree/parameterTypes/checklist.py | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/checklist.py b/pyqtgraph/parametertree/parameterTypes/checklist.py index 2232c19415..8cf4179737 100644 --- a/pyqtgraph/parametertree/parameterTypes/checklist.py +++ b/pyqtgraph/parametertree/parameterTypes/checklist.py @@ -239,23 +239,44 @@ def optsChanged(self, param, opts): def setValue(self, value, blockSignal=None): self.targetValue = value - exclusive = self.opts['exclusive'] - # Will emit at the end, so no problem discarding existing changes - cmpVals = value if isinstance(value, list) else [value] - for ii in range(len(cmpVals)-1, -1, -1): - exists = any(fn.eq(cmpVals[ii], lim) for lim in self.reverse[0]) - if not exists: - del cmpVals[ii] - names = [self.reverse[1][self.reverse[0].index(val)] for val in cmpVals] - if exclusive and len(names) > 1: - names = [names[0]] - elif exclusive and not len(names) and len(self.forward): - # An option is required during exclusivity - names = [self.reverse[1][0]] + if not isinstance(value, list): + value = [value] + names, values = self._intersectionWithLimits(value) + valueToSet = values + + if self.opts['exclusive']: + if len(self.forward): + # Exclusive means at least one entry must exist, grab from limits + # if they exist + names.append(self.reverse[1][0]) + if len(names) > 1: + names = names[:1] + if not len(names): + valueToSet = None + else: + valueToSet = self.forward[names[0]] + for chParam in self: checked = chParam.name() in names + # Will emit at the end, so no problem discarding existing changes chParam.setValue(checked, self._onChildChanging) - super().setValue(self.childrenValue(), blockSignal) + super().setValue(valueToSet, blockSignal) + + def _intersectionWithLimits(self, values: list): + """ + Returns the (names, values) from limits that intersect with ``values``. + """ + allowedNames = [] + allowedValues = [] + # Could be replaced by "value in self.reverse[0]" and "reverse[0].index", + # but this allows for using pg.eq to cover more diverse value options + for val in values: + for limitName, limitValue in zip(*self.reverse): + if fn.eq(limitValue, val): + allowedNames.append(limitName) + allowedValues.append(val) + break + return allowedNames, allowedValues def setToDefault(self): # Since changing values are covered by a proxy, this method must be overridden From 89b4e75ea3b8e246db062a1a19fe490a6c7f4486 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 4 Dec 2022 22:49:00 +0800 Subject: [PATCH 155/306] remove resizeEvent on screen change size is given in virtual pixel size. thus even on change to a screen with a different device pixel ratio, the (virtual) size does not change. --- pyqtgraph/opengl/GLViewWidget.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 2a078be5c8..0a55984a08 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -58,21 +58,6 @@ def __init__(self, parent=None, devicePixelRatio=None, rotationMethod='euler'): self.keyTimer = QtCore.QTimer() self.keyTimer.timeout.connect(self.evalKeyState) - def _updateScreen(self, screen): - self._updatePixelRatio() - if screen is not None: - screen.physicalDotsPerInchChanged.connect(self._updatePixelRatio) - screen.logicalDotsPerInchChanged.connect(self._updatePixelRatio) - - def _updatePixelRatio(self): - event = QtGui.QResizeEvent(self.size(), self.size()) - self.resizeEvent(event) - - def showEvent(self, event): - window = self.window().windowHandle() - window.screenChanged.connect(self._updateScreen) - self._updateScreen(window.screen()) - def deviceWidth(self): dpr = self.devicePixelRatioF() return int(self.width() * dpr) From 9a482aaca1f8c44d6e4e6d6f1b540af0b93816fb Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Thu, 8 Dec 2022 19:20:14 -0500 Subject: [PATCH 156/306] use ThreadsafeTimer class instead of module --- pyqtgraph/SignalProxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 75005b70fc..5f31b25cc1 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -1,9 +1,9 @@ import weakref from time import perf_counter -from . import ThreadsafeTimer from .functions import SignalBlock from .Qt import QtCore +from .ThreadsafeTimer import ThreadsafeTimer __all__ = ['SignalProxy'] From e6e58cf88eda3dfdbbc8c3fdac570f4d5d5b1b29 Mon Sep 17 00:00:00 2001 From: Zhengwu Date: Wed, 14 Dec 2022 11:34:51 +0800 Subject: [PATCH 157/306] Update Used By list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c96b36446e..d78c0a0fc1 100644 --- a/README.md +++ b/README.md @@ -138,5 +138,6 @@ Here is a partial listing of some of the applications that make use of PyQtGraph * [PySpectra](http://hasyweb.desy.de/services/computing/Spock/node138.html) * [rapidtide](https://rapidtide.readthedocs.io/en/latest/) * [Semi-Supervised Semantic Annotator](https://gitlab.com/s3a/s3a) +* [STDF-Viewer](https://github.com/noonchen/STDF-Viewer) Do you use PyQtGraph in your own project, and want to add it to the list? Submit a pull request to update this listing! From 9e126fb36f56048d3546bc10822a9208ac9baf8c Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 14 Dec 2022 11:32:58 +0800 Subject: [PATCH 158/306] compute floating point boundingRect --- pyqtgraph/examples/customGraphicsItem.py | 6 ++++-- pyqtgraph/graphicsItems/BarGraphItem.py | 4 +--- pyqtgraph/graphicsItems/NonUniformImage.py | 9 +++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/examples/customGraphicsItem.py b/pyqtgraph/examples/customGraphicsItem.py index ff03e4b413..17edf16ae0 100644 --- a/pyqtgraph/examples/customGraphicsItem.py +++ b/pyqtgraph/examples/customGraphicsItem.py @@ -19,6 +19,7 @@ def __init__(self, data): def generatePicture(self): ## pre-computing a QPicture object allows paint() to run much more quickly, ## rather than re-drawing the shapes every time. + bounds = QtCore.QRectF() self.picture = QtGui.QPicture() p = QtGui.QPainter(self.picture) p.setPen(pg.mkPen('w')) @@ -30,7 +31,9 @@ def generatePicture(self): else: p.setBrush(pg.mkBrush('g')) p.drawRect(QtCore.QRectF(t-w, open, w*2, close-open)) + bounds |= QtCore.QRectF(t-w, min, w*2, max-min) p.end() + self._bounds = bounds def paint(self, p, *args): p.drawPicture(0, 0, self.picture) @@ -38,8 +41,7 @@ def paint(self, p, *args): def boundingRect(self): ## boundingRect _must_ indicate the entire area that will be drawn on ## or else we will get artifacts and possibly crashing. - ## (in this case, QPicture does all the work of computing the bouning rect for us) - return QtCore.QRectF(self.picture.boundingRect()) + return self._bounds data = [ ## fields are (time, open, close, min, max). (1., 10, 13, 5, 15), diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 40bf5c2823..56a28dc072 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -159,9 +159,7 @@ def paint(self, p, *args): self.picture.play(p) def boundingRect(self): - if self.picture is None: - self.drawPicture() - return QtCore.QRectF(self.picture.boundingRect()) + return self.shape().controlPointRect() def shape(self): if self.picture is None: diff --git a/pyqtgraph/graphicsItems/NonUniformImage.py b/pyqtgraph/graphicsItems/NonUniformImage.py index b148fac876..c3388756b2 100644 --- a/pyqtgraph/graphicsItems/NonUniformImage.py +++ b/pyqtgraph/graphicsItems/NonUniformImage.py @@ -76,6 +76,7 @@ def generatePicture(self): x, y, z = self.data + bounds = QtCore.QRectF() self.picture = QtGui.QPicture() p = QtGui.QPainter(self.picture) p.setPen(mkPen(None)) @@ -116,7 +117,11 @@ def generatePicture(self): b = y[0] if j == 0 else (y[j - 1] + y[j]) / 2 t = (y[j] + y[j + 1]) / 2 if j < y.size - 1 else y[-1] - p.drawRect(QtCore.QRectF(l, t, r - l, b - t)) + rect = QtCore.QRectF(l, t, r - l, b - t) + bounds |= rect + p.drawRect(rect) + + self._bounds = bounds if self.border is not None: p.setPen(self.border) @@ -131,4 +136,4 @@ def paint(self, p, *args): p.drawPicture(0, 0, self.picture) def boundingRect(self): - return QtCore.QRectF(self.picture.boundingRect()) + return self._bounds From 629e1e5f64ceb4e1257eb005956920dd5d488e4e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 20 Dec 2022 16:15:23 +0800 Subject: [PATCH 159/306] draw example histogram using BarGraphItem --- pyqtgraph/examples/histogram.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/examples/histogram.py b/pyqtgraph/examples/histogram.py index f09361bb28..9e46b4e0b7 100644 --- a/pyqtgraph/examples/histogram.py +++ b/pyqtgraph/examples/histogram.py @@ -7,7 +7,7 @@ import pyqtgraph as pg win = pg.GraphicsLayoutWidget(show=True) -win.resize(800,350) +win.resize(800,480) win.setWindowTitle('pyqtgraph example: Histogram') plt1 = win.addPlot() plt2 = win.addPlot() @@ -23,9 +23,14 @@ plt1.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0,0,255,150)) ## Now draw all points as a nicely-spaced scatter plot -y = pg.pseudoScatter(vals, spacing=0.15) -#plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5) -plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150)) +psy = pg.pseudoScatter(vals, spacing=0.15) +plt2.plot(vals, psy, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150)) + +# draw histogram using BarGraphItem +win.nextRow() +plt3 = win.addPlot() +bgi = pg.BarGraphItem(x0=x[:-1], x1=x[1:], height=y, pen='w', brush=(0,0,255,150)) +plt3.addItem(bgi) if __name__ == '__main__': pg.exec() From 26ce697cad38980f4c208cc6486fb8336d0048fa Mon Sep 17 00:00:00 2001 From: "M.H" Date: Fri, 23 Dec 2022 16:54:27 -0600 Subject: [PATCH 160/306] add PatchView to application list (#2559) * add PatchView to application list * a2z --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d78c0a0fc1..f9df0f7bdd 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ Here is a partial listing of some of the applications that make use of PyQtGraph * [Joulescope](https://www.joulescope.com/) * [neurotic](https://neurotic.readthedocs.io) * [Orange3](https://orangedatamining.com/) +* [PatchView](https://github.com/ZeitgeberH/patchview) * [pyplotter](https://github.com/pyplotter/pyplotter) * [PyMeasure](https://github.com/pymeasure/pymeasure) * [PySpectra](http://hasyweb.desy.de/services/computing/Spock/node138.html) @@ -140,4 +141,5 @@ Here is a partial listing of some of the applications that make use of PyQtGraph * [Semi-Supervised Semantic Annotator](https://gitlab.com/s3a/s3a) * [STDF-Viewer](https://github.com/noonchen/STDF-Viewer) + Do you use PyQtGraph in your own project, and want to add it to the list? Submit a pull request to update this listing! From 6fc4bf97e31872a85981a74800e16f2860533ac6 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 23 Dec 2022 14:55:21 -0800 Subject: [PATCH 161/306] Update Discord Invite Link Used a non-expiring #pyqtgraph channel invite link to the python discord server. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9df0f7bdd..40398205b7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ PyQtGraph [![Documentation Status](https://readthedocs.org/projects/pyqtgraph/badge/?version=latest)](https://pyqtgraph.readthedocs.io/en/latest/?badge=latest) [![Total alerts](https://img.shields.io/lgtm/alerts/g/pyqtgraph/pyqtgraph.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/pyqtgraph/pyqtgraph/alerts/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/pyqtgraph/pyqtgraph.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/pyqtgraph/pyqtgraph/context:python) -[![Discord](https://img.shields.io/discord/946624673200893953.svg?label=PyQtGraph&logo=discord)](https://discord.gg/3Qxjz5BF) +[![Discord](https://img.shields.io/discord/946624673200893953.svg?label=PyQtGraph&logo=discord)](https://discord.gg/ufTVNNreAZ) A pure-Python graphics library for PyQt5/PyQt6/PySide2/PySide6 Copyright 2022 PyQtGraph developers From f52673851c72a814fe1be95ecb169ee2a66d46f3 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 23 Dec 2022 15:46:27 -0800 Subject: [PATCH 162/306] Revert "compute floating point boundingRect" --- pyqtgraph/examples/customGraphicsItem.py | 6 ++---- pyqtgraph/graphicsItems/BarGraphItem.py | 4 +++- pyqtgraph/graphicsItems/NonUniformImage.py | 9 ++------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/examples/customGraphicsItem.py b/pyqtgraph/examples/customGraphicsItem.py index 17edf16ae0..ff03e4b413 100644 --- a/pyqtgraph/examples/customGraphicsItem.py +++ b/pyqtgraph/examples/customGraphicsItem.py @@ -19,7 +19,6 @@ def __init__(self, data): def generatePicture(self): ## pre-computing a QPicture object allows paint() to run much more quickly, ## rather than re-drawing the shapes every time. - bounds = QtCore.QRectF() self.picture = QtGui.QPicture() p = QtGui.QPainter(self.picture) p.setPen(pg.mkPen('w')) @@ -31,9 +30,7 @@ def generatePicture(self): else: p.setBrush(pg.mkBrush('g')) p.drawRect(QtCore.QRectF(t-w, open, w*2, close-open)) - bounds |= QtCore.QRectF(t-w, min, w*2, max-min) p.end() - self._bounds = bounds def paint(self, p, *args): p.drawPicture(0, 0, self.picture) @@ -41,7 +38,8 @@ def paint(self, p, *args): def boundingRect(self): ## boundingRect _must_ indicate the entire area that will be drawn on ## or else we will get artifacts and possibly crashing. - return self._bounds + ## (in this case, QPicture does all the work of computing the bouning rect for us) + return QtCore.QRectF(self.picture.boundingRect()) data = [ ## fields are (time, open, close, min, max). (1., 10, 13, 5, 15), diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 56a28dc072..40bf5c2823 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -159,7 +159,9 @@ def paint(self, p, *args): self.picture.play(p) def boundingRect(self): - return self.shape().controlPointRect() + if self.picture is None: + self.drawPicture() + return QtCore.QRectF(self.picture.boundingRect()) def shape(self): if self.picture is None: diff --git a/pyqtgraph/graphicsItems/NonUniformImage.py b/pyqtgraph/graphicsItems/NonUniformImage.py index c3388756b2..b148fac876 100644 --- a/pyqtgraph/graphicsItems/NonUniformImage.py +++ b/pyqtgraph/graphicsItems/NonUniformImage.py @@ -76,7 +76,6 @@ def generatePicture(self): x, y, z = self.data - bounds = QtCore.QRectF() self.picture = QtGui.QPicture() p = QtGui.QPainter(self.picture) p.setPen(mkPen(None)) @@ -117,11 +116,7 @@ def generatePicture(self): b = y[0] if j == 0 else (y[j - 1] + y[j]) / 2 t = (y[j] + y[j + 1]) / 2 if j < y.size - 1 else y[-1] - rect = QtCore.QRectF(l, t, r - l, b - t) - bounds |= rect - p.drawRect(rect) - - self._bounds = bounds + p.drawRect(QtCore.QRectF(l, t, r - l, b - t)) if self.border is not None: p.setPen(self.border) @@ -136,4 +131,4 @@ def paint(self, p, *args): p.drawPicture(0, 0, self.picture) def boundingRect(self): - return self._bounds + return QtCore.QRectF(self.picture.boundingRect()) From b2de5fc580c7fa148a4742591043370ddedf766a Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 31 Dec 2022 15:09:40 +0800 Subject: [PATCH 163/306] implement BarGraphItem dataBounds and pixelPadding and implement boundingRect in terms of dataBounds and pixelPadding --- pyqtgraph/graphicsItems/BarGraphItem.py | 48 +++++++++++++++++++++---- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 40bf5c2823..0df743d877 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -121,6 +121,8 @@ def asarray(x): p.setPen(fn.mkPen(pen)) p.setBrush(fn.mkBrush(brush)) + dataBounds = QtCore.QRectF() + pixelPadding = 0 for i in range(len(x0 if not np.isscalar(x0) else y0)): if pens is not None: p.setPen(fn.mkPen(pens[i])) @@ -148,8 +150,23 @@ def asarray(x): rect = QtCore.QRectF(x, y, w, h) p.drawRect(rect) self._shape.addRect(rect) - + + pen = p.pen() + pw = pen.widthF() + if pen.style() == QtCore.Qt.PenStyle.NoPen: + pw = 0 + elif pw == 0: + pw = 1 + pw *= 0.5 + if pen.isCosmetic(): + dataBounds |= rect + pixelPadding = max(pixelPadding, pw) + else: + dataBounds |= rect.adjusted(-pw, -pw, pw, pw) + p.end() + self._dataBounds = dataBounds + self._pixelPadding = pixelPadding self.prepareGeometryChange() @@ -158,11 +175,6 @@ def paint(self, p, *args): self.drawPicture() self.picture.play(p) - def boundingRect(self): - if self.picture is None: - self.drawPicture() - return QtCore.QRectF(self.picture.boundingRect()) - def shape(self): if self.picture is None: self.drawPicture() @@ -179,3 +191,27 @@ def name(self): def getData(self): return self.opts.get('x'), self.opts.get('height') + + def dataBounds(self, ax, frac=1.0, orthoRange=None): + if self.picture is None: + self.drawPicture() + l, t, r, b = self._dataBounds.getCoords() + return [l, r] if ax == 0 else [t, b] + + def pixelPadding(self): + if self.picture is None: + self.drawPicture() + return self._pixelPadding + + def boundingRect(self): + px = py = 0 + pxPad = self.pixelPadding() + if pxPad > 0: + # determine length of pixel in local x, y directions + px, py = self.pixelVectors() + px = 0 if px is None else px.length() + py = 0 if py is None else py.length() + # return bounds expanded by pixel size + px *= pxPad + py *= pxPad + return self._dataBounds.adjusted(-px, -py, px, py) From 120d4a352e91741cb508a9e35dd8f7417a442f56 Mon Sep 17 00:00:00 2001 From: Neil Girdhar Date: Wed, 4 Jan 2023 16:35:36 -0500 Subject: [PATCH 164/306] Remove antiquated Qt crash prevention --- pyqtgraph/__init__.py | 86 ++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 54 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 63eb4bbdfd..11f02d356c 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -35,8 +35,8 @@ elif 'darwin' in sys.platform: ## openGL can have a major impact on mac, but also has serious bugs useOpenGL = False else: - useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. - + useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. + CONFIG_OPTIONS = { 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. 'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox @@ -72,9 +72,9 @@ def setConfigOption(opt, value): CONFIG_OPTIONS[opt] = value def setConfigOptions(**opts): - """Set global configuration options. - - Each keyword argument sets one global option. + """Set global configuration options. + + Each keyword argument sets one global option. """ for k,v in opts.items(): setConfigOption(k, v) @@ -90,7 +90,7 @@ def systemInfo(): print("sys.version: %s" % sys.version) from .Qt import VERSION_INFO print("qt bindings: %s" % VERSION_INFO) - + global __version__ rev = None if __version__ is None: ## this code was probably checked out from bzr; look up the last-revision file @@ -98,7 +98,7 @@ def systemInfo(): if os.path.exists(lastRevFile): with open(lastRevFile, 'r') as fd: rev = fd.read().strip() - + print("pyqtgraph: %s; %s" % (__version__, rev)) print("config:") import pprint @@ -106,16 +106,16 @@ def systemInfo(): ## Rename orphaned .pyc files. This is *probably* safe :) ## We only do this if __version__ is None, indicating the code was probably pulled -## from the repository. +## from the repository. def renamePyc(startDir): ### Used to rename orphaned .pyc files ### When a python file changes its location in the repository, usually the .pyc file - ### is left behind, possibly causing mysterious and difficult to track bugs. + ### is left behind, possibly causing mysterious and difficult to track bugs. ### Note that this is no longer necessary for python 3.2; from PEP 3147: - ### "If the py source file is missing, the pyc file inside __pycache__ will be ignored. + ### "If the py source file is missing, the pyc file inside __pycache__ will be ignored. ### This eliminates the problem of accidental stale pyc file imports." - + printed = False startDir = os.path.abspath(startDir) for path, dirs, files in os.walk(startDir): @@ -138,7 +138,7 @@ def renamePyc(startDir): print(" " + fileName + " ==>") print(" " + name2) os.rename(fileName, name2) - + path = os.path.split(__file__)[0] ## Import almost everything to make it available from a single namespace @@ -147,8 +147,8 @@ def renamePyc(startDir): #from . import frozenSupport #def importModules(path, globals, locals, excludes=()): #"""Import all modules residing within *path*, return a dict of name: module pairs. - - #Note that *path* MUST be relative to the module doing the import. + + #Note that *path* MUST be relative to the module doing the import. #""" #d = os.path.join(os.path.split(globals['__file__'])[0], path) #files = set() @@ -159,7 +159,7 @@ def renamePyc(startDir): #files.add(f[:-3]) #elif f[-4:] == '.pyc' and f != '__init__.pyc': #files.add(f[:-4]) - + #mods = {} #path = path.replace(os.sep, '.') #for modName in files: @@ -176,7 +176,7 @@ def renamePyc(startDir): #traceback.print_stack() #sys.excepthook(*sys.exc_info()) #print("[Error importing module: %s]" % modName) - + #return mods #def importAll(path, globals, locals, excludes=()): @@ -288,7 +288,7 @@ def renamePyc(startDir): from .widgets.VerticalLabel import * ############################################################## -## PyQt and PySide both are prone to crashing on exit. +## PyQt and PySide both are prone to crashing on exit. ## There are two general approaches to dealing with this: ## 1. Install atexit handlers that assist in tearing down to avoid crashes. ## This helps, but is never perfect. @@ -300,34 +300,12 @@ def cleanup(): global _cleanupCalled if _cleanupCalled: return - + if not getConfigOption('exitCleanup'): return - + ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore. - - ## Workaround for Qt exit crash: - ## ALL QGraphicsItems must have a scene before they are deleted. - ## This is potentially very expensive, but preferred over crashing. - ## Note: this appears to be fixed in PySide as of 2012.12, but it should be left in for a while longer.. - app = QtWidgets.QApplication.instance() - if app is None or not isinstance(app, QtWidgets.QApplication): - # app was never constructed is already deleted or is an - # QCoreApplication/QGuiApplication and not a full QApplication - return - import gc - s = QtWidgets.QGraphicsScene() - for o in gc.get_objects(): - try: - if isinstance(o, QtWidgets.QGraphicsItem) and isQObjectAlive(o) and o.scene() is None: - if getConfigOption('crashWarning'): - sys.stderr.write('Error: graphics item without scene. ' - 'Make sure ViewBox.close() and GraphicsView.close() ' - 'are properly called before app shutdown (%s)\n' % (o,)) - - s.addItem(o) - except (RuntimeError, ReferenceError): ## occurs if a python wrapper no longer has its underlying C++ object - continue + _cleanupCalled = True atexit.register(cleanup) @@ -349,28 +327,28 @@ def _connectCleanup(): def exit(): """ Causes python to exit without garbage-collecting any objects, and thus avoids - calling object destructor methods. This is a sledgehammer workaround for + calling object destructor methods. This is a sledgehammer workaround for a variety of bugs in PyQt and Pyside that cause crashes on exit. - + This function does the following in an attempt to 'safely' terminate the process: - + * Invoke atexit callbacks * Close all open file handles * os._exit() - + Note: there is some potential for causing damage with this function if you are using objects that _require_ their destructors to be called (for example, to properly terminate log files, disconnect from devices, etc). Situations like this are probably quite rare, but use at your own risk. """ - + ## first disable our own cleanup function; won't be needing it. setConfigOptions(exitCleanup=False) - + ## invoke atexit callbacks atexit._run_exitfuncs() - + ## close file handles if sys.platform == 'darwin': for fd in range(3, 4096): @@ -384,7 +362,7 @@ def exit(): os.closerange(3, 4096) ## just guessing on the maximum descriptor count.. os._exit(0) - + ## Convenience functions for command-line use plots = [] @@ -393,7 +371,7 @@ def exit(): def plot(*args, **kargs): """ - Create and return a :class:`PlotWidget ` + Create and return a :class:`PlotWidget ` Accepts a *title* argument to set the title of the window. All other arguments are used to plot data. (see :func:`PlotItem.plot() `) """ @@ -417,7 +395,7 @@ def plot(*args, **kargs): def image(*args, **kargs): """ - Create and return an :class:`ImageView ` + Create and return an :class:`ImageView ` Will show 2D or 3D image data. Accepts a *title* argument to set the title of the window. All other arguments are used to show data. (see :func:`ImageView.setImage() `) @@ -436,7 +414,7 @@ def image(*args, **kargs): def dbg(*args, **kwds): """ Create a console window and begin watching for exceptions. - + All arguments are passed to :func:`ConsoleWidget.__init__() `. """ mkQApp() @@ -455,7 +433,7 @@ def dbg(*args, **kwds): def stack(*args, **kwds): """ Create a console window and show the current stack trace. - + All arguments are passed to :func:`ConsoleWidget.__init__() `. """ mkQApp() From 8eeddbd8175f1ab077b069508529e227406ffe4d Mon Sep 17 00:00:00 2001 From: Kosuke Tahara Date: Mon, 9 Jan 2023 17:36:05 +0900 Subject: [PATCH 165/306] Accept custom ROI objects for ImageView --- pyqtgraph/imageview/ImageView.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 06836e2181..5f72625874 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -88,6 +88,8 @@ def __init__( imageItem=None, levelMode='mono', discreteTimeLine=False, + roi=None, + normRoi=None, *args, ): """ @@ -116,6 +118,10 @@ def __init__( See the *levelMode* argument to :func:`HistogramLUTItem.__init__() ` discreteTimeLine : bool Whether to snap to xvals / frame numbers when interacting with the timeline position. + roi : ROI + If specified, this object is used as ROI for the plot feature. Must be an instance of ROI. + normRoi : ROI + If specified, this object is used as ROI for the normalization feature. Must be an instance of ROI. """ QtWidgets.QWidget.__init__(self, parent, *args) self._imageLevels = None # [(min, max), ...] per channel image metrics @@ -145,12 +151,18 @@ def __init__( self.ui.normGroup.hide() - self.roi = PlotROI(10) + if roi is None: + self.roi = PlotROI(10) + else: + self.roi = roi self.roi.setZValue(20) self.view.addItem(self.roi) self.roi.hide() - self.normRoi = PlotROI(10) - self.normRoi.setPen('y') + if normRoi is None: + self.normRoi = PlotROI(10) + self.normRoi.setPen('y') + else: + self.normRoi = normRoi self.normRoi.setZValue(20) self.view.addItem(self.normRoi) self.normRoi.hide() From a11d10992711cd4d5058111e846831926020a383 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 10 Jan 2023 04:14:33 +0100 Subject: [PATCH 166/306] Use base_prefix to detect virtual env (#2566) * use base_prefix to detect virtual env * add links to github issues for future reference --- pyqtgraph/multiprocess/processes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 7f8e343fd6..06a2f1e44f 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -136,7 +136,10 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de # the multiprocessing connection. Technically, only the launched subprocess needs to # send its pid back. Practically, we hijack the ppid parameter to indicate to the # subprocess that pid exchange is needed. - xchg_pids = sys.platform == 'win32' and os.getenv('VIRTUAL_ENV') is not None + # + # We detect a virtual environment using sys.base_prefix, see https://docs.python.org/3/library/sys.html#sys.base_prefix + # See https://github.com/pyqtgraph/pyqtgraph/pull/2566 and https://github.com/spyder-ide/spyder/issues/20273 + xchg_pids = sys.platform == 'win32' and sys.prefix != sys.base_prefix ## Send everything the remote process needs to start correctly data = dict( From 4b19ff2548d7d290d61dddf43b92dfec5221bd13 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 15 Jan 2023 15:19:27 +0800 Subject: [PATCH 167/306] typo: dataRange -> dataBounds --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6ad9d56952..2b8a0d5038 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -844,7 +844,7 @@ def enableAutoRange(self, axis=None, enable=True, x=None, y=None): (if *axis* is omitted, both axes will be changed). When enabled, the axis will automatically rescale when items are added/removed or change their shape. The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should - be visible (this only works with items implementing a dataRange method, such as PlotDataItem). + be visible (this only works with items implementing a dataBounds method, such as PlotDataItem). """ # support simpler interface: if x is not None or y is not None: From 1437a3ac34b362dfee711d1c6dff2a3e9c934b8a Mon Sep 17 00:00:00 2001 From: koutoftimer Date: Thu, 19 Jan 2023 08:39:33 +0200 Subject: [PATCH 168/306] Maintain argument propagatoin to super class (#2516) * Maintain argument propagatoin to super class All classes from `pyqtgraph.opengl.__init__` based on `GLGraphicsItem` didn't respect `parentItem` argument, which leads to `setParentItem()` call each time you want to group up graphics. * Adds partial typehints for GLGraphicsItem * Adds tests for opengl items * Ignore tests if OpenGL module is absent --- pyqtgraph/opengl/GLGraphicsItem.py | 6 +++--- pyqtgraph/opengl/items/GLAxisItem.py | 4 ++-- pyqtgraph/opengl/items/GLBarGraphItem.py | 4 ++-- pyqtgraph/opengl/items/GLBoxItem.py | 4 ++-- pyqtgraph/opengl/items/GLGradientLegendItem.py | 4 ++-- pyqtgraph/opengl/items/GLGraphItem.py | 4 ++-- pyqtgraph/opengl/items/GLGridItem.py | 4 ++-- pyqtgraph/opengl/items/GLImageItem.py | 4 ++-- pyqtgraph/opengl/items/GLLinePlotItem.py | 4 ++-- pyqtgraph/opengl/items/GLMeshItem.py | 4 ++-- pyqtgraph/opengl/items/GLScatterPlotItem.py | 4 ++-- pyqtgraph/opengl/items/GLSurfacePlotItem.py | 4 ++-- pyqtgraph/opengl/items/GLTextItem.py | 4 ++-- pyqtgraph/opengl/items/GLVolumeItem.py | 4 ++-- tests/opengl/items/common.py | 6 ++++++ tests/opengl/items/test_GLAxisItem.py | 13 +++++++++++++ tests/opengl/items/test_GLBarGraphItem.py | 15 +++++++++++++++ tests/opengl/items/test_GLBoxItem.py | 13 +++++++++++++ tests/opengl/items/test_GLGradientLegendItem.py | 13 +++++++++++++ tests/opengl/items/test_GLGraphItem.py | 13 +++++++++++++ tests/opengl/items/test_GLGridItem.py | 13 +++++++++++++ tests/opengl/items/test_GLImageItem.py | 13 +++++++++++++ tests/opengl/items/test_GLLinePlotItem.py | 13 +++++++++++++ tests/opengl/items/test_GLMeshItem.py | 13 +++++++++++++ tests/opengl/items/test_GLScatterPlotItem.py | 13 +++++++++++++ tests/opengl/items/test_GLSurfacePlotItem.py | 13 +++++++++++++ tests/opengl/items/test_GLTextItem.py | 13 +++++++++++++ tests/opengl/items/test_GLVolumeItem.py | 13 +++++++++++++ 28 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 tests/opengl/items/common.py create mode 100644 tests/opengl/items/test_GLAxisItem.py create mode 100644 tests/opengl/items/test_GLBarGraphItem.py create mode 100644 tests/opengl/items/test_GLBoxItem.py create mode 100644 tests/opengl/items/test_GLGradientLegendItem.py create mode 100644 tests/opengl/items/test_GLGraphItem.py create mode 100644 tests/opengl/items/test_GLGridItem.py create mode 100644 tests/opengl/items/test_GLImageItem.py create mode 100644 tests/opengl/items/test_GLLinePlotItem.py create mode 100644 tests/opengl/items/test_GLMeshItem.py create mode 100644 tests/opengl/items/test_GLScatterPlotItem.py create mode 100644 tests/opengl/items/test_GLSurfacePlotItem.py create mode 100644 tests/opengl/items/test_GLTextItem.py create mode 100644 tests/opengl/items/test_GLVolumeItem.py diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index 4a232b7ee3..e8efa6dc2c 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -31,14 +31,14 @@ class GLGraphicsItem(QtCore.QObject): _nextId = 0 - def __init__(self, parentItem=None): + def __init__(self, parentItem: 'GLGraphicsItem' = None): super().__init__() self._id = GLGraphicsItem._nextId GLGraphicsItem._nextId += 1 - self.__parent = None + self.__parent: GLGraphicsItem | None = None self.__view = None - self.__children = set() + self.__children: set[GLGraphicsItem] = set() self.__transform = Transform3D() self.__visible = True self.__initialized = False diff --git a/pyqtgraph/opengl/items/GLAxisItem.py b/pyqtgraph/opengl/items/GLAxisItem.py index 44a66fc329..a4a5ce8e9b 100644 --- a/pyqtgraph/opengl/items/GLAxisItem.py +++ b/pyqtgraph/opengl/items/GLAxisItem.py @@ -12,8 +12,8 @@ class GLAxisItem(GLGraphicsItem): """ - def __init__(self, size=None, antialias=True, glOptions='translucent'): - GLGraphicsItem.__init__(self) + def __init__(self, size=None, antialias=True, glOptions='translucent', parentItem=None): + super().__init__(parentItem=parentItem) if size is None: size = QtGui.QVector3D(1,1,1) self.antialias = antialias diff --git a/pyqtgraph/opengl/items/GLBarGraphItem.py b/pyqtgraph/opengl/items/GLBarGraphItem.py index 9760403dd4..98c1dab04c 100644 --- a/pyqtgraph/opengl/items/GLBarGraphItem.py +++ b/pyqtgraph/opengl/items/GLBarGraphItem.py @@ -7,7 +7,7 @@ class GLBarGraphItem(GLMeshItem): - def __init__(self, pos, size): + def __init__(self, pos, size, parentItem=None): """ pos is (...,3) array of the bar positions (the corner of each bar) size is (...,3) array of the sizes of each bar @@ -27,4 +27,4 @@ def __init__(self, pos, size): faces = cubeFaces + (np.arange(nCubes) * 8).reshape(nCubes,1,1) md = MeshData(verts.reshape(nCubes*8,3), faces.reshape(nCubes*12,3)) - GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False) + super().__init__(meshdata=md, shader='shaded', smooth=False, parentItem=parentItem) diff --git a/pyqtgraph/opengl/items/GLBoxItem.py b/pyqtgraph/opengl/items/GLBoxItem.py index 9c743d4f4d..2f8f68a9ad 100644 --- a/pyqtgraph/opengl/items/GLBoxItem.py +++ b/pyqtgraph/opengl/items/GLBoxItem.py @@ -11,8 +11,8 @@ class GLBoxItem(GLGraphicsItem): Displays a wire-frame box. """ - def __init__(self, size=None, color=None, glOptions='translucent'): - GLGraphicsItem.__init__(self) + def __init__(self, size=None, color=None, glOptions='translucent', parentItem=None): + super().__init__(parentItem=parentItem) if size is None: size = QtGui.QVector3D(1,1,1) self.setSize(size=size) diff --git a/pyqtgraph/opengl/items/GLGradientLegendItem.py b/pyqtgraph/opengl/items/GLGradientLegendItem.py index 9453221eec..35285f1997 100644 --- a/pyqtgraph/opengl/items/GLGradientLegendItem.py +++ b/pyqtgraph/opengl/items/GLGradientLegendItem.py @@ -10,7 +10,7 @@ class GLGradientLegendItem(GLGraphicsItem): Displays legend colorbar on the screen. """ - def __init__(self, **kwds): + def __init__(self, parentItem=None, **kwds): """ Arguments: pos: position of the colorbar on the screen, from the top left corner, in pixels @@ -24,7 +24,7 @@ def __init__(self, **kwds): size as percentage legend title """ - GLGraphicsItem.__init__(self) + super().__init__(parentItem=parentItem) glopts = kwds.pop("glOptions", "additive") self.setGLOptions(glopts) self.pos = (10, 10) diff --git a/pyqtgraph/opengl/items/GLGraphItem.py b/pyqtgraph/opengl/items/GLGraphItem.py index e5e3f875b7..987a41eb01 100644 --- a/pyqtgraph/opengl/items/GLGraphItem.py +++ b/pyqtgraph/opengl/items/GLGraphItem.py @@ -14,8 +14,8 @@ class GLGraphItem(GLGraphicsItem): Useful for drawing networks, trees, etc. """ - def __init__(self, **kwds): - GLGraphicsItem.__init__(self) + def __init__(self, parentItem=None, **kwds): + super().__init__(parentItem=parentItem) self.edges = None self.edgeColor = QtGui.QColor(QtCore.Qt.GlobalColor.white) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 59436ec0be..49e1ba9e28 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -14,8 +14,8 @@ class GLGridItem(GLGraphicsItem): Displays a wire-frame grid. """ - def __init__(self, size=None, color=(255, 255, 255, 76.5), antialias=True, glOptions='translucent'): - GLGraphicsItem.__init__(self) + def __init__(self, size=None, color=(255, 255, 255, 76.5), antialias=True, glOptions='translucent', parentItem=None): + super().__init__(parentItem=parentItem) self.setGLOptions(glOptions) self.antialias = antialias if size is None: diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 460055d946..0dcecc872c 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -13,7 +13,7 @@ class GLImageItem(GLGraphicsItem): """ - def __init__(self, data, smooth=False, glOptions='translucent'): + def __init__(self, data, smooth=False, glOptions='translucent', parentItem=None): """ ============== ======================================================================================= @@ -26,7 +26,7 @@ def __init__(self, data, smooth=False, glOptions='translucent'): self.smooth = smooth self._needUpdate = False - GLGraphicsItem.__init__(self) + super().__init__(parentItem=parentItem) self.setData(data) self.setGLOptions(glOptions) self.texture = None diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index b1c5b364aa..c3e626ed6f 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -10,9 +10,9 @@ class GLLinePlotItem(GLGraphicsItem): """Draws line plots in 3D.""" - def __init__(self, **kwds): + def __init__(self, parentItem=None, **kwds): """All keyword arguments are passed to setData()""" - GLGraphicsItem.__init__(self) + super().__init__(parentItem=parentItem) glopts = kwds.pop('glOptions', 'additive') self.setGLOptions(glopts) self.pos = None diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index 9a3f4de67b..f333e5d58a 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -14,7 +14,7 @@ class GLMeshItem(GLGraphicsItem): Displays a 3D triangle mesh. """ - def __init__(self, **kwds): + def __init__(self, parentItem=None, **kwds): """ ============== ===================================================== **Arguments:** @@ -47,7 +47,7 @@ def __init__(self, **kwds): 'computeNormals': True, } - GLGraphicsItem.__init__(self) + super().__init__(parentItem=parentItem) glopts = kwds.pop('glOptions', 'opaque') self.setGLOptions(glopts) shader = kwds.pop('shader', None) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index eb61d86510..3fb384592b 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -11,8 +11,8 @@ class GLScatterPlotItem(GLGraphicsItem): """Draws points at a list of 3D positions.""" - def __init__(self, **kwds): - super().__init__() + def __init__(self, parentItem=None, **kwds): + super().__init__(parentItem=parentItem) glopts = kwds.pop('glOptions', 'additive') self.setGLOptions(glopts) self.pos = None diff --git a/pyqtgraph/opengl/items/GLSurfacePlotItem.py b/pyqtgraph/opengl/items/GLSurfacePlotItem.py index 655cf15b51..aa40ca0db7 100644 --- a/pyqtgraph/opengl/items/GLSurfacePlotItem.py +++ b/pyqtgraph/opengl/items/GLSurfacePlotItem.py @@ -12,7 +12,7 @@ class GLSurfacePlotItem(GLMeshItem): Displays a surface plot on a regular x,y grid """ - def __init__(self, x=None, y=None, z=None, colors=None, **kwds): + def __init__(self, x=None, y=None, z=None, colors=None, parentItem=None, **kwds): """ The x, y, z, and colors arguments are passed to setData(). All other keyword arguments are passed to GLMeshItem.__init__(). @@ -24,7 +24,7 @@ def __init__(self, x=None, y=None, z=None, colors=None, **kwds): self._color = None self._vertexes = None self._meshdata = MeshData() - GLMeshItem.__init__(self, meshdata=self._meshdata, **kwds) + super().__init__(parentItem=parentItem, meshdata=self._meshdata, **kwds) self.setData(x, y, z, colors) diff --git a/pyqtgraph/opengl/items/GLTextItem.py b/pyqtgraph/opengl/items/GLTextItem.py index 1cb6d0d2b3..4c7a2555af 100644 --- a/pyqtgraph/opengl/items/GLTextItem.py +++ b/pyqtgraph/opengl/items/GLTextItem.py @@ -10,9 +10,9 @@ class GLTextItem(GLGraphicsItem): """Draws text in 3D.""" - def __init__(self, **kwds): + def __init__(self, parentItem=None, **kwds): """All keyword arguments are passed to setData()""" - GLGraphicsItem.__init__(self) + super().__init__(parentItem=parentItem) glopts = kwds.pop('glOptions', 'additive') self.setGLOptions(glopts) diff --git a/pyqtgraph/opengl/items/GLVolumeItem.py b/pyqtgraph/opengl/items/GLVolumeItem.py index 2fd73cb0d9..a1c28d3afb 100644 --- a/pyqtgraph/opengl/items/GLVolumeItem.py +++ b/pyqtgraph/opengl/items/GLVolumeItem.py @@ -14,7 +14,7 @@ class GLVolumeItem(GLGraphicsItem): """ - def __init__(self, data, sliceDensity=1, smooth=True, glOptions='translucent'): + def __init__(self, data, sliceDensity=1, smooth=True, glOptions='translucent', parentItem=None): """ ============== ======================================================================================= **Arguments:** @@ -29,7 +29,7 @@ def __init__(self, data, sliceDensity=1, smooth=True, glOptions='translucent'): self.data = None self._needUpload = False self.texture = None - GLGraphicsItem.__init__(self) + super().__init__(parentItem=parentItem) self.setGLOptions(glOptions) self.setData(data) diff --git a/tests/opengl/items/common.py b/tests/opengl/items/common.py new file mode 100644 index 0000000000..3e6e078753 --- /dev/null +++ b/tests/opengl/items/common.py @@ -0,0 +1,6 @@ +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem + + +def ensure_parentItem(parent: GLGraphicsItem, child: GLGraphicsItem): + assert child in parent.childItems() + assert parent is child.parentItem() diff --git a/tests/opengl/items/test_GLAxisItem.py b/tests/opengl/items/test_GLAxisItem.py new file mode 100644 index 0000000000..695c042045 --- /dev/null +++ b/tests/opengl/items/test_GLAxisItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLAxisItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLAxisItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLBarGraphItem.py b/tests/opengl/items/test_GLBarGraphItem.py new file mode 100644 index 0000000000..a9478b3357 --- /dev/null +++ b/tests/opengl/items/test_GLBarGraphItem.py @@ -0,0 +1,15 @@ +import pytest +pytest.importorskip('OpenGL') + +import numpy as np + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLBarGraphItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLBarGraphItem(np.ndarray([0,0,0]), np.ndarray([0,0,0]), parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLBoxItem.py b/tests/opengl/items/test_GLBoxItem.py new file mode 100644 index 0000000000..2ff53ee4df --- /dev/null +++ b/tests/opengl/items/test_GLBoxItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLBoxItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLBoxItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLGradientLegendItem.py b/tests/opengl/items/test_GLGradientLegendItem.py new file mode 100644 index 0000000000..e9b31833d4 --- /dev/null +++ b/tests/opengl/items/test_GLGradientLegendItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLGradientLegendItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLGradientLegendItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLGraphItem.py b/tests/opengl/items/test_GLGraphItem.py new file mode 100644 index 0000000000..fd66629393 --- /dev/null +++ b/tests/opengl/items/test_GLGraphItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLGraphItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLGraphItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLGridItem.py b/tests/opengl/items/test_GLGridItem.py new file mode 100644 index 0000000000..d42d6a2306 --- /dev/null +++ b/tests/opengl/items/test_GLGridItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLGridItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLGridItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLImageItem.py b/tests/opengl/items/test_GLImageItem.py new file mode 100644 index 0000000000..695c042045 --- /dev/null +++ b/tests/opengl/items/test_GLImageItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLAxisItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLAxisItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLLinePlotItem.py b/tests/opengl/items/test_GLLinePlotItem.py new file mode 100644 index 0000000000..5db6410bd2 --- /dev/null +++ b/tests/opengl/items/test_GLLinePlotItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLLinePlotItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLLinePlotItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLMeshItem.py b/tests/opengl/items/test_GLMeshItem.py new file mode 100644 index 0000000000..3b8e69856b --- /dev/null +++ b/tests/opengl/items/test_GLMeshItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLMeshItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLMeshItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLScatterPlotItem.py b/tests/opengl/items/test_GLScatterPlotItem.py new file mode 100644 index 0000000000..d7d10d71c2 --- /dev/null +++ b/tests/opengl/items/test_GLScatterPlotItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLScatterPlotItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLScatterPlotItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLSurfacePlotItem.py b/tests/opengl/items/test_GLSurfacePlotItem.py new file mode 100644 index 0000000000..3b98f285b3 --- /dev/null +++ b/tests/opengl/items/test_GLSurfacePlotItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLSurfacePlotItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLSurfacePlotItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLTextItem.py b/tests/opengl/items/test_GLTextItem.py new file mode 100644 index 0000000000..fc74b5ea2b --- /dev/null +++ b/tests/opengl/items/test_GLTextItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLTextItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLTextItem(parentItem=parent) + ensure_parentItem(parent, child) diff --git a/tests/opengl/items/test_GLVolumeItem.py b/tests/opengl/items/test_GLVolumeItem.py new file mode 100644 index 0000000000..104abeb4c6 --- /dev/null +++ b/tests/opengl/items/test_GLVolumeItem.py @@ -0,0 +1,13 @@ +import pytest +pytest.importorskip('OpenGL') + +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.opengl import GLVolumeItem + +from common import ensure_parentItem + + +def test_parentItem(): + parent = GLGraphicsItem() + child = GLVolumeItem(None, parentItem=parent) + ensure_parentItem(parent, child) From ac19304847b7d9bd1cfdd9f7d56ee1ecd7900553 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 14 Jan 2023 21:53:09 +0800 Subject: [PATCH 169/306] PColorMeshItem: implement dataBounds and pixelPadding --- pyqtgraph/graphicsItems/PColorMeshItem.py | 58 ++++++++++++++++++----- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index 401073a5ec..5643e325f5 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -12,12 +12,6 @@ __all__ = ['PColorMeshItem'] -if Qt.QT_LIB.startswith('PyQt'): - wrapinstance = Qt.sip.wrapinstance -else: - wrapinstance = Qt.shiboken.wrapInstance - - class QuadInstances: def __init__(self): self.polys = [] @@ -27,7 +21,7 @@ def alloc(self, size): # 2 * (size + 1) vertices, (x, y) arr = np.empty((2 * (size + 1), 2), dtype=np.float64) - ptrs = list(map(wrapinstance, + ptrs = list(map(Qt.compat.wrapinstance, itertools.count(arr.ctypes.data, arr.strides[0]), itertools.repeat(QtCore.QPointF, arr.shape[0]))) @@ -102,6 +96,7 @@ def __init__(self, *args, **kwargs): edgecolors : dict, optional The color of the edges of the polygons. Default None means no edges. + Only cosmetic pens are supported. The dict may contains any arguments accepted by :func:`mkColor() `. Example: ``mkPen(color='w', width=2)`` antialiasing : bool, default False @@ -115,8 +110,14 @@ def __init__(self, *args, **kwargs): self.x = None self.y = None self.z = None + self._dataBounds = None self.edgecolors = kwargs.get('edgecolors', None) + if self.edgecolors is not None: + self.edgecolors = fn.mkPen(self.edgecolors) + # force the pen to be cosmetic. see discussion in + # https://github.com/pyqtgraph/pyqtgraph/pull/2586 + self.edgecolors.setCosmetic(True) self.antialiasing = kwargs.get('antialiasing', False) self.levels = kwargs.get('levels', None) self.enableAutoLevels = kwargs.get('enableAutoLevels', True) @@ -164,6 +165,8 @@ def _prepareData(self, args): self.x = None self.y = None self.z = None + + self._dataBounds = None # User only specified z elif len(args)==1: @@ -173,6 +176,8 @@ def _prepareData(self, args): self.x, self.y = np.meshgrid(x, y, indexing='ij') self.z = args[0] + self._dataBounds = ((x[0], x[-1]), (y[0], y[-1])) + # User specified x, y, z elif len(args)==3: @@ -187,6 +192,10 @@ def _prepareData(self, args): self.y = args[1] self.z = args[2] + xmn, xmx = np.min(self.x), np.max(self.x) + ymn, ymx = np.min(self.y), np.max(self.y) + self._dataBounds = ((xmn, xmx), (ymn, ymx)) + else: ValueError('Data must been sent as (z) or (x, y, z)') @@ -242,7 +251,7 @@ def setData(self, *args, **kwargs): if self.edgecolors is None: painter.setPen(QtCore.Qt.PenStyle.NoPen) else: - painter.setPen(fn.mkPen(self.edgecolors)) + painter.setPen(self.edgecolors) if self.antialiasing: painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) @@ -327,7 +336,34 @@ def height(self): + def dataBounds(self, ax, frac=1.0, orthoRange=None): + if self._dataBounds is None: + return (None, None) + return self._dataBounds[ax] + + def pixelPadding(self): + # pen is known to be cosmetic + pen = self.edgecolors + no_pen = (pen is None) or (pen.style() == QtCore.Qt.PenStyle.NoPen) + return 0 if no_pen else (pen.widthF() or 1) * 0.5 + def boundingRect(self): - if self.qpicture is None: - return QtCore.QRectF(0., 0., 0., 0.) - return QtCore.QRectF(self.qpicture.boundingRect()) + xmn, xmx = self.dataBounds(ax=0) + if xmn is None or xmx is None: + return QtCore.QRectF() + ymn, ymx = self.dataBounds(ax=1) + if ymn is None or ymx is None: + return QtCore.QRectF() + + px = py = 0 + pxPad = self.pixelPadding() + if pxPad > 0: + # determine length of pixel in local x, y directions + px, py = self.pixelVectors() + px = 0 if px is None else px.length() + py = 0 if py is None else py.length() + # return bounds expanded by pixel size + px *= pxPad + py *= pxPad + + return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) From 27c019ff4e137a989615a6ec021f8da60142159a Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 15 Jan 2023 14:18:28 +0800 Subject: [PATCH 170/306] pcmi: fix width() and height() --- pyqtgraph/graphicsItems/PColorMeshItem.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index 5643e325f5..14a727a52e 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -322,19 +322,16 @@ def setBorder(self, b): def width(self): - if self.x is None: - return None - return np.max(self.x) - - + if self._dataBounds is None: + return 0 + bounds = self._dataBounds[0] + return bounds[1]-bounds[0] def height(self): - if self.y is None: - return None - return np.max(self.y) - - - + if self._dataBounds is None: + return 0 + bounds = self._dataBounds[1] + return bounds[1]-bounds[0] def dataBounds(self, ax, frac=1.0, orthoRange=None): if self._dataBounds is None: From 282f9f9e8bfc9b6cfa6612cb737bc9d575415dc6 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Mon, 16 Jan 2023 21:36:48 +0800 Subject: [PATCH 171/306] keep fps textitem position fixed --- pyqtgraph/examples/PColorMeshItem.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/examples/PColorMeshItem.py b/pyqtgraph/examples/PColorMeshItem.py index a1e2cc7396..4338c10a0b 100644 --- a/pyqtgraph/examples/PColorMeshItem.py +++ b/pyqtgraph/examples/PColorMeshItem.py @@ -67,6 +67,12 @@ color_speed = 0.3 color_noise_freq = 0.05 +# display info in top-right corner +miny = np.min(y) - wave_amplitude +maxy = np.max(y) + wave_amplitude +view.setYRange(miny, maxy) +textitem.setPos(np.max(x), maxy) + timer = QtCore.QTimer() timer.setSingleShot(True) # not using QTimer.singleShot() because of persistence on PyQt. see PR #1605 @@ -91,11 +97,7 @@ def updateData(): i += wave_speed - # display info in top-right corner textitem.setText(f'{(t2 - t1)*1000:.1f} ms') - if textpos is None: - textpos = pcmi.width(), pcmi.height() - textitem.setPos(*textpos) # cap update rate at fps delay = max(1000/fps - (t2 - t0), 0) From b6dbb8f258c92230461b460220b4c3572c8dac82 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 19 Jan 2023 20:54:45 +0800 Subject: [PATCH 172/306] NonUniformImage: implement floating point boundingRect --- pyqtgraph/graphicsItems/NonUniformImage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/NonUniformImage.py b/pyqtgraph/graphicsItems/NonUniformImage.py index b148fac876..7c18f567bc 100644 --- a/pyqtgraph/graphicsItems/NonUniformImage.py +++ b/pyqtgraph/graphicsItems/NonUniformImage.py @@ -131,4 +131,5 @@ def paint(self, p, *args): p.drawPicture(0, 0, self.picture) def boundingRect(self): - return QtCore.QRectF(self.picture.boundingRect()) + x, y, _ = self.data + return QtCore.QRectF(x[0], y[0], x[-1]-x[0], y[-1]-y[0]) From 9675bd9ae0e128dda4bf542fc0242274a92efec5 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 24 Jan 2023 09:09:12 +0800 Subject: [PATCH 173/306] create internals.PrimitiveArray --- pyqtgraph/Qt/internals.py | 84 +++++++++++++++++++++- pyqtgraph/graphicsItems/PlotCurveItem.py | 38 +++------- pyqtgraph/graphicsItems/ScatterPlotItem.py | 65 +++-------------- 3 files changed, 104 insertions(+), 83 deletions(-) diff --git a/pyqtgraph/Qt/internals.py b/pyqtgraph/Qt/internals.py index 3ef44c86c4..a4d200176f 100644 --- a/pyqtgraph/Qt/internals.py +++ b/pyqtgraph/Qt/internals.py @@ -1,10 +1,14 @@ import ctypes +import itertools import numpy as np -from . import QT_LIB +from . import QT_LIB, QtCore, QtGui from . import compat __all__ = ["get_qpainterpath_element_array"] +if QT_LIB.startswith('PyQt'): + from . import sip + class QArrayDataQt5(ctypes.Structure): _fields_ = [ ("ref", ctypes.c_int), @@ -69,3 +73,81 @@ def get_qpainterpath_element_array(qpath, nelems=None): vp = compat.voidptr(eptr, itemsize*nelems, writable) return np.frombuffer(vp, dtype=dtype) + +class PrimitiveArray: + # QPainter has a C++ native API that takes an array of objects: + # drawPrimitives(const Primitive *array, int count, ...) + # where "Primitive" is one of QPointF, QLineF, QRectF, PixmapFragment + # + # PySide (with the exception of drawPixmapFragments) and older PyQt + # require a Python list of "Primitive" instances to be provided to + # the respective "drawPrimitives" method. + # + # This is inefficient because: + # 1) constructing the Python list involves calling wrapinstance multiple times. + # - this is mitigated here by reusing the instance pointers + # 2) The binding will anyway have to repack the instances into a contiguous array, + # in order to call the underlying C++ native API. + # + # Newer PyQt provides sip.array, which is more efficient. + # + # PySide's drawPixmapFragments() takes an instance to the first item of a + # C array of PixmapFragment(s) _and_ the length of the array. + # There is no overload that takes a Python list of PixmapFragment(s). + + def __init__(self, Klass, nfields, method=None): + self._Klass = Klass + self._nfields = nfields + self._ndarray = None + + if QT_LIB.startswith('PyQt'): + if method is None: + method = ( + hasattr(sip, 'array') and + ( + (0x60301 <= QtCore.PYQT_VERSION) or + (0x50f07 <= QtCore.PYQT_VERSION < 0x60000) + ) + ) + self.use_sip_array = method + else: + self.use_sip_array = False + + if QT_LIB.startswith('PySide'): + if method is None: + method = ( + Klass is QtGui.QPainter.PixmapFragment + ) + self.use_ptr_to_array = method + else: + self.use_ptr_to_array = False + + self.resize(0) + + def resize(self, size): + if self._ndarray is not None and len(self._ndarray) == size: + return + + if self.use_sip_array: + self._objs = sip.array(self._Klass, size) + vp = sip.voidptr(self._objs, size*self._nfields*8) + array = np.frombuffer(vp, dtype=np.float64).reshape((-1, self._nfields)) + elif self.use_ptr_to_array: + array = np.empty((size, self._nfields), dtype=np.float64) + self._objs = compat.wrapinstance(array.ctypes.data, self._Klass) + else: + array = np.empty((size, self._nfields), dtype=np.float64) + self._objs = list(map(compat.wrapinstance, + itertools.count(array.ctypes.data, array.strides[0]), + itertools.repeat(self._Klass, array.shape[0]))) + + self._ndarray = array + + def __len__(self): + return len(self._ndarray) + + def ndarray(self): + return self._ndarray + + def instances(self): + return self._objs diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 0a50c706fd..e261c8e050 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -1,7 +1,6 @@ from ..Qt import QtCore, QtGui, QtWidgets HAVE_OPENGL = hasattr(QtWidgets, 'QOpenGLWidget') -import itertools import math import sys import warnings @@ -40,35 +39,20 @@ def have_native_drawlines_array(): class LineSegments: def __init__(self): - self.use_sip_array = ( - Qt.QT_LIB.startswith('PyQt') and - hasattr(Qt.sip, 'array') and - ( - (0x60301 <= QtCore.PYQT_VERSION) or - (0x50f07 <= QtCore.PYQT_VERSION < 0x60000) - ) - ) + method = None + + # "use_native_drawlines" is pending the following issue and code review + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1924 + # https://codereview.qt-project.org/c/pyside/pyside-setup/+/415702 self.use_native_drawlines = Qt.QT_LIB.startswith('PySide') and _have_native_drawlines_array - self.alloc(0) - - def alloc(self, size): - if self.use_sip_array: - self.objs = Qt.sip.array(QtCore.QLineF, size) - vp = Qt.sip.voidptr(self.objs, len(self.objs)*4*8) - self.arr = np.frombuffer(vp, dtype=np.float64).reshape((-1, 4)) - elif self.use_native_drawlines: - self.arr = np.empty((size, 4), dtype=np.float64) - self.objs = Qt.compat.wrapinstance(self.arr.ctypes.data, QtCore.QLineF) - else: - self.arr = np.empty((size, 4), dtype=np.float64) - self.objs = list(map(Qt.compat.wrapinstance, - itertools.count(self.arr.ctypes.data, self.arr.strides[0]), - itertools.repeat(QtCore.QLineF, self.arr.shape[0]))) + if self.use_native_drawlines: + method = True + + self.array = Qt.internals.PrimitiveArray(QtCore.QLineF, 4, method=method) def get(self, size): - if size != self.arr.shape[0]: - self.alloc(size) - return self.objs, self.arr + self.array.resize(size) + return self.array.instances(), self.array.ndarray() def arrayToLineSegments(self, x, y, connect, finiteCheck): # analogue of arrayToQPath taking the same parameters diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index fd94132f00..ee6e2290aa 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -9,7 +9,7 @@ from .. import functions as fn from .. import getConfigOption from ..Point import Point -from ..Qt import QT_LIB, QtCore, QtGui +from ..Qt import QtCore, QtGui from .GraphicsObject import GraphicsObject __all__ = ['ScatterPlotItem', 'SpotItem'] @@ -133,57 +133,6 @@ def _mkBrush(*args, **kwargs): return fn.mkBrush(*args, **kwargs) -class PixmapFragments: - def __init__(self): - self.use_sip_array = ( - Qt.QT_LIB.startswith('PyQt') and - hasattr(Qt.sip, 'array') and - ( - (0x60301 <= QtCore.PYQT_VERSION) or - (0x50f07 <= QtCore.PYQT_VERSION < 0x60000) - ) - ) - self.alloc(0) - - def alloc(self, size): - # The C++ native API is: - # drawPixmapFragments(const PixmapFragment *fragments, int fragmentCount, - # const QPixmap &pixmap) - # - # PySide exposes this API whereas PyQt wraps it to be more Pythonic. - # In PyQt, a Python list of PixmapFragment instances needs to be provided. - # This is inefficient because: - # 1) constructing the Python list involves calling sip.wrapinstance multiple times. - # - this is mitigated here by reusing the instance pointers - # 2) PyQt will anyway deconstruct the Python list and repack the PixmapFragment - # instances into a contiguous array, in order to call the underlying C++ native API. - if self.use_sip_array: - self.objs = Qt.sip.array(QtGui.QPainter.PixmapFragment, size) - vp = Qt.sip.voidptr(self.objs, len(self.objs)*10*8) - self.arr = np.frombuffer(vp, dtype=np.float64).reshape((-1, 10)) - else: - self.arr = np.empty((size, 10), dtype=np.float64) - if QT_LIB.startswith('PyQt'): - self.objs = list(map(Qt.sip.wrapinstance, - itertools.count(self.arr.ctypes.data, self.arr.strides[0]), - itertools.repeat(QtGui.QPainter.PixmapFragment, self.arr.shape[0]))) - else: - self.objs = Qt.shiboken.wrapInstance(self.arr.ctypes.data, QtGui.QPainter.PixmapFragment) - - def array(self, size): - if size != self.arr.shape[0]: - self.alloc(size) - return self.arr - - def draw(self, painter, pixmap): - if not len(self.arr): - return - if QT_LIB.startswith('PyQt'): - painter.drawPixmapFragments(self.objs, pixmap) - else: - painter.drawPixmapFragments(self.objs, len(self.arr), pixmap) - - class SymbolAtlas(object): """ Used to efficiently construct a single QPixmap containing all rendered symbols @@ -427,7 +376,7 @@ def __init__(self, *args, **kargs): self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots - self._pixmapFragments = PixmapFragments() + self._pixmapFragments = Qt.internals.PrimitiveArray(QtGui.QPainter.PixmapFragment, 10) self.opts = { 'pxMode': True, 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. @@ -1002,13 +951,19 @@ def paint(self, p, *args): xy = pts[:, viewMask].T sr = self.data['sourceRect'][viewMask] - frags = self._pixmapFragments.array(sr.size) + self._pixmapFragments.resize(sr.size) + frags = self._pixmapFragments.ndarray() frags[:, 0:2] = xy frags[:, 2:6] = np.frombuffer(sr, dtype=int).reshape((-1, 4)) # sx, sy, sw, sh frags[:, 6:10] = [1.0, 1.0, 0.0, 1.0] # scaleX, scaleY, rotation, opacity profiler('prep') - self._pixmapFragments.draw(p, self.fragmentAtlas.pixmap) + inst = self._pixmapFragments.instances() + if Qt.QT_LIB.startswith('PySide'): + args = inst, sr.size + else: + args = inst, + p.drawPixmapFragments(*args, self.fragmentAtlas.pixmap) profiler('draw') else: # render each symbol individually From 54142b8dba1db3222e427c94e7e4929d02d645ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20E=2E=20S=C3=B8rensen?= Date: Thu, 26 Jan 2023 06:36:07 +0100 Subject: [PATCH 174/306] PColorMeshItem colorbar support (#2477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add keyword argument in PColorMeshItem to enable consistent colormap scaling during animation. * Update PColorMeshItem example to illustrate usage of keyword arguments `colorMap` and `limit` * Rename limits and cmaplim to levels in order to conform with naming convention in ImageItem and ColorBarItem * Add autoLevels parameters to PColorMeshItem Add enableAutoLevels parameter to PColorMeshItem and autoLevels parameter to PColorMeshItem.setData() in order to conform with known APIs from ViewBox and ImageItem * Update PColorMeshItem.py Fix typos and improve code quality * Experimental support for Colorbar and PColorMeshItem Experimental first draft of ColorBar support in PColorMeshItem. Working prototype, but has flawed implementation that always disables autoLevels (autoscaling of colors) when the colorbar is added. Documentation is copied from ImageItem and has not been modified at all yet. * Add ColorBarItem support for PColorMeshItem Add consistent logic for when ColorBarItem is in control of PColorMeshItem's levels, and the other way around. Add a separate plot in examples/PColorMeshItem.py to showcase both dynamic/autoscaling colorbars and static/consistent colorbars in the same example. * Update PColorMeshItem.py Fix conflicting name between method and variable * Update PColorMeshItem.py Remove an unnecessary TODO comment (based on a mixup between colormap and LUT) * Remove commented out code * Revert behavior of ColorBarItem's colormap priority ColorBarItem will pick a colormap from the "first and best" assigned image in cases where the ColorBarItem does not have a colormap explicitly assigned. * Revert change to (interactive) ColorBarItem that made it attempt to disable auto-scaling Remove logic that made ColorBarItem attempt to disable auto-scaling for its assigned image items. This should instead be done manually by the user. Co-authored-by: Simen E. Sørensen --- pyqtgraph/examples/PColorMeshItem.py | 43 +++++++++--- pyqtgraph/graphicsItems/ColorBarItem.py | 78 ++++++++++++++++----- pyqtgraph/graphicsItems/PColorMeshItem.py | 83 +++++++++++++++++++++-- 3 files changed, 171 insertions(+), 33 deletions(-) diff --git a/pyqtgraph/examples/PColorMeshItem.py b/pyqtgraph/examples/PColorMeshItem.py index 4338c10a0b..dc4baeec7e 100644 --- a/pyqtgraph/examples/PColorMeshItem.py +++ b/pyqtgraph/examples/PColorMeshItem.py @@ -15,7 +15,8 @@ win = pg.GraphicsLayoutWidget() win.show() ## show widget alone in its own window win.setWindowTitle('pyqtgraph example: pColorMeshItem') -view = win.addViewBox() +view_auto_scale = win.addPlot(0,0,1,1, title="Auto-scaling colorscheme", enableMenu=False) +view_consistent_scale = win.addPlot(1,0,1,1, title="Consistent colorscheme", enableMenu=False) ## Create data @@ -41,21 +42,40 @@ # z being the color of the polygons its shape must be decreased by one in each dimension z = np.exp(-(x*xn)**2/1000)[:-1,:-1] -## Create image item +## Create autoscaling image item edgecolors = None antialiasing = False cmap = pg.colormap.get('viridis') levels = (-2,2) # Will be overwritten unless enableAutoLevels is set to False -enableAutoLevels = True # edgecolors = {'color':'w', 'width':2} # May be uncommented to see edgecolor effect # antialiasing = True # May be uncommented to see antialiasing effect # cmap = pg.colormap.get('plasma') # May be uncommented to see a different colormap than the default 'viridis' -# enableAutoLevels = False # may be uncommented to see changes in the absolute value of z (color_noise) which is hidden by the autoscaling colormap when using the default `levels=None` -pcmi = pg.PColorMeshItem(edgecolors=edgecolors, antialiasing=antialiasing, colorMap=cmap, levels=levels, enableAutoLevels=enableAutoLevels) -view.addItem(pcmi) +pcmi_auto = pg.PColorMeshItem(edgecolors=edgecolors, antialiasing=antialiasing, colorMap=cmap, levels=levels, enableAutoLevels=True) +view_auto_scale.addItem(pcmi_auto) + +# Add colorbar +bar = pg.ColorBarItem( + label = "Z value [arbitrary unit]", + interactive=False, # Setting `interactive=True` would override `enableAutoLevels=True` of pcmi_auto (resulting in consistent colors) + rounding=0.1) +bar.setImageItem( [pcmi_auto] ) +win.addItem(bar, 0,1,1,1) + +# Create image item with consistent colors and an interactive colorbar +pcmi_consistent = pg.PColorMeshItem(edgecolors=edgecolors, antialiasing=antialiasing, colorMap=cmap, levels=levels, enableAutoLevels=False) +view_consistent_scale.addItem(pcmi_consistent) + +# Add colorbar +bar_static = pg.ColorBarItem( + label = "Z value [arbitrary unit]", + interactive=True, + rounding=0.1) +bar_static.setImageItem( [pcmi_consistent] ) +win.addItem(bar_static,1,1,1,1) + +# Add timing label to the autoscaling view textitem = pg.TextItem(anchor=(1, 0)) -view.addItem(textitem) - +view_auto_scale.addItem(textitem) ## Set the animation fps = 25 # Frame per second of the animation @@ -70,7 +90,7 @@ # display info in top-right corner miny = np.min(y) - wave_amplitude maxy = np.max(y) + wave_amplitude -view.setYRange(miny, maxy) +view_auto_scale.setYRange(miny, maxy) textitem.setPos(np.max(x), maxy) timer = QtCore.QTimer() @@ -90,7 +110,10 @@ def updateData(): new_y = y+wave_amplitude*np.cos(x/wave_length+i) new_z = np.exp(-(x-np.cos(i*color_speed)*xn)**2/1000)[:-1,:-1] + color_noise t1 = time.perf_counter() - pcmi.setData(new_x, + pcmi_auto.setData(new_x, + new_y, + new_z) + pcmi_consistent.setData(new_x, new_y, new_z) t2 = time.perf_counter() diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py index bb1fbc34a1..6b84c840bf 100644 --- a/pyqtgraph/graphicsItems/ColorBarItem.py +++ b/pyqtgraph/graphicsItems/ColorBarItem.py @@ -9,6 +9,7 @@ from .ImageItem import ImageItem from .LinearRegionItem import LinearRegionItem from .PlotItem import PlotItem +from .PColorMeshItem import PColorMeshItem __all__ = ['ColorBarItem'] @@ -40,7 +41,7 @@ class ColorBarItem(PlotItem): sigLevelsChanged = QtCore.Signal(object) sigLevelsChangeFinished = QtCore.Signal(object) - def __init__(self, values=(0,1), width=25, colorMap=None, label=None, + def __init__(self, values=None, width=25, colorMap=None, label=None, interactive=True, limits=None, rounding=1, orientation='vertical', pen='w', hoverPen='r', hoverBrush='#FF000080', cmap=None ): """ @@ -50,8 +51,10 @@ def __init__(self, values=(0,1), width=25, colorMap=None, label=None, ---------- colorMap: `str` or :class:`~pyqtgraph.ColorMap` Determines the color map displayed and applied to assigned ImageItem(s). - values: tuple of float - The range of image levels covered by the color bar, as ``(min, max)``. + values: tuple of float, optional + The range of values that will be represented by the color bar, as ``(min, max)``. + If no values are supplied, the default is to use user-specified values from + an assigned image. If that does not exist, values will default to (0,1). width: float, default=25.0 The width of the displayed color bar. label: str, optional @@ -73,6 +76,14 @@ def __init__(self, values=(0,1), width=25, colorMap=None, label=None, """ super().__init__() self.img_list = [] # list of controlled ImageItems + self._actively_adjusted_values = False + if values is None: + # Use default values + # NOTE: User-specified values from the assigned item will be preferred over the default values of ColorBarItem + values = (0,1) + else: + # The user explicitly entered values, prefer these over values from assigned image. + self._actively_adjusted_values = True self.values = values self._colorMap = None self.rounding = rounding @@ -128,6 +139,7 @@ def __init__(self, values=(0,1), width=25, colorMap=None, label=None, self.addItem(self.bar) if colorMap is not None: self.setColorMap(colorMap) + self.interactive = interactive if interactive: if self.horizontal: align = 'vertical' @@ -151,16 +163,23 @@ def __init__(self, values=(0,1), width=25, colorMap=None, label=None, def setImageItem(self, img, insert_in=None): """ - Assigns an ImageItem or list of ImageItems to be represented and controlled + Assigns an item or list of items to be represented and controlled. + Supported "image items": class:`~pyqtgraph.ImageItem`, class:`~pyqtgraph.PColorMeshItem` Parameters ---------- - image: :class:`~pyqtgraph.ImageItem` or list of `[ImageItem, ImageItem, ...]` - Assigns one or more ImageItems to this ColorBarItem. + image: :image item or list of `[image item, image item, ...]` + Assigns one or more image items to this ColorBarItem. If a :class:`~pyqtgraph.ColorMap` is defined for ColorBarItem, this will be assigned to the - ImageItems. Otherwise, the ColorBarItem will attempt to retrieve a color map from the ImageItems. - In interactive mode, ColorBarItem will control the levels of the assigned ImageItems, + ImageItems. Otherwise, the ColorBarItem will attempt to retrieve a color map from the image items. + In interactive mode, ColorBarItem will control the levels of the assigned image items, simultaneously if there is more than one. + If the ColorBarItem was initialized without a specified ``values`` parameter, it will attempt + to retrieve a set of user-defined ``levels`` from one of the image items. If this fails, + the default values of ColorBarItem will be used as the (min, max) levels of the colorbar. + Note that, for non-interactive ColorBarItems, levels may be overridden by image items with + auto-scaling colors (defined by ``enableAutoLevels``). When using an interactive ColorBarItem + in an animated plot, auto-scaling for its assigned image items should be *manually* disabled. insert_in: :class:`~pyqtgraph.PlotItem`, optional If a PlotItem is given, the color bar is inserted on the right or bottom of the plot, depending on the specified orientation. @@ -169,14 +188,28 @@ def setImageItem(self, img, insert_in=None): self.img_list = [ weakref.ref(item) for item in img ] except TypeError: # failed to iterate, make a single-item list self.img_list = [ weakref.ref( img ) ] - if self._colorMap is None: # check if one of the assigned images has a defined color map - for img_weakref in self.img_list: - img = img_weakref() - if img is not None: + colormap_is_undefined = self._colorMap is None + for img_weakref in self.img_list: + img = img_weakref() + if img is not None: + if hasattr(img, "sigLevelsChanged"): + img.sigLevelsChanged.connect(self._levelsChangedHandler) + + if colormap_is_undefined and hasattr(img, 'getColorMap'): # check if one of the assigned images has a defined color map img_cm = img.getColorMap() if img_cm is not None: self._colorMap = img_cm - break + colormap_is_undefined = False + + if not self._actively_adjusted_values: + # check if one of the assigned images has a non-default set of levels + if hasattr(img, 'getLevels'): + img_levels = img.getLevels() + + if img_levels is not None: + self.setLevels(img_levels, update_items=False) + + if insert_in is not None: if self.horizontal: insert_in.layout.addItem( self, 5, 1 ) # insert in layout below bottom axis @@ -205,7 +238,7 @@ def colorMap(self): """ return self._colorMap - def setLevels(self, values=None, low=None, high=None ): + def setLevels(self, values=None, low=None, high=None, update_items=True): """ Sets the displayed range of image levels. @@ -232,7 +265,11 @@ def setLevels(self, values=None, low=None, high=None ): if self.lo_lim is not None and lo_new < self.lo_lim: lo_new = self.lo_lim if self.hi_lim is not None and hi_new > self.hi_lim: hi_new = self.hi_lim self.values = self.lo_prv, self.hi_prv = (lo_new, hi_new) - self._update_items() + if update_items: + self._update_items() + else: + # update color bar only: + self.axis.setRange( self.values[0], self.values[1] ) def levels(self): """ Returns the currently set levels as the tuple ``(low, high)``. """ @@ -250,7 +287,16 @@ def _update_items(self, update_cmap=False): if img is None: continue # dereference weakref img.setLevels( self.values ) # (min,max) tuple if update_cmap and self._colorMap is not None: - img.setLookupTable( self._colorMap.getLookupTable(nPts=256) ) + if isinstance(img, PColorMeshItem): + img.setLookupTable( self._colorMap.getLookupTable(nPts=256, mode=self._colorMap.QCOLOR) ) + else: + img.setLookupTable( self._colorMap.getLookupTable(nPts=256) ) + + def _levelsChangedHandler(self, levels): + """ internal: called when child item for some reason decides to update its levels without using ColorBarItem. + Will update colormap for the bar based on child items new levels """ + if levels != self.values: + self.setLevels(levels, update_items=False) def _regionChanged(self): """ internal: snap adjusters back to default positions on release """ diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index 14a727a52e..107ee0b91a 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -50,6 +50,7 @@ class PColorMeshItem(GraphicsObject): **Bases:** :class:`GraphicsObject ` """ + sigLevelsChanged = QtCore.Signal(object) # emits tuple with levels (low,high) when color levels are changed. def __init__(self, *args, **kwargs): """ @@ -120,7 +121,7 @@ def __init__(self, *args, **kwargs): self.edgecolors.setCosmetic(True) self.antialiasing = kwargs.get('antialiasing', False) self.levels = kwargs.get('levels', None) - self.enableAutoLevels = kwargs.get('enableAutoLevels', True) + self.enableautolevels = kwargs.get('enableAutoLevels', True) if 'colorMap' in kwargs: cmap = kwargs.get('colorMap') @@ -241,8 +242,15 @@ def setData(self, *args, **kwargs): if np.any(self.x != args[0]) or np.any(self.y != args[1]): shapeChanged = True - # Prepare data - self._prepareData(args) + if len(args)==0: + # No data was received. + if self.z is None: + # No data is currently displayed, + # so other settings (like colormap) can not be updated + return + else: + # Got new data. Prepare it for plotting + self._prepareData(args) self.qpicture = QtGui.QPicture() @@ -262,12 +270,11 @@ def setData(self, *args, **kwargs): # Second we associate each z value, that we normalize, to the lut scale = len(lut) - 1 # Decide whether to autoscale the colormap or use the same levels as before - if (self.levels is None) or (self.enableAutoLevels and autoLevels): + if (self.levels is None) or (self.enableautolevels and autoLevels): # Autoscale colormap z_min = self.z.min() z_max = self.z.max() - if not self.enableAutoLevels: - self.levels = (z_min, z_max) + self.setLevels( (z_min, z_max), update=False) else: # Use consistent colormap scaling z_min = self.levels[0] @@ -307,6 +314,69 @@ def setData(self, *args, **kwargs): + def _updateDisplayWithCurrentState(self, *args, **kargs): + ## Used for re-rendering mesh from self.z. + ## For example when a new colormap is applied, or the levels are adjusted + + defaults = { + 'autoLevels': False, + } + defaults.update(kargs) + return self.setData(*args, **defaults) + + + + def setLevels(self, levels, update=True): + """ + Sets color-scaling levels for the mesh. + + Parameters + ---------- + levels: tuple + ``(low, high)`` + sets the range for which values can be represented in the colormap. + update: bool, optional + Controls if mesh immediately updates to reflect the new color levels. + """ + self.levels = levels + self.sigLevelsChanged.emit(levels) + if update: + self._updateDisplayWithCurrentState() + + + + def getLevels(self): + """ + Returns a tuple containing the current level settings. See :func:`~setLevels`. + The format is ``(low, high)``. + """ + return self.levels + + + + def setLookupTable(self, lut, update=True): + if lut is not self.lut_qbrush: + self.lut_qbrush = [QtGui.QBrush(x) for x in lut] + if update: + self._updateDisplayWithCurrentState() + + + + def getColorMap(self): + return self.cmap + + + + def enableAutoLevels(self): + self.enableautolevels = True + + + + def disableAutoLevels(self): + self.enableautolevels = False + + + def paint(self, p, *args): if self.z is None: return @@ -314,7 +384,6 @@ def paint(self, p, *args): p.drawPicture(0, 0, self.qpicture) - def setBorder(self, b): self.border = fn.mkPen(b) self.update() From bf7187ad85ec427ac197165eef9d82d37260d897 Mon Sep 17 00:00:00 2001 From: djdt <10266332+djdt@users.noreply.github.com> Date: Fri, 27 Jan 2023 18:50:04 +1100 Subject: [PATCH 175/306] fix error when computing dataBounds with orthoRange defined for stepMode="center" --- pyqtgraph/graphicsItems/PlotCurveItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index e261c8e050..0f93a87565 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -266,6 +266,8 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): ## If an orthogonal range is specified, mask the data now if orthoRange is not None: mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) + if self.opts.get("stepMode", None) == "center": + mask = mask[:-1] # len(y) == len(x) - 1 when stepMode is center d = d[mask] #d2 = d2[mask] From 476f466e6db4e5942d3003786f358f6cd9486347 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 10:45:32 -0800 Subject: [PATCH 176/306] Bump pyqt6 from 6.4.0 to 6.4.1 in /doc (#2600) Bumps [pyqt6](https://www.riverbankcomputing.com/software/pyqt/) from 6.4.0 to 6.4.1. --- updated-dependencies: - dependency-name: pyqt6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index ce3957f06e..49a0592141 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -PyQt6==6.4.0 +PyQt6==6.4.1 sphinx==5.3.0 pydata-sphinx-theme==0.11.0 sphinx-design==0.3.0 From 117706676e00127f8f56b28148384be1926a57b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 13:23:39 +0000 Subject: [PATCH 177/306] Bump pyqt6 from 6.4.1 to 6.4.2 in /doc Bumps [pyqt6](https://www.riverbankcomputing.com/software/pyqt/) from 6.4.1 to 6.4.2. --- updated-dependencies: - dependency-name: pyqt6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 49a0592141..43ba56aa66 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -PyQt6==6.4.1 +PyQt6==6.4.2 sphinx==5.3.0 pydata-sphinx-theme==0.11.0 sphinx-design==0.3.0 From 8a8d480e93faf79767cc16832641a25bcbf23d31 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 9 Feb 2023 21:59:49 -0800 Subject: [PATCH 178/306] Demote ValueError to a warning --- .pre-commit-config.yaml | 4 ++-- pyqtgraph/GraphicsScene/GraphicsScene.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab28aeb656..1253d644d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.4.0 hooks: - id: check-added-large-files args: ['--maxkb=100'] @@ -15,6 +15,6 @@ repos: - id: debug-statements - id: check-ast - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index dd7f757472..e3344b5d18 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,3 +1,4 @@ +import warnings import weakref from time import perf_counter, perf_counter_ns @@ -220,10 +221,18 @@ def mouseReleaseEvent(self, ev): cev = [e for e in self.clickEvents if e.button() == ev.button()] if cev: if self.sendClickEvent(cev[0]): - #print "sent click event" ev.accept() - self.clickEvents.remove(cev[0]) - + try: + self.clickEvents.remove(cev[0]) + except ValueError: + warnings.warn( + ("A ValueError can occur here with errant " + "QApplication.processEvent() calls, see " + "https://github.com/pyqtgraph/pyqtgraph/pull/2580 " + "for more information."), + RuntimeWarning, + stacklevel=2 + ) if not ev.buttons(): self.dragItem = None self.dragButtons = [] From 2a9956708b88e2e3ac9413bb5e2055eaab0ee3e3 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 10 Feb 2023 00:05:28 -0800 Subject: [PATCH 179/306] Slight CI Updates (#2606) * Add Python 3.11 to Matrix * Move away from set-output+update minor stuff * Have dependabot update pinned dependencies in CI --- .github/dependabot.yml | 13 ++++++------ .github/workflows/etc/requirements-numpy.txt | 5 +++++ .github/workflows/etc/requirements.txt | 19 +++++++++++++++++ .github/workflows/main.yml | 22 +++++++++++++------- .github/workflows/requirements.txt | 22 -------------------- 5 files changed, 46 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/etc/requirements-numpy.txt create mode 100644 .github/workflows/etc/requirements.txt delete mode 100644 .github/workflows/requirements.txt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c9fb66e350..800c407fd5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,14 +1,15 @@ -# Set update schedule for GitHub Actions - version: 2 updates: - - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every weekday interval: "daily" - - package-ecosystem: pip - directory: /doc + - package-ecosystem: "pip" + directory: "/doc" schedule: - interval: daily + interval: "daily" + - package-ecosystem: "pip" + directory: ".github/workflows/etc" + schedule: + interval: "daily" diff --git a/.github/workflows/etc/requirements-numpy.txt b/.github/workflows/etc/requirements-numpy.txt new file mode 100644 index 0000000000..e927edd293 --- /dev/null +++ b/.github/workflows/etc/requirements-numpy.txt @@ -0,0 +1,5 @@ +# numpy based on python version and NEP-29 requirements +numpy; python_version == '3.10' +numpy; python_version == '3.11' +numpy~=1.21.0; python_version == '3.9' +numpy~=1.20.0; python_version == '3.8' diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt new file mode 100644 index 0000000000..77fb42475b --- /dev/null +++ b/.github/workflows/etc/requirements.txt @@ -0,0 +1,19 @@ +-r requirements-numpy.txt + +# image testing +scipy==1.10.0 + +# optional high performance paths +numba==0.56.4; python_version == '3.9' + +# optional 3D +pyopengl==3.1.6 + +# supplimental tools +matplotlib==3.6.3 +h5py==3.8.0 + +# testing +pytest==7.2.1 +pytest-xdist==3.2.0 +pytest-xvfb==2.0.0; sys_platform == 'linux' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d51b7dfc8c..764489b4d3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] qt-lib: [pyqt, pyside] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] include: - python-version: "3.8" qt-lib: "pyqt" @@ -43,6 +43,12 @@ jobs: - python-version: "3.10" qt-lib: "pyside" qt-version: "PySide6-Essentials" + - python-version: "3.11" + qt-lib: "pyqt" + qt-version: "PyQt6" + - python-version: "3.11" + qt-lib: "pyside" + qt-version: "PySide6-Essentials" steps: - name: Checkout uses: actions/checkout@v3 @@ -55,7 +61,8 @@ jobs: - name: Get pip cache dir id: pip-cache run: | - echo "::set-output name=dir::$(pip cache dir)" + echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT + shell: bash - name: pip cache uses: actions/cache@v3 with: @@ -82,7 +89,7 @@ jobs: shell: cmd - name: Install Dependencies run: | - python -m pip install -r .github/workflows/requirements.txt ${{ matrix.qt-version }} . + python -m pip install -r .github/workflows/etc/requirements.txt ${{ matrix.qt-version }} . - name: "Install Linux VirtualDisplay" if: runner.os == 'Linux' run: | @@ -161,7 +168,8 @@ jobs: miniforge-variant: Mambaforge environment-file: ${{ matrix.environment-file }} auto-update-conda: false - python-version: "3.9" + python-version: "3.11" + use-mamba: true - name: "Install Test Framework" run: pip install pytest pytest-xdist - name: "Install Windows-Mesa OpenGL DLL" @@ -233,10 +241,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Setup Python 3.9 + - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' - name: Build Wheel run: | python -m pip install setuptools wheel @@ -255,7 +263,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' - name: Install Dependencies run: | python -m pip install PyQt5 numpy scipy diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt deleted file mode 100644 index bac65b8b83..0000000000 --- a/.github/workflows/requirements.txt +++ /dev/null @@ -1,22 +0,0 @@ -# numpy based on python version and NEP-29 requirements -numpy; python_version == '3.10' -numpy~=1.21.0; python_version == '3.9' -numpy~=1.20.0; python_version == '3.8' - -# image testing -scipy==1.8.1 - -# optional high performance paths -numba==0.55.2; python_version == '3.9' - -# optional 3D -pyopengl==3.1.6 - -# supplimental tools -matplotlib==3.6.1 -h5py==3.7.0 - -# testing -pytest==7.1.2 -pytest-xdist==2.5.0 -pytest-xvfb==2.0.0; sys_platform == 'linux' From a4d35d738d45087036490a888ac604996340203d Mon Sep 17 00:00:00 2001 From: jmkerloch <53606373+jmkerloch@users.noreply.github.com> Date: Fri, 10 Feb 2023 09:06:07 +0100 Subject: [PATCH 180/306] feat(PlotItem) define context menu action visibility (#2584) * feat(PlotItem) define context menu action visibility * feat(PlotItem) define context menu action visibility, update docstring --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index cab009e0c0..f9cfdf56ef 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1149,6 +1149,26 @@ def setMenuEnabled(self, enableMenu=True, enableViewBoxMenu='same'): def menuEnabled(self): return self._menuEnabled + + def setContextMenuActionVisible(self, name : str, visible : bool) -> None: + """ + Changes the context menu action visibility + + Parameters + ---------- + name: str + Action name + must be one of 'Transforms', 'Downsample', 'Average','Alpha', 'Grid', or 'Points' + visible: bool + Determines if action will be display + True action is visible + False action is invisible. + """ + translated_name = translate("PlotItem", name) + for action in self.ctrlMenu.actions(): + if action.text() == translated_name: + action.setVisible(visible) + break def hoverEvent(self, ev): if ev.enter: From eecd0f1eed5cd6fed6a5abb899a721e166b9fd4a Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 10 Feb 2023 00:59:25 -0800 Subject: [PATCH 181/306] Dependabot, leave numpy alone (#2608) --- .github/dependabot.yml | 3 +++ .github/workflows/etc/requirements-numpy.txt | 5 ----- .github/workflows/etc/requirements.txt | 6 +++++- 3 files changed, 8 insertions(+), 6 deletions(-) delete mode 100644 .github/workflows/etc/requirements-numpy.txt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 800c407fd5..72a5e26b20 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,5 +11,8 @@ updates: interval: "daily" - package-ecosystem: "pip" directory: ".github/workflows/etc" + ignore: + - dependency-name: "numpy" + # NEP-29 governs supported versions of NumPy schedule: interval: "daily" diff --git a/.github/workflows/etc/requirements-numpy.txt b/.github/workflows/etc/requirements-numpy.txt deleted file mode 100644 index e927edd293..0000000000 --- a/.github/workflows/etc/requirements-numpy.txt +++ /dev/null @@ -1,5 +0,0 @@ -# numpy based on python version and NEP-29 requirements -numpy; python_version == '3.10' -numpy; python_version == '3.11' -numpy~=1.21.0; python_version == '3.9' -numpy~=1.20.0; python_version == '3.8' diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 77fb42475b..cc9c9a353c 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -1,4 +1,8 @@ --r requirements-numpy.txt +# numpy based on python version and NEP-29 requirements +numpy; python_version == '3.10' +numpy; python_version == '3.11' +numpy~=1.21.0; python_version == '3.9' +numpy~=1.20.0; python_version == '3.8' # image testing scipy==1.10.0 From 21e48a575de4a71b4f7e3a0088698434dabc77db Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 10 Feb 2023 19:21:55 +0800 Subject: [PATCH 182/306] calculate boundingRect without drawing --- pyqtgraph/graphicsItems/BarGraphItem.py | 230 +++++++++++++++--------- 1 file changed, 148 insertions(+), 82 deletions(-) diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 0df743d877..1e601bd2e6 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -2,6 +2,7 @@ from .. import functions as fn from .. import getConfigOption +from .. import Qt from ..Qt import QtCore, QtGui from .GraphicsObject import GraphicsObject @@ -48,6 +49,14 @@ def __init__(self, **opts): pens=None, brushes=None, ) + + if 'pen' not in opts: + opts['pen'] = getConfigOption('foreground') + if 'brush' not in opts: + opts['brush'] = (128, 128, 128) + # the first call to _updateColors() will thus always be an update + + self._rectarray = Qt.internals.PrimitiveArray(QtCore.QRectF, 4) self._shape = None self.picture = None self.setOpts(**opts) @@ -56,31 +65,74 @@ def setOpts(self, **opts): self.opts.update(opts) self.picture = None self._shape = None + self._prepareData() + self._updateColors(opts) + self.prepareGeometryChange() self.update() self.informViewBoundsChanged() - - def drawPicture(self): - self.picture = QtGui.QPicture() - self._shape = QtGui.QPainterPath() - p = QtGui.QPainter(self.picture) - - pen = self.opts['pen'] - pens = self.opts['pens'] - - if pen is None and pens is None: - pen = getConfigOption('foreground') - - brush = self.opts['brush'] - brushes = self.opts['brushes'] - if brush is None and brushes is None: - brush = (128, 128, 128) - + + def _updatePenWidth(self, pen): + no_pen = pen is None or pen.style() == QtCore.Qt.PenStyle.NoPen + if no_pen: + return + + idx = pen.isCosmetic() + self._penWidth[idx] = max(self._penWidth[idx], pen.widthF()) + + def _updateColors(self, opts): + # the logic here is to permit the user to update only data + # without updating pens/brushes + + # update only if fresh pen/pens supplied + if 'pen' in opts or 'pens' in opts: + self._penWidth = [0, 0] + + if self.opts['pens'] is None: + # pens not configured, use single pen + pen = self.opts['pen'] + pen = fn.mkPen(pen) + self._updatePenWidth(pen) + self._sharedPen = pen + self._pens = None + else: + # pens configured, ignore single pen (if any) + pens = [] + for pen in self.opts['pens']: + if not isinstance(pen, QtGui.QPen): + pen = fn.mkPen(pen) + pens.append(pen) + self._updatePenWidth(pen) + self._sharedPen = None + self._pens = pens + + # update only if fresh brush/brushes supplied + if 'brush' in opts or 'brushes' in opts: + if self.opts['brushes'] is None: + # brushes not configured, use single brush + brush = self.opts['brush'] + self._sharedBrush = fn.mkBrush(brush) + self._brushes = None + else: + # brushes configured, ignore single brush (if any) + brushes = [] + for brush in self.opts['brushes']: + if not isinstance(brush, QtGui.QBrush): + brush = fn.mkBrush(brush) + brushes.append(brush) + self._sharedBrush = None + self._brushes = brushes + + self._singleColor = ( + self._sharedPen is not None and + self._sharedBrush is not None + ) + + def _getNormalizedCoords(self): def asarray(x): if x is None or np.isscalar(x) or isinstance(x, np.ndarray): return x return np.array(x) - x = asarray(self.opts.get('x')) x0 = asarray(self.opts.get('x0')) x1 = asarray(self.opts.get('x1')) @@ -118,66 +170,71 @@ def asarray(x): if y1 is None: raise Exception('must specify either y1 or height') height = y1 - y0 - - p.setPen(fn.mkPen(pen)) - p.setBrush(fn.mkBrush(brush)) - dataBounds = QtCore.QRectF() - pixelPadding = 0 - for i in range(len(x0 if not np.isscalar(x0) else y0)): - if pens is not None: - p.setPen(fn.mkPen(pens[i])) - if brushes is not None: - p.setBrush(fn.mkBrush(brushes[i])) - - if np.isscalar(x0): - x = x0 - else: - x = x0[i] - if np.isscalar(y0): - y = y0 - else: - y = y0[i] - if np.isscalar(width): - w = width - else: - w = width[i] - if np.isscalar(height): - h = height - else: - h = height[i] - - - rect = QtCore.QRectF(x, y, w, h) - p.drawRect(rect) - self._shape.addRect(rect) - - pen = p.pen() - pw = pen.widthF() - if pen.style() == QtCore.Qt.PenStyle.NoPen: - pw = 0 - elif pw == 0: - pw = 1 - pw *= 0.5 - if pen.isCosmetic(): - dataBounds |= rect - pixelPadding = max(pixelPadding, pw) - else: - dataBounds |= rect.adjusted(-pw, -pw, pw, pw) - p.end() - self._dataBounds = dataBounds - self._pixelPadding = pixelPadding - self.prepareGeometryChange() - - + # ensure x0 < x1 and y0 < y1 + t0, t1 = x0, x0 + width + x0 = np.minimum(t0, t1, dtype=np.float64) + x1 = np.maximum(t0, t1, dtype=np.float64) + t0, t1 = y0, y0 + height + y0 = np.minimum(t0, t1, dtype=np.float64) + y1 = np.maximum(t0, t1, dtype=np.float64) + + # here, all of x0, y0, x1, y1 are numpy objects, + # BUT could possibly be numpy scalars + return x0, y0, x1, y1 + + def _prepareData(self): + x0, y0, x1, y1 = self._getNormalizedCoords() + xmn, xmx = np.min(x0), np.max(x1) + ymn, ymx = np.min(y0), np.max(y1) + self._dataBounds = (xmn, xmx), (ymn, ymx) + + self._rectarray.resize(max(x0.size, y0.size)) + memory = self._rectarray.ndarray() + memory[:, 0] = x0 + memory[:, 1] = y0 + memory[:, 2] = x1 - x0 + memory[:, 3] = y1 - y0 + + def _render(self, painter): + if self._sharedPen is not None: + painter.setPen(self._sharedPen) + if self._sharedBrush is not None: + painter.setBrush(self._sharedBrush) + + rects = self._rectarray.instances() + for idx, rect in enumerate(rects): + if self._pens is not None: + painter.setPen(self._pens[idx]) + if self._brushes is not None: + painter.setBrush(self._brushes[idx]) + + painter.drawRect(rect) + + def drawPicture(self): + self.picture = QtGui.QPicture() + painter = QtGui.QPainter(self.picture) + self._render(painter) + painter.end() + def paint(self, p, *args): - if self.picture is None: - self.drawPicture() - self.picture.play(p) + if self._singleColor: + p.setPen(self._sharedPen) + p.setBrush(self._sharedBrush) + inst = self._rectarray.instances() + p.drawRects(inst) + else: + if self.picture is None: + self.drawPicture() + self.picture.play(p) def shape(self): - if self.picture is None: - self.drawPicture() + if self._shape is None: + shape = QtGui.QPainterPath() + rects = self._rectarray.instances() + for rect in rects: + shape.addRect(rect) + self._shape = shape return self._shape def implements(self, interface=None): @@ -193,17 +250,25 @@ def getData(self): return self.opts.get('x'), self.opts.get('height') def dataBounds(self, ax, frac=1.0, orthoRange=None): - if self.picture is None: - self.drawPicture() - l, t, r, b = self._dataBounds.getCoords() - return [l, r] if ax == 0 else [t, b] + # _penWidth is available after _updateColors() + pw = self._penWidth[0] * 0.5 + # _dataBounds is available after _prepareData() + bounds = self._dataBounds[ax] + return (bounds[0] - pw, bounds[1] + pw) def pixelPadding(self): - if self.picture is None: - self.drawPicture() - return self._pixelPadding + # _penWidth is available after _updateColors() + pw = (self._penWidth[1] or 1) * 0.5 + return pw def boundingRect(self): + xmn, xmx = self.dataBounds(ax=0) + if xmn is None or xmx is None: + return QtCore.QRectF() + ymn, ymx = self.dataBounds(ax=1) + if ymn is None or ymx is None: + return QtCore.QRectF() + px = py = 0 pxPad = self.pixelPadding() if pxPad > 0: @@ -214,4 +279,5 @@ def boundingRect(self): # return bounds expanded by pixel size px *= pxPad py *= pxPad - return self._dataBounds.adjusted(-px, -py, px, py) + + return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) From d1653ecedec9b9446512aca1059ec06df76cca2f Mon Sep 17 00:00:00 2001 From: pijyoi Date: Sat, 11 Feb 2023 12:11:29 +0800 Subject: [PATCH 183/306] rename "method" to "use_array" and make it keyword only (#2609) document explicitly however that PrimitiveArray is not public facing API. --- pyqtgraph/Qt/internals.py | 17 ++++++++++------- pyqtgraph/graphicsItems/PlotCurveItem.py | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/Qt/internals.py b/pyqtgraph/Qt/internals.py index a4d200176f..1bc62a5f64 100644 --- a/pyqtgraph/Qt/internals.py +++ b/pyqtgraph/Qt/internals.py @@ -75,6 +75,9 @@ def get_qpainterpath_element_array(qpath, nelems=None): return np.frombuffer(vp, dtype=dtype) class PrimitiveArray: + # Note: This class is an internal implementation detail and is not part + # of the public API. + # # QPainter has a C++ native API that takes an array of objects: # drawPrimitives(const Primitive *array, int count, ...) # where "Primitive" is one of QPointF, QLineF, QRectF, PixmapFragment @@ -95,30 +98,30 @@ class PrimitiveArray: # C array of PixmapFragment(s) _and_ the length of the array. # There is no overload that takes a Python list of PixmapFragment(s). - def __init__(self, Klass, nfields, method=None): + def __init__(self, Klass, nfields, *, use_array=None): self._Klass = Klass self._nfields = nfields self._ndarray = None if QT_LIB.startswith('PyQt'): - if method is None: - method = ( + if use_array is None: + use_array = ( hasattr(sip, 'array') and ( (0x60301 <= QtCore.PYQT_VERSION) or (0x50f07 <= QtCore.PYQT_VERSION < 0x60000) ) ) - self.use_sip_array = method + self.use_sip_array = use_array else: self.use_sip_array = False if QT_LIB.startswith('PySide'): - if method is None: - method = ( + if use_array is None: + use_array = ( Klass is QtGui.QPainter.PixmapFragment ) - self.use_ptr_to_array = method + self.use_ptr_to_array = use_array else: self.use_ptr_to_array = False diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 0f93a87565..c40943ade7 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -39,16 +39,16 @@ def have_native_drawlines_array(): class LineSegments: def __init__(self): - method = None + use_array = None # "use_native_drawlines" is pending the following issue and code review # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1924 # https://codereview.qt-project.org/c/pyside/pyside-setup/+/415702 self.use_native_drawlines = Qt.QT_LIB.startswith('PySide') and _have_native_drawlines_array if self.use_native_drawlines: - method = True + use_array = True - self.array = Qt.internals.PrimitiveArray(QtCore.QLineF, 4, method=method) + self.array = Qt.internals.PrimitiveArray(QtCore.QLineF, 4, use_array=use_array) def get(self, size): self.array.resize(size) From 9ff0ca4de70c07f07b41b39a584c2560ad8ad719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:09:55 +0000 Subject: [PATCH 184/306] Bump matplotlib from 3.6.3 to 3.7.0 in /.github/workflows/etc Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.6.3 to 3.7.0. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.6.3...v3.7.0) --- updated-dependencies: - dependency-name: matplotlib dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index cc9c9a353c..ecd62017cc 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -14,7 +14,7 @@ numba==0.56.4; python_version == '3.9' pyopengl==3.1.6 # supplimental tools -matplotlib==3.6.3 +matplotlib==3.7.0 h5py==3.8.0 # testing From 9f04dec192b837d9ee4c8d13f5c9cb74069459d3 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 19 Feb 2023 10:13:54 +0800 Subject: [PATCH 185/306] special case no_pen and multi_brush to use fillRect --- pyqtgraph/graphicsItems/BarGraphItem.py | 32 +++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 1e601bd2e6..59f18fcfa6 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -197,19 +197,31 @@ def _prepareData(self): memory[:, 3] = y1 - y0 def _render(self, painter): - if self._sharedPen is not None: - painter.setPen(self._sharedPen) - if self._sharedBrush is not None: - painter.setBrush(self._sharedBrush) + multi_pen = self._pens is not None + multi_brush = self._brushes is not None + no_pen = ( + not multi_pen + and self._sharedPen.style() == QtCore.Qt.PenStyle.NoPen + ) rects = self._rectarray.instances() - for idx, rect in enumerate(rects): - if self._pens is not None: - painter.setPen(self._pens[idx]) - if self._brushes is not None: - painter.setBrush(self._brushes[idx]) - painter.drawRect(rect) + if no_pen and multi_brush: + for idx, rect in enumerate(rects): + painter.fillRect(rect, self._brushes[idx]) + else: + if not multi_pen: + painter.setPen(self._sharedPen) + if not multi_brush: + painter.setBrush(self._sharedBrush) + + for idx, rect in enumerate(rects): + if multi_pen: + painter.setPen(self._pens[idx]) + if multi_brush: + painter.setBrush(self._brushes[idx]) + + painter.drawRect(rect) def drawPicture(self): self.picture = QtGui.QPicture() From 8f261eb074849a1a3ce28ccb2d92a8ffae0e4524 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Feb 2023 14:25:03 +0000 Subject: [PATCH 186/306] Bump scipy from 1.10.0 to 1.10.1 in /.github/workflows/etc Bumps [scipy](https://github.com/scipy/scipy) from 1.10.0 to 1.10.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.10.0...v1.10.1) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index ecd62017cc..4c0458c7ed 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -5,7 +5,7 @@ numpy~=1.21.0; python_version == '3.9' numpy~=1.20.0; python_version == '3.8' # image testing -scipy==1.10.0 +scipy==1.10.1 # optional high performance paths numba==0.56.4; python_version == '3.9' From fa53d77e164edd16cf0caf72c7bbe849f8f92394 Mon Sep 17 00:00:00 2001 From: Petras Jokubauskas Date: Thu, 23 Feb 2023 11:47:50 +0100 Subject: [PATCH 187/306] FIX: ImageView nframes method is too hardcoded This is fixing the behviour of ImageView for 3 dimentional images, where stacking or time is not in 0 index of array shape. as nframes is being called by other methods which are enbled only if axes t is recognised, there is no need to check for if axes['t] is None (then frame should be 1); alternatively, would not it be better to remove this method and use where needed `self.tVals.shape[0]` ? From performance point of view that would be better, but seeing the checking `if self.image is None:` probably there is the reason for cases where would be 0 frames. Thus I had added at the end `return 1` and so `nframes()` should cover all possible cases also then called externally. --- pyqtgraph/imageview/ImageView.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5f72625874..d6169e0815 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -473,8 +473,9 @@ def nframes(self): """ if self.image is None: return 0 - else: - return self.image.shape[0] + elif self.axes['t'] is not None: + return self.image.shape[self.axes['t']] + return 1 def autoLevels(self): """Set the min/max intensity levels automatically to match the image data.""" From aed87f61ece7f4a7821c2f014b697faf0cc95181 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 24 Feb 2023 20:15:31 +0800 Subject: [PATCH 188/306] don't use pg.plot() in tests a global reference is kept to all PlotWidgets created by pg.plot() --- tests/exporters/test_csv.py | 6 ++++-- tests/exporters/test_hdf5.py | 6 ++++-- tests/exporters/test_image.py | 6 ++++-- tests/exporters/test_matplotlib.py | 10 +++++++--- tests/graphicsItems/test_InfiniteLine.py | 6 ++++-- tests/graphicsItems/test_ROI.py | 4 ++-- tests/graphicsItems/test_TextItem.py | 3 ++- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/tests/exporters/test_csv.py b/tests/exporters/test_csv.py index 75410a4322..05a5a96f3d 100644 --- a/tests/exporters/test_csv.py +++ b/tests/exporters/test_csv.py @@ -17,7 +17,8 @@ def approxeq(a, b): def test_CSVExporter(): - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() y1 = [1,3,2,3,1,6,9,8,4,2] plt.plot(y=y1, name='myPlot') @@ -55,7 +56,8 @@ def test_CSVExporter(): assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i]) def test_CSVExporter_with_ErrorBarItem(): - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() x=np.arange(5) y=np.array([1, 2, 3, 2, 1]) top_error = np.array([2, 3, 3, 3, 2]) diff --git a/tests/exporters/test_hdf5.py b/tests/exporters/test_hdf5.py index ed427e73a4..9ca603a2d1 100644 --- a/tests/exporters/test_hdf5.py +++ b/tests/exporters/test_hdf5.py @@ -21,7 +21,8 @@ def test_HDF5Exporter(tmp_h5, combine): y1 = np.sin(x) y2 = np.cos(x) - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() plt.plot(x=x, y=y1) plt.plot(x=x, y=y2) @@ -51,7 +52,8 @@ def test_HDF5Exporter_unequal_lengths(tmp_h5): x2 = np.linspace(0, 1, 100) y2 = np.cos(x2) - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() plt.plot(x=x1, y=y1, name='plot0') plt.plot(x=x2, y=y2) diff --git a/tests/exporters/test_image.py b/tests/exporters/test_image.py index aff6bed311..8ef12a26bd 100644 --- a/tests/exporters/test_image.py +++ b/tests/exporters/test_image.py @@ -11,13 +11,15 @@ def test_ImageExporter_filename_dialog(): """Tests ImageExporter code path that opens a file dialog. Regression test for pull request 1133.""" - p = pg.plot() + p = pg.PlotWidget() + p.show() exp = ImageExporter(p.getPlotItem()) exp.export() def test_ImageExporter_toBytes(): - p = pg.plot() + p = pg.PlotWidget() + p.show() p.hideAxis('bottom') p.hideAxis('left') exp = ImageExporter(p.getPlotItem()) diff --git a/tests/exporters/test_matplotlib.py b/tests/exporters/test_matplotlib.py index e740c3a118..8d082e3024 100644 --- a/tests/exporters/test_matplotlib.py +++ b/tests/exporters/test_matplotlib.py @@ -33,7 +33,8 @@ @skip_qt6 def test_MatplotlibExporter(): - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() # curve item plt.plot([0, 1, 2], [0, 1, 2]) @@ -48,7 +49,8 @@ def test_MatplotlibExporter(): @skip_qt6 def test_MatplotlibExporter_nonplotitem(): # attempting to export something other than a PlotItem raises an exception - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() plt.plot([0, 1, 2], [2, 3, 4]) exp = MatplotlibExporter(plt.getPlotItem().getViewBox()) with pytest.raises(Exception): @@ -59,7 +61,9 @@ def test_MatplotlibExporter_nonplotitem(): def test_MatplotlibExporter_siscale(scale): # coarse test to verify that plot data is scaled before export when # autoSIPrefix is in effect (so mpl doesn't add its own multiplier label) - plt = pg.plot([0, 1, 2], [(i+1)*scale for i in range(3)]) + plt = pg.PlotWidget() + plt.show() + plt.plot([0, 1, 2], [(i+1)*scale for i in range(3)]) # set the label so autoSIPrefix works plt.setLabel('left', 'magnitude') exp = MatplotlibExporter(plt.getPlotItem()) diff --git a/tests/graphicsItems/test_InfiniteLine.py b/tests/graphicsItems/test_InfiniteLine.py index 991dedad53..2d0690490c 100644 --- a/tests/graphicsItems/test_InfiniteLine.py +++ b/tests/graphicsItems/test_InfiniteLine.py @@ -10,7 +10,8 @@ def test_InfiniteLine(): pg.setConfigOption('mouseRateLimit', -1) # Test basic InfiniteLine API - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() plt.setXRange(-10, 10) plt.setYRange(-10, 10) plt.resize(600, 600) @@ -54,7 +55,8 @@ def test_mouseInteraction(): # disable delay of mouse move events because events is called immediately in test pg.setConfigOption('mouseRateLimit', -1) - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. vline = plt.addLine(x=0, movable=True) hline = plt.addLine(y=0, movable=True) diff --git a/tests/graphicsItems/test_ROI.py b/tests/graphicsItems/test_ROI.py index ce2c1b4a3e..796359be0a 100644 --- a/tests/graphicsItems/test_ROI.py +++ b/tests/graphicsItems/test_ROI.py @@ -244,7 +244,6 @@ def test_PolyLineROI(): 'open') ] - # plt = pg.plot() plt = pg.GraphicsView() plt.show() resizeWindow(plt, 200, 200) @@ -347,7 +346,8 @@ def test_PolyLineROI(): ((-2, 1), (-4, -8)), ]) def test_LineROI_coords(p1, p2): - pw = pg.plot() + pw = pg.PlotWidget() + pw.show() lineroi = pg.LineROI(p1, p2, width=0.5, pen="r") pw.addItem(lineroi) diff --git a/tests/graphicsItems/test_TextItem.py b/tests/graphicsItems/test_TextItem.py index c6143b6f57..55c99196d2 100644 --- a/tests/graphicsItems/test_TextItem.py +++ b/tests/graphicsItems/test_TextItem.py @@ -4,7 +4,8 @@ def test_TextItem_setAngle(): - plt = pg.plot() + plt = pg.PlotWidget() + plt.show() plt.setXRange(-10, 10) plt.setYRange(-20, 20) item = pg.TextItem(text="test") From 7118248d5902e72414acf3bee27e7c0f61e0737a Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 25 Feb 2023 23:32:53 +0800 Subject: [PATCH 189/306] test of ImageItem should not create ImageView we are testing ImageItem, not ImageView; and furthermore pg.image() creates a global reference. some additional fixes: 1) set lower bound of xRange from "-5+25" to "-5e+25" - in practice, it doesn't matter. because we just need xRange to be very large. - furthermore, ImageView sets the aspect to be locked, so it would have been adjusted to follow yRange anyway 2) reduce the qWait time - follow what the other tests do and wait for window exposed --- tests/graphicsItems/test_ImageItem.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/graphicsItems/test_ImageItem.py b/tests/graphicsItems/test_ImageItem.py index 99e165c344..f3e245e302 100644 --- a/tests/graphicsItems/test_ImageItem.py +++ b/tests/graphicsItems/test_ImageItem.py @@ -230,11 +230,18 @@ def assert_equal_transforms(tr1, tr2): def test_dividebyzero(): - im = pg.image(np.random.normal(size=(100,100))) - im.imageItem.setAutoDownsample(True) - im.view.setRange(xRange=[-5+25, 5e+25],yRange=[-5e+25, 5e+25]) - app.processEvents() - QtTest.QTest.qWait(1000) - # must manually call im.imageItem.render here or the exception + # test that the calculation of downsample factors + # does not result in a division by zero + plt = pg.PlotWidget() + plt.show() + plt.setAspectLocked(True) + imgitem = pg.ImageItem(np.random.normal(size=(100,100))) + imgitem.setAutoDownsample(True) + plt.addItem(imgitem) + + plt.setRange(xRange=[-5e+25, 5e+25],yRange=[-5e+25, 5e+25]) + QtTest.QTest.qWaitForWindowExposed(plt) + QtTest.QTest.qWait(100) + # must manually call imgitem.render here or the exception # will only exist on the Qt event loop - im.imageItem.render() + imgitem.render() From d191299259715abf6bc2e04450ec08a6927d2d97 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 26 Feb 2023 00:11:36 +0800 Subject: [PATCH 190/306] don't use pg.image() in tests a global reference is created by pg.image() --- tests/imageview/test_imageview.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/imageview/test_imageview.py b/tests/imageview/test_imageview.py index 73de91599f..84043d2003 100644 --- a/tests/imageview/test_imageview.py +++ b/tests/imageview/test_imageview.py @@ -8,10 +8,12 @@ def test_nan_image(): img = np.ones((10,10)) img[0,0] = np.nan - v = pg.image(img) - v.imageItem.getHistogram() + iv = pg.ImageView() + iv.setImage(img) + iv.show() + iv.getImageItem().getHistogram() app.processEvents() - v.window().close() + iv.window().close() def test_timeslide_snap(): From da5202611b1fd9735d9619d71d966b01c6bfdb13 Mon Sep 17 00:00:00 2001 From: Nils Nemitz Date: Sat, 4 Mar 2023 01:22:35 +0900 Subject: [PATCH 191/306] Fix bounds handling when data is int16 or similar formats (#2515) * merged upstream * return PlotCurveItem bounds as float, add tests for this * DO actually test different data types --- pyqtgraph/graphicsItems/PlotCurveItem.py | 11 +++++++---- tests/graphicsItems/test_PlotDataItem.py | 14 +++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index c40943ade7..1f561710b9 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -281,13 +281,13 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): with warnings.catch_warnings(): # All-NaN data is acceptable; Explicit numpy warning is not needed. warnings.simplefilter("ignore") - b = (np.nanmin(d), np.nanmax(d)) + b = ( float(np.nanmin(d)), float(np.nanmax(d)) ) # enforce float format for bounds, even if data format is different if math.isinf(b[0]) or math.isinf(b[1]): mask = np.isfinite(d) d = d[mask] if len(d) == 0: return (None, None) - b = (d.min(), d.max()) + b = ( float(d.min()), float(d.max()) ) # enforce float format for bounds, even if data format is different elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) @@ -297,11 +297,14 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): d = d[mask] if len(d) == 0: return (None, None) - b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) + b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) # percentile result is always float64 or larger ## adjust for fill level if ax == 1 and self.opts['fillLevel'] not in [None, 'enclosed']: - b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel'])) + b = ( + float( min(b[0], self.opts['fillLevel']) ), + float( max(b[1], self.opts['fillLevel']) ) + ) # enforce float format for bounds, even if data format is different ## Add pen width only if it is non-cosmetic. pen = self.opts['pen'] diff --git a/tests/graphicsItems/test_PlotDataItem.py b/tests/graphicsItems/test_PlotDataItem.py index e89eb65c11..1cd9f32d6b 100644 --- a/tests/graphicsItems/test_PlotDataItem.py +++ b/tests/graphicsItems/test_PlotDataItem.py @@ -11,12 +11,20 @@ def test_bool(): truths = np.random.randint(0, 2, size=(100,)).astype(bool) pdi = pg.PlotDataItem(truths) - bounds = pdi.dataBounds(1) - assert isinstance(bounds[0], np.uint8) - assert isinstance(bounds[1], np.uint8) xdata, ydata = pdi.getData() assert ydata.dtype == np.uint8 +def test_bound_formats(): + for datatype in (bool, np.uint8, np.int16, float): + truths = np.random.randint(0, 2, size=(100,)).astype(datatype) + pdi_scatter = pg.PlotDataItem(truths, symbol='o', pen=None) + pdi_line = pg.PlotDataItem(truths) + bounds = pdi_scatter.dataBounds(1) + assert isinstance(bounds[0], float), 'bound 0 is not float for scatter plot of '+str(datatype) + assert isinstance(bounds[0], float), 'bound 1 is not float for scatter plot of '+str(datatype) + bounds = pdi_line.dataBounds(1) + assert isinstance(bounds[0], float), 'bound 0 is not float for line plot of '+str(datatype) + assert isinstance(bounds[0], float), 'bound 1 is not float for line plot of '+str(datatype) def test_fft(): f = 20. From 02375953e7d8fa6bfadbe4d63e54d692f60244d1 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 3 Mar 2023 20:53:39 -0800 Subject: [PATCH 192/306] Prepare for 0.13.2 release --- CHANGELOG | 105 ++++++++++++++++++++++++++++++++++++++++++ pyqtgraph/__init__.py | 2 +- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5a5fb5199b..06ca63859f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,108 @@ +pyqtgraph-0.13.2 + +## What's Changed + +### Highlights + +* Fix InfiniteLine bounding rect calculation by @ixjlyons in https://github.com/pyqtgraph/pyqtgraph/pull/2407 +* Allow plotting multiple data items at once by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2461 +* Migrate to PyData Sphinx Theme - Restructure Docs by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2449 + +### API/Behavior Changes + +* re-enable hmac authentication for win32 by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2465 +* Add keyword argument in PColorMeshItem to enable consistent colormap scaling during animation by @SimenZhor in https://github.com/pyqtgraph/pyqtgraph/pull/2463 +* fix: use connect='finite' if finite-ness of data is unknown by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2471 +* Fix `ViewBox.autoRange()` for case of data with constant y-value by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2489 +* Make `ActionGroup` compatible with existing Parameter conventions by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2505 +* Update `PenParameter` by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2536 +* remove resizeEvent on screen change by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2546 +* BarGraphItem: implement dataBounds and pixelPadding by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2565 +* Maintain argument propagation for GLGraphicsItems to super class by @koutoftimer in https://github.com/pyqtgraph/pyqtgraph/pull/2516 +* Accept custom ROI objects for ImageView by @ktahar in https://github.com/pyqtgraph/pyqtgraph/pull/2581 +* PColorMeshItem colorbar support by @SimenZhor in https://github.com/pyqtgraph/pyqtgraph/pull/2477 +* feat(PlotItem) define context menu action visibility by @jmkerloch in https://github.com/pyqtgraph/pyqtgraph/pull/2584 +* Allow plotting multiple data items at once by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2461 + +### Bug Fixes + +* Fix renderView to not use mremap on FreeBSD by @yurivict in https://github.com/pyqtgraph/pyqtgraph/pull/2445 +* Fix action parameter button that is briefly made visible before getting a parent by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2451 +* Fix InfiniteLine bounding rect calculation by @ixjlyons in https://github.com/pyqtgraph/pyqtgraph/pull/2407 +* Fix disconnect of signal proxy by @dgoeries in https://github.com/pyqtgraph/pyqtgraph/pull/2453 +* Fix ButtonItem hover event by @bbc131 in https://github.com/pyqtgraph/pyqtgraph/pull/2473 +* test and fix for ChecklistParameter.show bug by @outofculture in https://github.com/pyqtgraph/pyqtgraph/pull/2480 +* fix segmented line mode with no segments by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2481 +* Prevent flickering `ActionGroup` when switching parameter trees by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2484 +* fix: ndarray_from_qimage does not hold a reference to qimage by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2492 +* Fix exportDialog drawn off screen by @aksy2512 in https://github.com/pyqtgraph/pyqtgraph/pull/2510 +* partial fix for Qmenu leak by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2518 +* Fix last QMenu leak and its associated segfaults by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2522 +* fix setMaximumHeight(1e6) in SpinBox.py by @sem-geologist in https://github.com/pyqtgraph/pyqtgraph/pull/2519 +* Use base_prefix to detect virtual env by @eendebakpt in https://github.com/pyqtgraph/pyqtgraph/pull/2566 +* typo: dataRange -> dataBounds by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2589 +* NonUniformImage: implement floating point boundingRect by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2587 +* PColorMeshItem: implement dataBounds and pixelPadding by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2586 +* BarGraphItem: calculate boundingRect without drawing by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2599 +* Fix bounds handling when data is int16 or similar formats by @NilsNemitz in https://github.com/pyqtgraph/pyqtgraph/pull/2515 +* ImageView: make .nframes() to use .axis['t'] instead of .shape[0] by @sem-geologist in https://github.com/pyqtgraph/pyqtgraph/pull/2623 +* Fix GraphicsScene ValueError in mouseReleaseEvent by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2605 +* PlotCurveItem error with stepMode="center", autoRange and autoVisible by @djdt in https://github.com/pyqtgraph/pyqtgraph/pull/2595 + +### Examples + +* Fix #2482 argparse inputs were ignored by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2483 +* PlotSpeedTest: reflect initial use_opengl state by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2487 +* draw example histogram using BarGraphItem by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2556 + +### Tests + +* ROI: Add test with mouseDrag event and check snapping by @dgoeries in https://github.com/pyqtgraph/pyqtgraph/pull/2476 +* fix wrong logic for assert_alldead by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2503 +* fix: instantiate QApplication for test_Parameter.py by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2539 +* don't use pg.plot() in tests by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2625 + +### Docs + +* Migrate to PyData Sphinx Theme - Restructure Docs by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2449 +* Fix Qt crash course example by @Jaime02 in https://github.com/pyqtgraph/pyqtgraph/pull/2470 + +### Other + +* Remove remaining templates by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2448 +* Have canvas deprecation warning by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2446 +* unify win32 and unix mmap codepaths by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2457 +* Update setup.py, import distutils after setuptools by @LocutusOfBorg in https://github.com/pyqtgraph/pyqtgraph/pull/2459 +* Raise appropriate Exceptions in place of generic exceptions by @Nibba2018 in https://github.com/pyqtgraph/pyqtgraph/pull/2474 +* Remove old unused mains by @Jaime02 in https://github.com/pyqtgraph/pyqtgraph/pull/2490 +* make DockDrop be a non-mixin by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2450 +* Use non-deprecated QMouseEvent signatures by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2509 +* Remove STRTransform main by @Jaime02 in https://github.com/pyqtgraph/pyqtgraph/pull/2466 +* Minor improvements to `InteractiveFunction` ecosystem by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2521 +* Improve `ChecklistParameter.setValue` logic. by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2544 +* Remove antiquated Qt crash prevention by @NeilGirdhar in https://github.com/pyqtgraph/pyqtgraph/pull/2573 +* create internals.PrimitiveArray by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2591 +* rename "method" to "use_array" and make it keyword only by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2609 + +## New Contributors + +* @yurivict made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2445 +* @Jaime02 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2468 +* @SimenZhor made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2463 +* @Nibba2018 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2474 +* @rookiepeng made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2491 +* @aksy2512 made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2510 +* @noonchen made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2553 +* @ZeitgeberH made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2559 +* @NeilGirdhar made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2573 +* @koutoftimer made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2516 +* @ktahar made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2581 +* @bilaljo made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2577 +* @djdt made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2595 +* @jmkerloch made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2584 + +**Full Changelog**: https://github.com/pyqtgraph/pyqtgraph/compare/pyqtgraph-0.13.1...pyqtgraph-0.13.2 + pyqtgraph-0.13.1 ## What's Changed diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 11f02d356c..d537ea3962 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.2.dev0' +__version__ = '0.13.2' ### import all the goodies and add some helper functions for easy CLI use From ae12d70c75f7ffd9e20ccf8dd4ccce1a8bd9e989 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 4 Mar 2023 08:25:13 -0800 Subject: [PATCH 193/306] Add .dev0 to version --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index d537ea3962..11f02d356c 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.2' +__version__ = '0.13.2.dev0' ### import all the goodies and add some helper functions for easy CLI use From 4f2238ef9825bc8f1e00bc923fcb6c55db7b3a96 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sat, 4 Mar 2023 09:20:24 -0800 Subject: [PATCH 194/306] Revert "Add .dev0 to version" --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 11f02d356c..d537ea3962 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.2.dev0' +__version__ = '0.13.2' ### import all the goodies and add some helper functions for easy CLI use From f7605c1a6c74d3c19078bd96b71d6c49f584be9f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 4 Mar 2023 09:21:39 -0800 Subject: [PATCH 195/306] Bump to development version --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index d537ea3962..29aee874e0 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.2' +__version__ = '0.13.3.dev0' ### import all the goodies and add some helper functions for easy CLI use From fcc7193c1d634e6a0af954dd8a8b54fc185f81b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 14:26:21 +0000 Subject: [PATCH 196/306] Bump pytest from 7.2.1 to 7.2.2 in /.github/workflows/etc Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.1 to 7.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.1...7.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 4c0458c7ed..cc3b677b7e 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -18,6 +18,6 @@ matplotlib==3.7.0 h5py==3.8.0 # testing -pytest==7.2.1 +pytest==7.2.2 pytest-xdist==3.2.0 pytest-xvfb==2.0.0; sys_platform == 'linux' From 8d92251a1c4e0f6f08ba9ebc5a1e6ef70644a227 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 14:26:21 +0000 Subject: [PATCH 197/306] Bump matplotlib from 3.7.0 to 3.7.1 in /.github/workflows/etc Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.7.0 to 3.7.1. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.7.0...v3.7.1) --- updated-dependencies: - dependency-name: matplotlib dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 4c0458c7ed..4987cdc5ab 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -14,7 +14,7 @@ numba==0.56.4; python_version == '3.9' pyopengl==3.1.6 # supplimental tools -matplotlib==3.7.0 +matplotlib==3.7.1 h5py==3.8.0 # testing From 96c1be455072c8fd78474d23149942fe4d8e2899 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Mon, 6 Mar 2023 20:39:29 -0800 Subject: [PATCH 198/306] Update pydata-sphinx-theme and fix warnings --- doc/requirements.txt | 3 ++- doc/source/conf.py | 28 +++++++------------- pyqtgraph/graphicsItems/ColorBarItem.py | 4 +-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 43ba56aa66..2faf289996 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,9 @@ PyQt6==6.4.2 sphinx==5.3.0 -pydata-sphinx-theme==0.11.0 +pydata-sphinx-theme==0.13.1 sphinx-design==0.3.0 sphinxcontrib-images==0.9.4 +sphinx-favicon==1.0.1 sphinx-qt-documentation sphinxext-rediraffe numpy diff --git a/doc/source/conf.py b/doc/source/conf.py index bfc97263a2..f61625d989 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -39,6 +39,7 @@ "sphinx.ext.intersphinx", "sphinx_qt_documentation", "sphinx_design", + "sphinx_favicon", "sphinxext.rediraffe", "sphinxcontrib.images", ] @@ -138,37 +139,28 @@ # a list of builtin themes. html_theme = 'pydata_sphinx_theme' +# favicons +favicons = [ + "peegee_03_square_no_bg_32_cleaned.png", + "peegee_04_square_no_bg_180_cleaned.png", + "peegee_03_square_no_bg_32_cleaned.ico" +] + # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "favicons": [ - { - "rel": "icon", - "sizes": "32x32", - "href": "peegee_03_square_no_bg_32_cleaned.png" - }, - { - "rel": "icon", - "href": "peegee_03_square_no_bg_32_cleaned.ico", - "sizes": "any" - }, - { - "rel": "apple-touch-icon", - "href": "peegee_04_square_no_bg_180_cleaned.png" - }, - ], "github_url": "https://github.com/pyqtgraph/pyqtgraph", "navbar_end": ["theme-switcher", "navbar-icon-links"], "twitter_url": "https://twitter.com/pyqtgraph", "use_edit_page_button": False, - "page_sidebar_items": ["page-toc"] + "secondary_sidebar_items": ["page-toc"] } if os.getenv("BUILD_DASH_DOCSET"): html_theme_options |= { - 'page_sidebar_items': [], + 'secondary_sidebar_items': [], "show_prev_next": False, "collapse_navigation": True, } diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py index 6b84c840bf..53c452872b 100644 --- a/pyqtgraph/graphicsItems/ColorBarItem.py +++ b/pyqtgraph/graphicsItems/ColorBarItem.py @@ -8,8 +8,8 @@ from ..Qt import QtCore from .ImageItem import ImageItem from .LinearRegionItem import LinearRegionItem -from .PlotItem import PlotItem from .PColorMeshItem import PColorMeshItem +from .PlotItem import PlotItem __all__ = ['ColorBarItem'] @@ -168,7 +168,7 @@ def setImageItem(self, img, insert_in=None): Parameters ---------- - image: :image item or list of `[image item, image item, ...]` + image: :class:`~pyqtgraph.ImageItem` or list of :class:`~pyqtgraph.ImageItem` Assigns one or more image items to this ColorBarItem. If a :class:`~pyqtgraph.ColorMap` is defined for ColorBarItem, this will be assigned to the ImageItems. Otherwise, the ColorBarItem will attempt to retrieve a color map from the image items. diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index f9cfdf56ef..fb4824ff8b 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -672,7 +672,7 @@ def multiDataPlot(self, *, x=None, y=None, constKwargs=None, **kwargs): Parameters ---------- - x, y: array-like + x, y: array_like can be in the following formats: - {x or y} = [n1, n2, n3, ...]: The named argument iterates through ``n`` curves, while the unspecified argument is range(len(n)) for From b07218064d0c8d90952880b04beb3b1ce20dede9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 14:14:36 +0000 Subject: [PATCH 199/306] Bump pytest-xdist from 3.2.0 to 3.2.1 in /.github/workflows/etc Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/pytest-dev/pytest-xdist/releases) - [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.2.0...v3.2.1) --- updated-dependencies: - dependency-name: pytest-xdist dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index c1522d3e5d..0a868edc0a 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -19,5 +19,5 @@ h5py==3.8.0 # testing pytest==7.2.2 -pytest-xdist==3.2.0 +pytest-xdist==3.2.1 pytest-xvfb==2.0.0; sys_platform == 'linux' From 97a1bdf0d8af00b41088a4fb09ba025beb158fdd Mon Sep 17 00:00:00 2001 From: Nick Dimitroff Date: Mon, 13 Mar 2023 08:37:22 -0700 Subject: [PATCH 200/306] Return float values from QColor in getByIndex (#2648) * Return float values from QColor in getByIndex --- pyqtgraph/colormap.py | 2 +- tests/test_colormap.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 413c4436e1..5a2b8106b8 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -622,7 +622,7 @@ def mapToFloat(self, data): def getByIndex(self, idx): """Retrieve a QColor by the index of the stop it is assigned to.""" - return QtGui.QColor( *self.color[idx] ) + return QtGui.QColor.fromRgbF( *self.color[idx] ) def getGradient(self, p1=None, p2=None): """ diff --git a/tests/test_colormap.py b/tests/test_colormap.py index 0926302c65..16372ca6f5 100644 --- a/tests/test_colormap.py +++ b/tests/test_colormap.py @@ -74,3 +74,8 @@ def test_ColorMap_getColors(color_list): colors = cm.getColors('qcolor') for actual, good in zip(colors, qcols): assert actual.getRgbF() == good.getRgbF() + +def test_ColorMap_getByIndex(): + cm = pg.ColorMap([0.0, 1.0], [(0,0,0), (255,0,0)]) + assert cm.getByIndex(0) == QtGui.QColor.fromRgbF(0.0, 0.0, 0.0, 1.0) + assert cm.getByIndex(1) == QtGui.QColor.fromRgbF(1.0, 0.0, 0.0, 1.0) From ef019347971a6d9102170ac436988e2df2e62f22 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 17 Mar 2023 15:50:00 +0800 Subject: [PATCH 201/306] allow mouseMoveEvent w/o mousePressEvent --- pyqtgraph/opengl/GLViewWidget.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 0a55984a08..56d3fbf2e0 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -418,12 +418,10 @@ def pixelSize(self, pos): xDist = dist * 2. * tan(0.5 * radians(self.opts['fov'])) return xDist / self.width() - def mousePressEvent(self, ev): - lpos = ev.position() if hasattr(ev, 'position') else ev.localPos() - self.mousePos = lpos - def mouseMoveEvent(self, ev): lpos = ev.position() if hasattr(ev, 'position') else ev.localPos() + if not hasattr(self, 'mousePos'): + self.mousePos = lpos diff = lpos - self.mousePos self.mousePos = lpos From cc0a4f047e7f050fc2b9410d99c4591a7565a5b9 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 14 Feb 2023 05:17:35 +0800 Subject: [PATCH 202/306] voidptr(None) works so no need to special case None --- pyqtgraph/functions.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6e1b569eed..ffa50f7195 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2181,26 +2181,18 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True): return path def ndarray_from_qpolygonf(polyline): - nbytes = 2 * len(polyline) * 8 - if QT_LIB.startswith('PyQt'): - buffer = polyline.data() - if buffer is None: - buffer = Qt.sip.voidptr(0) - buffer.setsize(nbytes) - else: - ptr = polyline.data() - if ptr is None: - ptr = 0 - buffer = Qt.shiboken.VoidPtr(ptr, nbytes, True) - memory = np.frombuffer(buffer, np.double).reshape((-1, 2)) - return memory + # polyline.data() will be None if the pointer was null. + # voidptr(None) is the same as voidptr(0). + vp = Qt.compat.voidptr(polyline.data(), len(polyline)*2*8, True) + return np.frombuffer(vp, dtype=np.float64).reshape((-1, 2)) def create_qpolygonf(size): polyline = QtGui.QPolygonF() - if QT_LIB.startswith('PyQt'): - polyline.fill(QtCore.QPointF(), size) - else: + if hasattr(polyline, 'resize'): + # (PySide) and (PyQt6 >= 6.3.1) polyline.resize(size) + else: + polyline.fill(QtCore.QPointF(), size) return polyline def arrayToQPolygonF(x, y): From d1ec515ff670841090d99ee02b613a805a5553e0 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 26 Feb 2023 12:37:44 +0800 Subject: [PATCH 203/306] remove detection of PySide6 drawLines(array, count) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 33 +----------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 1f561710b9..498b3dcb4a 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -15,40 +15,9 @@ __all__ = ['PlotCurveItem'] -def have_native_drawlines_array(): - size = 10 - line = QtCore.QLineF(0, 0, size, size) - qimg = QtGui.QImage(size, size, QtGui.QImage.Format.Format_RGB32) - qimg.fill(QtCore.Qt.GlobalColor.transparent) - painter = QtGui.QPainter(qimg) - painter.setPen(QtCore.Qt.GlobalColor.white) - - try: - painter.drawLines(line, 1) - except TypeError: - success = False - else: - success = True - finally: - painter.end() - - return success - -_have_native_drawlines_array = Qt.QT_LIB.startswith('PySide') and have_native_drawlines_array() - - class LineSegments: def __init__(self): - use_array = None - - # "use_native_drawlines" is pending the following issue and code review - # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1924 - # https://codereview.qt-project.org/c/pyside/pyside-setup/+/415702 - self.use_native_drawlines = Qt.QT_LIB.startswith('PySide') and _have_native_drawlines_array - if self.use_native_drawlines: - use_array = True - - self.array = Qt.internals.PrimitiveArray(QtCore.QLineF, 4, use_array=use_array) + self.array = Qt.internals.PrimitiveArray(QtCore.QLineF, 4) def get(self, size): self.array.resize(size) From f9c593ecf6cc9acf844baa248aaf0c15966fa584 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 26 Feb 2023 12:37:58 +0800 Subject: [PATCH 204/306] add PrimitiveArray.drawargs() --- pyqtgraph/Qt/internals.py | 43 ++++++++++++++++++---- pyqtgraph/graphicsItems/BarGraphItem.py | 4 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 26 ++++++------- pyqtgraph/graphicsItems/ScatterPlotItem.py | 8 +--- tests/graphicsItems/test_PlotCurveItem.py | 5 ++- 5 files changed, 55 insertions(+), 31 deletions(-) diff --git a/pyqtgraph/Qt/internals.py b/pyqtgraph/Qt/internals.py index 1bc62a5f64..de2cf9e7af 100644 --- a/pyqtgraph/Qt/internals.py +++ b/pyqtgraph/Qt/internals.py @@ -8,6 +8,10 @@ if QT_LIB.startswith('PyQt'): from . import sip +elif QT_LIB == 'PySide2': + from PySide2 import __version_info__ as pyside_version_info +elif QT_LIB == 'PySide6': + from PySide6 import __version_info__ as pyside_version_info class QArrayDataQt5(ctypes.Structure): _fields_ = [ @@ -120,6 +124,7 @@ def __init__(self, Klass, nfields, *, use_array=None): if use_array is None: use_array = ( Klass is QtGui.QPainter.PixmapFragment + or pyside_version_info >= (6, 4, 3) ) self.use_ptr_to_array = use_array else: @@ -134,17 +139,18 @@ def resize(self, size): if self.use_sip_array: self._objs = sip.array(self._Klass, size) vp = sip.voidptr(self._objs, size*self._nfields*8) - array = np.frombuffer(vp, dtype=np.float64).reshape((-1, self._nfields)) + self._ndarray = np.frombuffer(vp, dtype=np.float64).reshape((-1, self._nfields)) elif self.use_ptr_to_array: - array = np.empty((size, self._nfields), dtype=np.float64) - self._objs = compat.wrapinstance(array.ctypes.data, self._Klass) + self._ndarray = np.empty((size, self._nfields), dtype=np.float64) + self._objs = None else: - array = np.empty((size, self._nfields), dtype=np.float64) - self._objs = list(map(compat.wrapinstance, - itertools.count(array.ctypes.data, array.strides[0]), - itertools.repeat(self._Klass, array.shape[0]))) + self._ndarray = np.empty((size, self._nfields), dtype=np.float64) + self._objs = self._wrap_instances(self._ndarray) - self._ndarray = array + def _wrap_instances(self, array): + return list(map(compat.wrapinstance, + itertools.count(array.ctypes.data, array.strides[0]), + itertools.repeat(self._Klass, array.shape[0]))) def __len__(self): return len(self._ndarray) @@ -153,4 +159,25 @@ def ndarray(self): return self._ndarray def instances(self): + # this returns an iterable container of Klass instances. + # for "use_ptr_to_array" mode, such a container may not + # be required at all, so its creation is deferred + if self._objs is None: + self._objs = self._wrap_instances(self._ndarray) return self._objs + + def drawargs(self): + # returns arguments to apply to the respective drawPrimitives() functions + if self.use_ptr_to_array: + size = len(self._ndarray) + if size: + # wrap memory only if it is safe to do so + ptr = compat.wrapinstance(self._ndarray.ctypes.data, self._Klass) + else: + # shiboken translates None <--> nullptr + # alternatively, we could instantiate a dummy _Klass() + ptr = None + return ptr, size + + else: + return self._objs, \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 59f18fcfa6..046340e31c 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -233,8 +233,8 @@ def paint(self, p, *args): if self._singleColor: p.setPen(self._sharedPen) p.setBrush(self._sharedBrush) - inst = self._rectarray.instances() - p.drawRects(inst) + drawargs = self._rectarray.drawargs() + p.drawRects(*drawargs) else: if self.picture is None: self.drawPicture() diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 498b3dcb4a..138c120dc6 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -19,14 +19,11 @@ class LineSegments: def __init__(self): self.array = Qt.internals.PrimitiveArray(QtCore.QLineF, 4) - def get(self, size): - self.array.resize(size) - return self.array.instances(), self.array.ndarray() - def arrayToLineSegments(self, x, y, connect, finiteCheck): # analogue of arrayToQPath taking the same parameters if len(x) < 2: - return [], + self.array.resize(0) + return self.array.drawargs() connect_array = None if isinstance(connect, np.ndarray): @@ -58,13 +55,11 @@ def arrayToLineSegments(self, x, y, connect, finiteCheck): x = x[backfill_idx] y = y[backfill_idx] - segs = [] - nsegs = 0 - if connect == 'all': nsegs = len(x) - 1 + self.array.resize(nsegs) if nsegs: - segs, memory = self.get(nsegs) + memory = self.array.ndarray() memory[:, 0] = x[:-1] memory[:, 2] = x[1:] memory[:, 1] = y[:-1] @@ -72,8 +67,9 @@ def arrayToLineSegments(self, x, y, connect, finiteCheck): elif connect == 'pairs': nsegs = len(x) // 2 + self.array.resize(nsegs) if nsegs: - segs, memory = self.get(nsegs) + memory = self.array.ndarray() memory = memory.reshape((-1, 2)) memory[:, 0] = x[:nsegs * 2] memory[:, 1] = y[:nsegs * 2] @@ -83,17 +79,19 @@ def arrayToLineSegments(self, x, y, connect, finiteCheck): # - 'array' # - 'finite' with non-finite elements nsegs = np.count_nonzero(connect_array) + self.array.resize(nsegs) if nsegs: - segs, memory = self.get(nsegs) + memory = self.array.ndarray() memory[:, 0] = x[:-1][connect_array] memory[:, 2] = x[1:][connect_array] memory[:, 1] = y[:-1][connect_array] memory[:, 3] = y[1:][connect_array] - if nsegs and self.use_native_drawlines: - return segs, nsegs else: - return segs, + nsegs = 0 + self.array.resize(nsegs) + + return self.array.drawargs() class PlotCurveItem(GraphicsObject): diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index ee6e2290aa..0a19882b5b 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -958,12 +958,8 @@ def paint(self, p, *args): frags[:, 6:10] = [1.0, 1.0, 0.0, 1.0] # scaleX, scaleY, rotation, opacity profiler('prep') - inst = self._pixmapFragments.instances() - if Qt.QT_LIB.startswith('PySide'): - args = inst, sr.size - else: - args = inst, - p.drawPixmapFragments(*args, self.fragmentAtlas.pixmap) + drawargs = self._pixmapFragments.drawargs() + p.drawPixmapFragments(*drawargs, self.fragmentAtlas.pixmap) profiler('draw') else: # render each symbol individually diff --git a/tests/graphicsItems/test_PlotCurveItem.py b/tests/graphicsItems/test_PlotCurveItem.py index 57e6a082e7..305ffadc5a 100644 --- a/tests/graphicsItems/test_PlotCurveItem.py +++ b/tests/graphicsItems/test_PlotCurveItem.py @@ -45,4 +45,7 @@ def test_LineSegments(): xy = np.array([0.]) segs = ls.arrayToLineSegments(xy, xy, connect='all', finiteCheck=True) assert isinstance(segs, tuple) and len(segs) in [1, 2] - assert len(segs[0]) == 0 + if len(segs) == 1: + assert len(segs[0]) == 0 + elif len(segs) == 2: + assert segs[1] == 0 From 7ae061c14071b813d896d68ac57a9d0cb86d4201 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 26 Feb 2023 12:38:03 +0800 Subject: [PATCH 205/306] allow shrinking w/o reallocating memory --- pyqtgraph/Qt/internals.py | 87 +++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/pyqtgraph/Qt/internals.py b/pyqtgraph/Qt/internals.py index de2cf9e7af..2e8c2c3c8b 100644 --- a/pyqtgraph/Qt/internals.py +++ b/pyqtgraph/Qt/internals.py @@ -105,7 +105,10 @@ class PrimitiveArray: def __init__(self, Klass, nfields, *, use_array=None): self._Klass = Klass self._nfields = nfields - self._ndarray = None + self._capa = -1 + + self.use_sip_array = False + self.use_ptr_to_array = False if QT_LIB.startswith('PyQt'): if use_array is None: @@ -117,8 +120,6 @@ def __init__(self, Klass, nfields, *, use_array=None): ) ) self.use_sip_array = use_array - else: - self.use_sip_array = False if QT_LIB.startswith('PySide'): if use_array is None: @@ -127,25 +128,40 @@ def __init__(self, Klass, nfields, *, use_array=None): or pyside_version_info >= (6, 4, 3) ) self.use_ptr_to_array = use_array - else: - self.use_ptr_to_array = False self.resize(0) def resize(self, size): - if self._ndarray is not None and len(self._ndarray) == size: - return - if self.use_sip_array: - self._objs = sip.array(self._Klass, size) - vp = sip.voidptr(self._objs, size*self._nfields*8) - self._ndarray = np.frombuffer(vp, dtype=np.float64).reshape((-1, self._nfields)) - elif self.use_ptr_to_array: - self._ndarray = np.empty((size, self._nfields), dtype=np.float64) - self._objs = None + # For reference, SIP_VERSION 6.7.8 first arrived + # in PyQt5_sip 12.11.2 and PyQt6_sip 13.4.2 + if sip.SIP_VERSION >= 0x60708: + if size <= self._capa: + self._size = size + return + else: + # sip.array prior to SIP_VERSION 6.7.8 had a + # buggy slicing implementation. + # so trigger a reallocate for any different size + if size == self._capa: + return + + self._siparray = sip.array(self._Klass, size) + else: + if size <= self._capa: + self._size = size + return self._ndarray = np.empty((size, self._nfields), dtype=np.float64) - self._objs = self._wrap_instances(self._ndarray) + + if self.use_ptr_to_array: + # defer creation + self._objs = None + else: + self._objs = self._wrap_instances(self._ndarray) + + self._capa = size + self._size = size def _wrap_instances(self, array): return list(map(compat.wrapinstance, @@ -153,31 +169,58 @@ def _wrap_instances(self, array): itertools.repeat(self._Klass, array.shape[0]))) def __len__(self): - return len(self._ndarray) + return self._size def ndarray(self): - return self._ndarray + # ndarray views are cheap to recreate each time + if self.use_sip_array: + if sip.SIP_VERSION >= 0x60708: + mv = self._siparray + else: + # sip.array prior to SIP_VERSION 6.7.8 had a buggy buffer protocol + # that set the wrong size. + # workaround it by going through a sip.voidptr + mv = sip.voidptr(self._siparray, self._capa*self._nfields*8) + # note that we perform the slicing by using only _size rows + nd = np.frombuffer(mv, dtype=np.float64, count=self._size*self._nfields) + return nd.reshape((-1, self._nfields)) + else: + return self._ndarray[:self._size] def instances(self): # this returns an iterable container of Klass instances. # for "use_ptr_to_array" mode, such a container may not # be required at all, so its creation is deferred + if self.use_sip_array: + if self._size == self._capa: + # avoiding slicing when it's not necessary + # handles the case where sip.array had a buggy + # slicing implementation + return self._siparray + else: + # this is a view + return self._siparray[:self._size] + if self._objs is None: self._objs = self._wrap_instances(self._ndarray) - return self._objs + + if self._size == self._capa: + return self._objs + else: + # this is a shallow copy + return self._objs[:self._size] def drawargs(self): # returns arguments to apply to the respective drawPrimitives() functions if self.use_ptr_to_array: - size = len(self._ndarray) - if size: + if self._capa > 0: # wrap memory only if it is safe to do so ptr = compat.wrapinstance(self._ndarray.ctypes.data, self._Klass) else: # shiboken translates None <--> nullptr # alternatively, we could instantiate a dummy _Klass() ptr = None - return ptr, size + return ptr, self._size else: - return self._objs, \ No newline at end of file + return self.instances(), From 8e12f3bc03f33b28c0765790fde143d4afb35b7d Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 7 Mar 2023 20:19:00 +0800 Subject: [PATCH 206/306] convert LineSegments.arrayToLineSegments() to free standing function --- pyqtgraph/graphicsItems/PlotCurveItem.py | 171 +++++++++++----------- tests/graphicsItems/test_PlotCurveItem.py | 9 +- 2 files changed, 89 insertions(+), 91 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 138c120dc6..0e4b1276cb 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -15,84 +15,82 @@ __all__ = ['PlotCurveItem'] -class LineSegments: - def __init__(self): - self.array = Qt.internals.PrimitiveArray(QtCore.QLineF, 4) - - def arrayToLineSegments(self, x, y, connect, finiteCheck): - # analogue of arrayToQPath taking the same parameters - if len(x) < 2: - self.array.resize(0) - return self.array.drawargs() - - connect_array = None - if isinstance(connect, np.ndarray): - # the last element is not used - connect_array, connect = np.asarray(connect[:-1], dtype=bool), 'array' - - all_finite = True - if finiteCheck or connect == 'finite': - mask = np.isfinite(x) & np.isfinite(y) - all_finite = np.all(mask) - - if connect == 'all': - if not all_finite: - # remove non-finite points, if any - x = x[mask] - y = y[mask] - - elif connect == 'finite': - if all_finite: - connect = 'all' - else: - # each non-finite point affects the segment before and after - connect_array = mask[:-1] & mask[1:] - - elif connect in ['pairs', 'array']: - if not all_finite: - # replicate the behavior of arrayToQPath - backfill_idx = fn._compute_backfill_indices(mask) - x = x[backfill_idx] - y = y[backfill_idx] - - if connect == 'all': - nsegs = len(x) - 1 - self.array.resize(nsegs) - if nsegs: - memory = self.array.ndarray() - memory[:, 0] = x[:-1] - memory[:, 2] = x[1:] - memory[:, 1] = y[:-1] - memory[:, 3] = y[1:] - - elif connect == 'pairs': - nsegs = len(x) // 2 - self.array.resize(nsegs) - if nsegs: - memory = self.array.ndarray() - memory = memory.reshape((-1, 2)) - memory[:, 0] = x[:nsegs * 2] - memory[:, 1] = y[:nsegs * 2] - - elif connect_array is not None: - # the following are handled here - # - 'array' - # - 'finite' with non-finite elements - nsegs = np.count_nonzero(connect_array) - self.array.resize(nsegs) - if nsegs: - memory = self.array.ndarray() - memory[:, 0] = x[:-1][connect_array] - memory[:, 2] = x[1:][connect_array] - memory[:, 1] = y[:-1][connect_array] - memory[:, 3] = y[1:][connect_array] - +def arrayToLineSegments(x, y, connect, finiteCheck, out=None): + if out is None: + out = Qt.internals.PrimitiveArray(QtCore.QLineF, 4) + + # analogue of arrayToQPath taking the same parameters + if len(x) < 2: + out.resize(0) + return out + + connect_array = None + if isinstance(connect, np.ndarray): + # the last element is not used + connect_array, connect = np.asarray(connect[:-1], dtype=bool), 'array' + + all_finite = True + if finiteCheck or connect == 'finite': + mask = np.isfinite(x) & np.isfinite(y) + all_finite = np.all(mask) + + if connect == 'all': + if not all_finite: + # remove non-finite points, if any + x = x[mask] + y = y[mask] + + elif connect == 'finite': + if all_finite: + connect = 'all' else: - nsegs = 0 - self.array.resize(nsegs) - - return self.array.drawargs() - + # each non-finite point affects the segment before and after + connect_array = mask[:-1] & mask[1:] + + elif connect in ['pairs', 'array']: + if not all_finite: + # replicate the behavior of arrayToQPath + backfill_idx = fn._compute_backfill_indices(mask) + x = x[backfill_idx] + y = y[backfill_idx] + + if connect == 'all': + nsegs = len(x) - 1 + out.resize(nsegs) + if nsegs: + memory = out.ndarray() + memory[:, 0] = x[:-1] + memory[:, 2] = x[1:] + memory[:, 1] = y[:-1] + memory[:, 3] = y[1:] + + elif connect == 'pairs': + nsegs = len(x) // 2 + out.resize(nsegs) + if nsegs: + memory = out.ndarray() + memory = memory.reshape((-1, 2)) + memory[:, 0] = x[:nsegs * 2] + memory[:, 1] = y[:nsegs * 2] + + elif connect_array is not None: + # the following are handled here + # - 'array' + # - 'finite' with non-finite elements + nsegs = np.count_nonzero(connect_array) + out.resize(nsegs) + if nsegs: + memory = out.ndarray() + memory[:, 0] = x[:-1][connect_array] + memory[:, 2] = x[1:][connect_array] + memory[:, 1] = y[:-1][connect_array] + memory[:, 3] = y[1:][connect_array] + + else: + nsegs = 0 + out.resize(nsegs) + + return out class PlotCurveItem(GraphicsObject): """ @@ -540,7 +538,7 @@ def updateData(self, *args, **kargs): self.fillPath = None self._fillPathList = None self._mouseShape = None - self._renderSegmentList = None + self._lineSegmentsRendered = False if 'name' in kargs: self.opts['name'] = kargs['name'] @@ -670,10 +668,7 @@ def _shouldUseDrawLineSegments(self, pen): ) def _getLineSegments(self): - if not hasattr(self, '_lineSegments'): - self._lineSegments = LineSegments() - - if self._renderSegmentList is None: + if not self._lineSegmentsRendered: x, y = self.getData() if self.opts['stepMode']: x, y = self._generateStepModeData( @@ -683,14 +678,17 @@ def _getLineSegments(self): baseline=self.opts['fillLevel'] ) - self._renderSegmentList = self._lineSegments.arrayToLineSegments( + self._lineSegments = arrayToLineSegments( x, y, connect=self.opts['connect'], - finiteCheck=not self.opts['skipFiniteCheck'] + finiteCheck=not self.opts['skipFiniteCheck'], + out=self._lineSegments ) - return self._renderSegmentList + self._lineSegmentsRendered = True + + return self._lineSegments.drawargs() def _getClosingSegments(self): # this is only used for fillOutline @@ -955,7 +953,8 @@ def paintGL(self, p, opt, widget): def clear(self): self.xData = None ## raw values self.yData = None - self._renderSegmentList = None + self._lineSegments = None + self._lineSegmentsRendered = False self.path = None self.fillPath = None self._fillPathList = None diff --git a/tests/graphicsItems/test_PlotCurveItem.py b/tests/graphicsItems/test_PlotCurveItem.py index 305ffadc5a..43a5a75a53 100644 --- a/tests/graphicsItems/test_PlotCurveItem.py +++ b/tests/graphicsItems/test_PlotCurveItem.py @@ -1,7 +1,7 @@ import numpy as np import pyqtgraph as pg -from pyqtgraph.graphicsItems.PlotCurveItem import LineSegments +from pyqtgraph.graphicsItems.PlotCurveItem import arrayToLineSegments from tests.image_testing import assertImageApproved @@ -38,12 +38,11 @@ def test_PlotCurveItem(): p.close() -def test_LineSegments(): - ls = LineSegments() - +def test_arrayToLineSegments(): # test the boundary case where the dataset consists of a single point xy = np.array([0.]) - segs = ls.arrayToLineSegments(xy, xy, connect='all', finiteCheck=True) + parray = arrayToLineSegments(xy, xy, connect='all', finiteCheck=True) + segs = parray.drawargs() assert isinstance(segs, tuple) and len(segs) in [1, 2] if len(segs) == 1: assert len(segs[0]) == 0 From caa564d00abe2304ff49f699b5b8bcdb27969da4 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 4 Mar 2023 11:43:41 +0800 Subject: [PATCH 207/306] test_PlotCurveItem: switch to non-SegmentedLineMode --- tests/graphicsItems/test_PlotCurveItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/graphicsItems/test_PlotCurveItem.py b/tests/graphicsItems/test_PlotCurveItem.py index 43a5a75a53..77873164ac 100644 --- a/tests/graphicsItems/test_PlotCurveItem.py +++ b/tests/graphicsItems/test_PlotCurveItem.py @@ -13,6 +13,7 @@ def test_PlotCurveItem(): v = p.addViewBox() data = np.array([1,4,2,3,np.inf,5,7,6,-np.inf,8,10,9,np.nan,-1,-2,0]) c = pg.PlotCurveItem(data) + c.setSegmentedLineMode('off') # test images assume non-segmented-line-mode v.addItem(c) v.autoRange() From c47168ea138d6b5428290398af47da3dd09c7fd8 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Mon, 20 Mar 2023 20:55:53 +0800 Subject: [PATCH 208/306] re-enable tests taking gui thread on PySide6 PYSIDE-2254 was fixed --- tests/widgets/test_busycursor.py | 2 +- tests/widgets/test_progressdialog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/widgets/test_busycursor.py b/tests/widgets/test_busycursor.py index 180a6e23d7..2647d99163 100644 --- a/tests/widgets/test_busycursor.py +++ b/tests/widgets/test_busycursor.py @@ -8,7 +8,7 @@ @pytest.mark.skipif( sys.platform.startswith("linux") and pg.Qt.QT_LIB == "PySide6" - and pg.Qt.PySide6.__version_info__ > (6, 0), + and (6, 0) < pg.Qt.PySide6.__version_info__ < (6, 4, 3), reason="taking gui thread causes segfault" ) def test_nested_busy_cursors_clear_after_all_exit(): diff --git a/tests/widgets/test_progressdialog.py b/tests/widgets/test_progressdialog.py index 50a580b467..2eb3f8a4ea 100644 --- a/tests/widgets/test_progressdialog.py +++ b/tests/widgets/test_progressdialog.py @@ -8,7 +8,7 @@ @pytest.mark.skipif( sys.platform.startswith("linux") and pg.Qt.QT_LIB == "PySide6" - and pg.Qt.PySide6.__version_info__ > (6, 0), + and (6, 0) < pg.Qt.PySide6.__version_info__ < (6, 4, 3), reason="taking gui thread causes segfault" ) def test_progress_dialog(): From 138f5ccd6c63fc3e40dc72317e5bdbc47664fb9e Mon Sep 17 00:00:00 2001 From: Jindrich Makovicka Date: Wed, 22 Mar 2023 13:00:57 -0400 Subject: [PATCH 209/306] Tolerate an empty BarGraphItem Sometimes, it is useful to be able to create an empty BarGraphItem to be populated later using setOpts, like bg = pg.BarGraphItem( x0=[], height=[], y0=[], width=[], pen=..., brush=... ) Avoid NumPy ValueError when computing np.max/min from and empty array in this case. --- pyqtgraph/graphicsItems/BarGraphItem.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 59f18fcfa6..a8c52dea05 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -185,6 +185,11 @@ def asarray(x): def _prepareData(self): x0, y0, x1, y1 = self._getNormalizedCoords() + if x0.size == 0 or y0.size == 0: + self._dataBounds = (None, None), (None, None) + self._rectarray.resize(0) + return + xmn, xmx = np.min(x0), np.max(x1) ymn, ymx = np.min(y0), np.max(y1) self._dataBounds = (xmn, xmx), (ymn, ymx) @@ -266,6 +271,9 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): pw = self._penWidth[0] * 0.5 # _dataBounds is available after _prepareData() bounds = self._dataBounds[ax] + if bounds[0] is None or bounds[1] is None: + return None, None + return (bounds[0] - pw, bounds[1] + pw) def pixelPadding(self): From 48a6106fcb06e6325750d67b893a843ab7948022 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 25 Mar 2023 12:59:08 +0800 Subject: [PATCH 210/306] inherit GraphicsWidgetAnchor on the left-hand-side --- pyqtgraph/graphicsItems/LabelItem.py | 2 +- pyqtgraph/graphicsItems/LegendItem.py | 2 +- pyqtgraph/graphicsItems/ScaleBar.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/LabelItem.py b/pyqtgraph/graphicsItems/LabelItem.py index 79f3280d14..85a6d88075 100644 --- a/pyqtgraph/graphicsItems/LabelItem.py +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -6,7 +6,7 @@ __all__ = ['LabelItem'] -class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): +class LabelItem(GraphicsWidgetAnchor, GraphicsWidget): """ GraphicsWidget displaying text. Used mainly as axis labels, titles, etc. diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 92b512d5d6..024a140ef6 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -14,7 +14,7 @@ __all__ = ['LegendItem', 'ItemSample'] -class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): +class LegendItem(GraphicsWidgetAnchor, GraphicsWidget): """ Displays a legend used for describing the contents of a plot. diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py index d4942a1b78..eb940962f0 100644 --- a/pyqtgraph/graphicsItems/ScaleBar.py +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -8,7 +8,7 @@ __all__ = ['ScaleBar'] -class ScaleBar(GraphicsObject, GraphicsWidgetAnchor): +class ScaleBar(GraphicsWidgetAnchor, GraphicsObject): """ Displays a rectangular bar to indicate the relative scale of objects on the view. """ From 14ff67b4e6e5f69acf65e970f326518ae02abbdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Mar 2023 14:06:21 +0000 Subject: [PATCH 211/306] Bump pydata-sphinx-theme from 0.13.1 to 0.13.2 in /doc Bumps [pydata-sphinx-theme](https://github.com/pydata/pydata-sphinx-theme) from 0.13.1 to 0.13.2. - [Release notes](https://github.com/pydata/pydata-sphinx-theme/releases) - [Commits](https://github.com/pydata/pydata-sphinx-theme/compare/v0.13.1...v0.13.2) --- updated-dependencies: - dependency-name: pydata-sphinx-theme dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 2faf289996..d92ab271d4 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ PyQt6==6.4.2 sphinx==5.3.0 -pydata-sphinx-theme==0.13.1 +pydata-sphinx-theme==0.13.2 sphinx-design==0.3.0 sphinxcontrib-images==0.9.4 sphinx-favicon==1.0.1 From 28cb2e768e24ef77cde6f496e60c0627e4bfde6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 14:02:43 +0000 Subject: [PATCH 212/306] Bump pydata-sphinx-theme from 0.13.2 to 0.13.3 in /doc Bumps [pydata-sphinx-theme](https://github.com/pydata/pydata-sphinx-theme) from 0.13.2 to 0.13.3. - [Release notes](https://github.com/pydata/pydata-sphinx-theme/releases) - [Commits](https://github.com/pydata/pydata-sphinx-theme/compare/v0.13.2...v0.13.3) --- updated-dependencies: - dependency-name: pydata-sphinx-theme dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index d92ab271d4..aa2ab42d04 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ PyQt6==6.4.2 sphinx==5.3.0 -pydata-sphinx-theme==0.13.2 +pydata-sphinx-theme==0.13.3 sphinx-design==0.3.0 sphinxcontrib-images==0.9.4 sphinx-favicon==1.0.1 From f470eee3e730b6416e8e28109e75c2ee33b7bd23 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Tue, 4 Apr 2023 21:08:50 -0700 Subject: [PATCH 213/306] Add libxcb-cursor0 thanks @ksunden --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 764489b4d3..95e02cd68e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -105,7 +105,8 @@ jobs: libxcb-randr0 \ libxcb-render-util0 \ libxcb-xinerama0 \ - libopengl0 + libopengl0 \ + libxcb-cursor0 - name: 'Debug Info' run: | echo python location: `which python` @@ -204,7 +205,8 @@ jobs: libxcb-randr0 \ libxcb-render-util0 \ libxcb-xinerama0 \ - libopengl0 + libopengl0 \ + libxcb-cursor0 pip install pytest-xvfb - name: 'Debug Info' run: | From 08f01be743cba772ae91ef567e6db59672d38f6c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 6 Apr 2023 18:29:52 -0700 Subject: [PATCH 214/306] Console upgrade (#2612) * split stackwidget out of consolewidget * use built in code package to handle single/multiline, rework text style handling (html is too unpredictable in qtextedit) * refactor exception handling and repl into separate modules, remove template * refactor stdout catching to a new class * Fix autoscroll * minor cleanups to exception handler * overhaul to display chained exceptions * fix stack display * fix bug preventing running edited code * clean up exmples * fix history read/write, exception handling bug * add missing example file * fix pasting multi-statement code * Fix console colors in dark mode --- pyqtgraph/console/CmdInput.py | 9 + pyqtgraph/console/Console.py | 492 ++++-------------- pyqtgraph/console/exception_widget.py | 243 +++++++++ pyqtgraph/console/repl_widget.py | 219 ++++++++ pyqtgraph/console/stackwidget.py | 157 ++++++ pyqtgraph/console/template.ui | 217 -------- pyqtgraph/console/template_generic.py | 119 ----- pyqtgraph/debug.py | 54 +- pyqtgraph/examples/ConsoleWidget.py | 1 + pyqtgraph/examples/ExampleApp.py | 4 +- .../examples/console_exception_inspection.py | 199 +++++++ pyqtgraph/examples/utils.py | 9 +- 12 files changed, 965 insertions(+), 758 deletions(-) create mode 100644 pyqtgraph/console/exception_widget.py create mode 100644 pyqtgraph/console/repl_widget.py create mode 100644 pyqtgraph/console/stackwidget.py delete mode 100644 pyqtgraph/console/template.ui delete mode 100644 pyqtgraph/console/template_generic.py create mode 100644 pyqtgraph/examples/console_exception_inspection.py diff --git a/pyqtgraph/console/CmdInput.py b/pyqtgraph/console/CmdInput.py index b291cbcee1..c41f5763ee 100644 --- a/pyqtgraph/console/CmdInput.py +++ b/pyqtgraph/console/CmdInput.py @@ -7,9 +7,18 @@ class CmdInput(QtWidgets.QLineEdit): def __init__(self, parent): QtWidgets.QLineEdit.__init__(self, parent) + self.ps1 = ">>> " + self.ps2 = "... " self.history = [""] self.ptr = 0 + self.setMultiline(False) + def setMultiline(self, ml): + if ml: + self.setPlaceholderText(self.ps2) + else: + self.setPlaceholderText(self.ps1) + def keyPressEvent(self, ev): if ev.key() == QtCore.Qt.Key.Key_Up: if self.ptr < len(self.history) - 1: diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 0b3381552e..104947210c 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,14 +1,12 @@ +import os +import sys import pickle -import re import subprocess -import sys -import traceback -from .. import exceptionHandling as exceptionHandling from .. import getConfigOption -from ..functions import SignalBlock -from ..Qt import QtCore, QtGui, QtWidgets -from . import template_generic as ui_template +from ..Qt import QtCore, QtWidgets +from .repl_widget import ReplWidget +from .exception_widget import ExceptionHandlerWidget class ConsoleWidget(QtWidgets.QWidget): @@ -29,8 +27,6 @@ class ConsoleWidget(QtWidgets.QWidget): - ability to add extra features like exception stack introspection - ability to have multiple interactive prompts, including for spawned sub-processes """ - _threadException = QtCore.Signal(object) - def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None): """ ============== ============================================================================= @@ -45,19 +41,18 @@ def __init__(self, parent=None, namespace=None, historyFile=None, text=None, edi ============== ============================================================================= """ QtWidgets.QWidget.__init__(self, parent) + + self._setupUi() + if namespace is None: namespace = {} namespace['__console__'] = self + self.localNamespace = namespace self.editor = editor - self.multiline = None - self.inCmd = False - self.frames = [] # stack frames to access when an item in the stack list is selected - self.ui = ui_template.Ui_Form() - self.ui.setupUi(self) - self.output = self.ui.output - self.input = self.ui.input + self.output = self.repl.output + self.input = self.repl.input self.input.setFocus() if text is not None: @@ -65,34 +60,70 @@ def __init__(self, parent=None, namespace=None, historyFile=None, text=None, edi self.historyFile = historyFile - history = self.loadHistory() + try: + history = self.loadHistory() + except Exception as exc: + sys.excepthook(*sys.exc_info()) + history = None if history is not None: self.input.history = [""] + history - self.ui.historyList.addItems(history[::-1]) - self.ui.historyList.hide() - self.ui.exceptionGroup.hide() - - self.input.sigExecuteCmd.connect(self.runCmd) - self.ui.historyBtn.toggled.connect(self.ui.historyList.setVisible) - self.ui.historyList.itemClicked.connect(self.cmdSelected) - self.ui.historyList.itemDoubleClicked.connect(self.cmdDblClicked) - self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible) - - self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions) - self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextException) - self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked) - self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked) - self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked) - self.ui.onlyUncaughtCheck.toggled.connect(self.updateSysTrace) - + self.historyList.addItems(history[::-1]) + self.currentTraceback = None - # send exceptions raised in non-gui threads back to the main thread by signal. - self._threadException.connect(self._threadExceptionHandler) - + def _setupUi(self): + self.layout = QtWidgets.QGridLayout(self) + self.setLayout(self.layout) + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) + + self.splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical, self) + self.layout.addWidget(self.splitter, 0, 0) + + self.repl = ReplWidget(self.globals, self.locals, self) + self.splitter.addWidget(self.repl) + + self.historyList = QtWidgets.QListWidget(self) + self.historyList.hide() + self.splitter.addWidget(self.historyList) + + self.historyBtn = QtWidgets.QPushButton('History', self) + self.historyBtn.setCheckable(True) + self.repl.inputLayout.addWidget(self.historyBtn) + + self.repl.sigCommandEntered.connect(self._commandEntered) + self.repl.sigCommandRaisedException.connect(self._commandRaisedException) + + self.excHandler = ExceptionHandlerWidget(self) + self.excHandler.hide() + self.splitter.addWidget(self.excHandler) + + self.exceptionBtn = QtWidgets.QPushButton("Exceptions..", self) + self.exceptionBtn.setCheckable(True) + self.repl.inputLayout.addWidget(self.exceptionBtn) + + self.excHandler.sigStackItemDblClicked.connect(self._stackItemDblClicked) + self.exceptionBtn.toggled.connect(self.excHandler.setVisible) + self.historyBtn.toggled.connect(self.historyList.setVisible) + self.historyList.itemClicked.connect(self.cmdSelected) + self.historyList.itemDoubleClicked.connect(self.cmdDblClicked) + + def catchAllExceptions(self, catch=True): + if catch: + self.exceptionBtn.setChecked(True) + self.excHandler.catchAllExceptions(catch) + + def catchNextException(self, catch=True): + if catch: + self.exceptionBtn.setChecked(True) + self.excHandler.catchNextException(catch) + + def setStack(self, frame=None): + self.excHandler.setStack(frame) + def loadHistory(self): """Return the list of previously-invoked command strings (or None).""" - if self.historyFile is not None: + if self.historyFile is not None and os.path.exists(self.historyFile): with open(self.historyFile, 'rb') as pf: return pickle.load(pf) @@ -100,395 +131,48 @@ def saveHistory(self, history): """Store the list of previously-invoked command strings.""" if self.historyFile is not None: with open(self.historyFile, 'wb') as pf: - pickle.dump(pf, history) - - def runCmd(self, cmd): - #cmd = str(self.input.lastCmd) - - orig_stdout = sys.stdout - orig_stderr = sys.stderr - encCmd = re.sub(r'>', '>', re.sub(r'<', '<', cmd)) - encCmd = re.sub(r' ', ' ', encCmd) + pickle.dump(history, pf) - self.ui.historyList.addItem(cmd) + def _commandEntered(self, repl, cmd): + self.historyList.addItem(cmd) self.saveHistory(self.input.history[1:100]) - - try: - sys.stdout = self - sys.stderr = self - if self.multiline is not None: - self.write("
    %s\n"%encCmd, html=True, scrollToBottom=True) - self.execMulti(cmd) - else: - self.write("
    %s\n"%encCmd, html=True, scrollToBottom=True) - self.inCmd = True - self.execSingle(cmd) - - if not self.inCmd: - self.write("
    \n", html=True, scrollToBottom=True) - - finally: - sys.stdout = orig_stdout - sys.stderr = orig_stderr - - sb = self.ui.historyList.verticalScrollBar() - sb.setValue(sb.maximum()) - + sb = self.historyList.verticalScrollBar() + sb.setValue(sb.maximum()) + + def _commandRaisedException(self, repl, exc): + self.excHandler.exceptionHandler(exc) + def globals(self): - frame = self.currentFrame() - if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): - return self.currentFrame().f_globals + frame = self.excHandler.selectedFrame() + if frame is not None and self.excHandler.runSelectedFrameCheck.isChecked(): + return frame.f_globals else: return self.localNamespace def locals(self): - frame = self.currentFrame() - if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): - return self.currentFrame().f_locals + frame = self.excHandler.selectedFrame() + if frame is not None and self.excHandler.runSelectedFrameCheck.isChecked(): + return frame.f_locals else: return self.localNamespace - - def currentFrame(self): - ## Return the currently selected exception stack frame (or None if there is no exception) - index = self.ui.exceptionStackList.currentRow() - if index >= 0 and index < len(self.frames): - return self.frames[index] - else: - return None - - def execSingle(self, cmd): - try: - output = eval(cmd, self.globals(), self.locals()) - self.write(repr(output) + '\n') - return - except SyntaxError: - pass - except: - self.displayException() - return - - # eval failed with syntax error; try exec instead - try: - exec(cmd, self.globals(), self.locals()) - except SyntaxError as exc: - if 'unexpected EOF' in exc.msg: - self.multiline = cmd - else: - self.displayException() - except: - self.displayException() - - def execMulti(self, nextLine): - if nextLine.strip() != '': - self.multiline += "\n" + nextLine - return - else: - cmd = self.multiline - - try: - output = eval(cmd, self.globals(), self.locals()) - self.write(str(output) + '\n') - self.multiline = None - return - except SyntaxError: - pass - except: - self.displayException() - self.multiline = None - return - - # eval failed with syntax error; try exec instead - try: - exec(cmd, self.globals(), self.locals()) - self.multiline = None - except SyntaxError as exc: - if 'unexpected EOF' in exc.msg: - self.multiline = cmd - else: - self.displayException() - self.multiline = None - except: - self.displayException() - self.multiline = None - - - def write(self, strn, html=False, scrollToBottom='auto'): - """Write a string into the console. - - If scrollToBottom is 'auto', then the console is automatically scrolled - to fit the new text only if it was already at the bottom. - """ - isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() - if not isGuiThread: - sys.__stdout__.write(strn) - return - - sb = self.output.verticalScrollBar() - scroll = sb.value() - if scrollToBottom == 'auto': - atBottom = scroll == sb.maximum() - scrollToBottom = atBottom - - self.output.moveCursor(QtGui.QTextCursor.MoveOperation.End) - if html: - self.output.textCursor().insertHtml(strn) - else: - if self.inCmd: - self.inCmd = False - self.output.textCursor().insertHtml("
    ") - self.output.insertPlainText(strn) - - if scrollToBottom: - sb.setValue(sb.maximum()) - else: - sb.setValue(scroll) - - def fileno(self): - # Need to implement this since we temporarily occlude sys.stdout, and someone may be looking for it (faulthandler, for example) - return 1 - - def displayException(self): - """ - Display the current exception and stack. - """ - tb = traceback.format_exc() - lines = [] - indent = 4 - prefix = '' - for l in tb.split('\n'): - lines.append(" "*indent + prefix + l) - self.write('\n'.join(lines)) - self.exceptionHandler(*sys.exc_info()) - def cmdSelected(self, item): - index = -(self.ui.historyList.row(item)+1) + index = -(self.historyList.row(item)+1) self.input.setHistory(index) self.input.setFocus() def cmdDblClicked(self, item): - index = -(self.ui.historyList.row(item)+1) + index = -(self.historyList.row(item)+1) self.input.setHistory(index) self.input.execCmd() - def flush(self): - pass - - def catchAllExceptions(self, catch=True): - """ - If True, the console will catch all unhandled exceptions and display the stack - trace. Each exception caught clears the last. - """ - with SignalBlock(self.ui.catchAllExceptionsBtn.toggled, self.catchAllExceptions): - self.ui.catchAllExceptionsBtn.setChecked(catch) - - if catch: - with SignalBlock(self.ui.catchNextExceptionBtn.toggled, self.catchNextException): - self.ui.catchNextExceptionBtn.setChecked(False) - self.enableExceptionHandling() - self.ui.exceptionBtn.setChecked(True) - else: - self.disableExceptionHandling() - - def catchNextException(self, catch=True): - """ - If True, the console will catch the next unhandled exception and display the stack - trace. - """ - with SignalBlock(self.ui.catchNextExceptionBtn.toggled, self.catchNextException): - self.ui.catchNextExceptionBtn.setChecked(catch) - if catch: - with SignalBlock(self.ui.catchAllExceptionsBtn.toggled, self.catchAllExceptions): - self.ui.catchAllExceptionsBtn.setChecked(False) - self.enableExceptionHandling() - self.ui.exceptionBtn.setChecked(True) - else: - self.disableExceptionHandling() - - def enableExceptionHandling(self): - exceptionHandling.register(self.exceptionHandler) - self.updateSysTrace() - - def disableExceptionHandling(self): - exceptionHandling.unregister(self.exceptionHandler) - self.updateSysTrace() - - def clearExceptionClicked(self): - self.currentTraceback = None - self.frames = [] - self.ui.exceptionInfoLabel.setText("[No current exception]") - self.ui.exceptionStackList.clear() - self.ui.clearExceptionBtn.setEnabled(False) - - def stackItemClicked(self, item): - pass - - def stackItemDblClicked(self, item): + def _stackItemDblClicked(self, handler, item): editor = self.editor if editor is None: editor = getConfigOption('editorCommand') if editor is None: return - tb = self.currentFrame() + tb = self.excHandler.selectedFrame() lineNum = tb.f_lineno fileName = tb.f_code.co_filename subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True) - - def updateSysTrace(self): - ## Install or uninstall sys.settrace handler - - if not self.ui.catchNextExceptionBtn.isChecked() and not self.ui.catchAllExceptionsBtn.isChecked(): - if sys.gettrace() == self.systrace: - sys.settrace(None) - return - - if self.ui.onlyUncaughtCheck.isChecked(): - if sys.gettrace() == self.systrace: - sys.settrace(None) - else: - if sys.gettrace() is not None and sys.gettrace() != self.systrace: - self.ui.onlyUncaughtCheck.setChecked(False) - raise Exception("sys.settrace is in use; cannot monitor for caught exceptions.") - else: - sys.settrace(self.systrace) - - def exceptionHandler(self, excType, exc, tb, systrace=False, frame=None): - if frame is None: - frame = sys._getframe() - - # exceptions raised in non-gui threads must be handled separately - isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() - if not isGuiThread: - # sending a frame from one thread to another.. probably not safe, but better than just - # dropping the exception? - self._threadException.emit((excType, exc, tb, systrace, frame.f_back)) - return - - if self.ui.catchNextExceptionBtn.isChecked(): - self.ui.catchNextExceptionBtn.setChecked(False) - elif not self.ui.catchAllExceptionsBtn.isChecked(): - return - - self.currentTraceback = tb - - excMessage = ''.join(traceback.format_exception_only(excType, exc)) - self.ui.exceptionInfoLabel.setText(excMessage) - - if systrace: - # exceptions caught using systrace don't need the usual - # call stack + traceback handling - self.setStack(frame.f_back.f_back) - else: - self.setStack(frame=frame.f_back, tb=tb) - - def _threadExceptionHandler(self, args): - self.exceptionHandler(*args) - - def setStack(self, frame=None, tb=None): - """Display a call stack and exception traceback. - - This allows the user to probe the contents of any frame in the given stack. - - *frame* may either be a Frame instance or None, in which case the current - frame is retrieved from ``sys._getframe()``. - - If *tb* is provided then the frames in the traceback will be appended to - the end of the stack list. If *tb* is None, then sys.exc_info() will - be checked instead. - """ - self.ui.clearExceptionBtn.setEnabled(True) - - if frame is None: - frame = sys._getframe().f_back - - if tb is None: - tb = sys.exc_info()[2] - - self.ui.exceptionStackList.clear() - self.frames = [] - - # Build stack up to this point - for index, line in enumerate(traceback.extract_stack(frame)): - # extract_stack return value changed in python 3.5 - if 'FrameSummary' in str(type(line)): - line = (line.filename, line.lineno, line.name, line._line) - - self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) - while frame is not None: - self.frames.insert(0, frame) - frame = frame.f_back - - if tb is None: - return - - self.ui.exceptionStackList.addItem('-- exception caught here: --') - item = self.ui.exceptionStackList.item(self.ui.exceptionStackList.count()-1) - item.setBackground(QtGui.QBrush(QtGui.QColor(200, 200, 200))) - item.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50))) - self.frames.append(None) - - # And finish the rest of the stack up to the exception - for index, line in enumerate(traceback.extract_tb(tb)): - # extract_stack return value changed in python 3.5 - if 'FrameSummary' in str(type(line)): - line = (line.filename, line.lineno, line.name, line._line) - - self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) - while tb is not None: - self.frames.append(tb.tb_frame) - tb = tb.tb_next - - def systrace(self, frame, event, arg): - if event == 'exception' and self.checkException(*arg): - self.exceptionHandler(*arg, systrace=True) - return self.systrace - - def checkException(self, excType, exc, tb): - ## Return True if the exception is interesting; False if it should be ignored. - - filename = tb.tb_frame.f_code.co_filename - function = tb.tb_frame.f_code.co_name - - filterStr = str(self.ui.filterText.text()) - if filterStr != '': - if isinstance(exc, Exception): - msg = traceback.format_exception_only(type(exc), exc) - elif isinstance(exc, str): - msg = exc - else: - msg = repr(exc) - match = re.search(filterStr, "%s:%s:%s" % (filename, function, msg)) - return match is not None - - ## Go through a list of common exception points we like to ignore: - if excType is GeneratorExit or excType is StopIteration: - return False - if excType is KeyError: - if filename.endswith('python2.7/weakref.py') and function in ('__contains__', 'get'): - return False - if filename.endswith('python2.7/copy.py') and function == '_keep_alive': - return False - if excType is AttributeError: - if filename.endswith('python2.7/collections.py') and function == '__init__': - return False - if filename.endswith('numpy/core/fromnumeric.py') and function in ('all', '_wrapit', 'transpose', 'sum'): - return False - if filename.endswith('numpy/core/arrayprint.py') and function in ('_array2string'): - return False - if filename.endswith('MetaArray.py') and function == '__getattr__': - for name in ('__array_interface__', '__array_struct__', '__array__'): ## numpy looks for these when converting objects to array - if name in exc: - return False - if filename.endswith('flowchart/eq.py'): - return False - if filename.endswith('pyqtgraph/functions.py') and function == 'makeQImage': - return False - if excType is TypeError: - if filename.endswith('numpy/lib/function_base.py') and function == 'iterable': - return False - if excType is ZeroDivisionError: - if filename.endswith('python2.7/traceback.py'): - return False - - return True - diff --git a/pyqtgraph/console/exception_widget.py b/pyqtgraph/console/exception_widget.py new file mode 100644 index 0000000000..73e4990840 --- /dev/null +++ b/pyqtgraph/console/exception_widget.py @@ -0,0 +1,243 @@ +import sys, re, traceback, threading +from .. import exceptionHandling as exceptionHandling +from ..Qt import QtWidgets, QtCore +from ..functions import SignalBlock +from .stackwidget import StackWidget + + +class ExceptionHandlerWidget(QtWidgets.QGroupBox): + sigStackItemClicked = QtCore.Signal(object, object) # self, item + sigStackItemDblClicked = QtCore.Signal(object, object) # self, item + _threadException = QtCore.Signal(object) + + def __init__(self, parent=None): + super().__init__(parent) + self._setupUi() + + self.filterString = '' + self._inSystrace = False + + # send exceptions raised in non-gui threads back to the main thread by signal. + self._threadException.connect(self._threadExceptionHandler) + + def _setupUi(self): + self.setTitle("Exception Handling") + + self.layout = QtWidgets.QGridLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setHorizontalSpacing(2) + self.layout.setVerticalSpacing(0) + + self.clearExceptionBtn = QtWidgets.QPushButton("Clear Stack", self) + self.clearExceptionBtn.setEnabled(False) + self.layout.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) + + self.catchAllExceptionsBtn = QtWidgets.QPushButton("Show All Exceptions", self) + self.catchAllExceptionsBtn.setCheckable(True) + self.layout.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) + + self.catchNextExceptionBtn = QtWidgets.QPushButton("Show Next Exception", self) + self.catchNextExceptionBtn.setCheckable(True) + self.layout.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) + + self.onlyUncaughtCheck = QtWidgets.QCheckBox("Only Uncaught Exceptions", self) + self.onlyUncaughtCheck.setChecked(True) + self.layout.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) + + self.stackTree = StackWidget(self) + self.layout.addWidget(self.stackTree, 2, 0, 1, 7) + + self.runSelectedFrameCheck = QtWidgets.QCheckBox("Run commands in selected stack frame", self) + self.runSelectedFrameCheck.setChecked(True) + self.layout.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) + + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.layout.addItem(spacerItem, 0, 5, 1, 1) + + self.filterLabel = QtWidgets.QLabel("Filter (regex):", self) + self.layout.addWidget(self.filterLabel, 0, 2, 1, 1) + + self.filterText = QtWidgets.QLineEdit(self) + self.layout.addWidget(self.filterText, 0, 3, 1, 1) + + self.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions) + self.catchNextExceptionBtn.toggled.connect(self.catchNextException) + self.clearExceptionBtn.clicked.connect(self.clearExceptionClicked) + self.stackTree.itemClicked.connect(self.stackItemClicked) + self.stackTree.itemDoubleClicked.connect(self.stackItemDblClicked) + self.onlyUncaughtCheck.toggled.connect(self.updateSysTrace) + self.filterText.textChanged.connect(self._filterTextChanged) + + def setStack(self, frame=None): + self.clearExceptionBtn.setEnabled(True) + self.stackTree.setStack(frame) + + def setException(self, exc=None, lastFrame=None): + self.clearExceptionBtn.setEnabled(True) + self.stackTree.setException(exc, lastFrame=lastFrame) + + def selectedFrame(self): + return self.stackTree.selectedFrame() + + def catchAllExceptions(self, catch=True): + """ + If True, the console will catch all unhandled exceptions and display the stack + trace. Each exception caught clears the last. + """ + with SignalBlock(self.catchAllExceptionsBtn.toggled, self.catchAllExceptions): + self.catchAllExceptionsBtn.setChecked(catch) + + if catch: + with SignalBlock(self.catchNextExceptionBtn.toggled, self.catchNextException): + self.catchNextExceptionBtn.setChecked(False) + self.enableExceptionHandling() + else: + self.disableExceptionHandling() + + def catchNextException(self, catch=True): + """ + If True, the console will catch the next unhandled exception and display the stack + trace. + """ + with SignalBlock(self.catchNextExceptionBtn.toggled, self.catchNextException): + self.catchNextExceptionBtn.setChecked(catch) + if catch: + with SignalBlock(self.catchAllExceptionsBtn.toggled, self.catchAllExceptions): + self.catchAllExceptionsBtn.setChecked(False) + self.enableExceptionHandling() + else: + self.disableExceptionHandling() + + def enableExceptionHandling(self): + exceptionHandling.registerCallback(self.exceptionHandler) + self.updateSysTrace() + + def disableExceptionHandling(self): + exceptionHandling.unregisterCallback(self.exceptionHandler) + self.updateSysTrace() + + def clearExceptionClicked(self): + self.stackTree.clear() + self.clearExceptionBtn.setEnabled(False) + + def updateSysTrace(self): + ## Install or uninstall sys.settrace handler + + if not self.catchNextExceptionBtn.isChecked() and not self.catchAllExceptionsBtn.isChecked(): + if sys.gettrace() == self.systrace: + self._disableSysTrace() + return + + if self.onlyUncaughtCheck.isChecked(): + if sys.gettrace() == self.systrace: + self._disableSysTrace() + else: + if sys.gettrace() not in (None, self.systrace): + self.onlyUncaughtCheck.setChecked(False) + raise Exception("sys.settrace is in use (are you using another debugger?); cannot monitor for caught exceptions.") + else: + self._enableSysTrace() + + def _enableSysTrace(self): + # set global trace function + # note: this has no effect on pre-existing frames or threads + # until settrace_all_threads arrives in python 3.12. + sys.settrace(self.systrace) # affects current thread only + threading.settrace(self.systrace) # affects new threads only + if hasattr(threading, 'settrace_all_threads'): + threading.settrace_all_threads(self.systrace) + + def _disableSysTrace(self): + sys.settrace(None) + threading.settrace(None) + if hasattr(threading, 'settrace_all_threads'): + threading.settrace_all_threads(None) + + def exceptionHandler(self, excInfo, lastFrame=None): + if isinstance(excInfo, Exception): + exc = excInfo + else: + exc = excInfo.exc_value + + # exceptions raised in non-gui threads must be sent to the gui thread by signal + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if not isGuiThread: + # note: we are giving the user the ability to modify a frame owned by another thread.. + # expect trouble :) + self._threadException.emit((excInfo, lastFrame)) + return + + if self.catchNextExceptionBtn.isChecked(): + self.catchNextExceptionBtn.setChecked(False) + elif not self.catchAllExceptionsBtn.isChecked(): + return + + self.setException(exc, lastFrame=lastFrame) + + def _threadExceptionHandler(self, args): + self.exceptionHandler(*args) + + def systrace(self, frame, event, arg): + if event != 'exception': + return self.systrace + + if self._inSystrace: + # prevent recursve calling + return self.systrace + self._inSystrace = True + try: + if self.checkException(*arg): + # note: the exception has no __traceback__ at this point! + self.exceptionHandler(arg[1], lastFrame=frame) + except Exception as exc: + print("Exception in systrace:") + traceback.print_exc() + finally: + self.inSystrace = False + return self.systrace + + def checkException(self, excType, exc, tb): + ## Return True if the exception is interesting; False if it should be ignored. + + filename = tb.tb_frame.f_code.co_filename + function = tb.tb_frame.f_code.co_name + + filterStr = self.filterString + if filterStr != '': + if isinstance(exc, Exception): + msg = traceback.format_exception_only(type(exc), exc) + elif isinstance(exc, str): + msg = exc + else: + msg = repr(exc) + match = re.search(filterStr, "%s:%s:%s" % (filename, function, msg)) + return match is not None + + ## Go through a list of common exception points we like to ignore: + if excType is GeneratorExit or excType is StopIteration: + return False + if excType is AttributeError: + if filename.endswith('numpy/core/fromnumeric.py') and function in ('all', '_wrapit', 'transpose', 'sum'): + return False + if filename.endswith('numpy/core/arrayprint.py') and function in ('_array2string'): + return False + if filename.endswith('MetaArray.py') and function == '__getattr__': + for name in ('__array_interface__', '__array_struct__', '__array__'): ## numpy looks for these when converting objects to array + if name in exc: + return False + if filename.endswith('flowchart/eq.py'): + return False + if excType is TypeError: + if filename.endswith('numpy/lib/function_base.py') and function == 'iterable': + return False + + return True + + def stackItemClicked(self, item): + self.sigStackItemClicked.emit(self, item) + + def stackItemDblClicked(self, item): + self.sigStackItemDblClicked.emit(self, item) + + def _filterTextChanged(self, value): + self.filterString = str(value) diff --git a/pyqtgraph/console/repl_widget.py b/pyqtgraph/console/repl_widget.py new file mode 100644 index 0000000000..ec095f689e --- /dev/null +++ b/pyqtgraph/console/repl_widget.py @@ -0,0 +1,219 @@ +import code, sys, traceback +from ..Qt import QtWidgets, QtGui, QtCore +from ..functions import mkBrush +from .CmdInput import CmdInput + + +class ReplWidget(QtWidgets.QWidget): + sigCommandEntered = QtCore.Signal(object, object) # self, command + sigCommandRaisedException = QtCore.Signal(object, object) # self, exc + + def __init__(self, globals, locals, parent=None): + self.globals = globals + self.locals = locals + self._lastCommandRow = None + self._commandBuffer = [] # buffer to hold multiple lines of input + self.stdoutInterceptor = StdoutInterceptor(self.write) + self.ps1 = ">>> " + self.ps2 = "... " + + QtWidgets.QWidget.__init__(self, parent=parent) + + self._setupUi() + + # define text styles + isDark = self.output.palette().color(QtGui.QPalette.ColorRole.Base).value() < 128 + outputBlockFormat = QtGui.QTextBlockFormat() + outputFirstLineBlockFormat = QtGui.QTextBlockFormat(outputBlockFormat) + outputFirstLineBlockFormat.setTopMargin(5) + outputCharFormat = QtGui.QTextCharFormat() + outputCharFormat.setFontWeight(QtGui.QFont.Weight.Normal) + cmdBlockFormat = QtGui.QTextBlockFormat() + cmdBlockFormat.setBackground(mkBrush("#335" if isDark else "#CCF")) + cmdCharFormat = QtGui.QTextCharFormat() + cmdCharFormat.setFontWeight(QtGui.QFont.Weight.Bold) + self.textStyles = { + 'command': (cmdCharFormat, cmdBlockFormat), + 'output': (outputCharFormat, outputBlockFormat), + 'output_first_line': (outputCharFormat, outputFirstLineBlockFormat), + } + + self.input.ps1 = self.ps1 + self.input.ps2 = self.ps2 + + def _setupUi(self): + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) + self.setLayout(self.layout) + + self.output = QtWidgets.QTextEdit(self) + font = QtGui.QFont() + font.setFamily("Courier New") + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) + self.output.setFont(font) + self.output.setReadOnly(True) + self.layout.addWidget(self.output) + + # put input box in a horizontal layout so we can easily place buttons at the end + self.inputWidget = QtWidgets.QWidget(self) + self.layout.addWidget(self.inputWidget) + self.inputLayout = QtWidgets.QHBoxLayout() + self.inputWidget.setLayout(self.inputLayout) + self.inputLayout.setContentsMargins(0, 0, 0, 0) + + self.input = CmdInput(parent=self) + self.inputLayout.addWidget(self.input) + + self.input.sigExecuteCmd.connect(self.runCmd) + + def runCmd(self, cmd): + if '\n' in cmd: + for line in cmd.split('\n'): + self.runCmd(line) + return + + if len(self._commandBuffer) == 0: + self.write(f"{self.ps1}{cmd}\n", style='command') + else: + self.write(f"{self.ps2}{cmd}\n", style='command') + + self.sigCommandEntered.emit(self, cmd) + self._commandBuffer.append(cmd) + + fullcmd = '\n'.join(self._commandBuffer) + try: + cmdCode = code.compile_command(fullcmd) + self.input.setMultiline(False) + except Exception: + # cannot continue processing this command; reset and print exception + self._commandBuffer = [] + self.displayException() + self.input.setMultiline(False) + else: + if cmdCode is None: + # incomplete input; wait for next line + self.input.setMultiline(True) + return + + self._commandBuffer = [] + + # run command + try: + with self.stdoutInterceptor: + exec(cmdCode, self.globals(), self.locals()) + except Exception as exc: + self.displayException() + self.sigCommandRaisedException.emit(self, exc) + + # Add a newline if the output did not + cursor = self.output.textCursor() + if cursor.columnNumber() > 0: + self.write('\n') + + def write(self, strn, style='output', scrollToBottom='auto'): + """Write a string into the console. + + If scrollToBottom is 'auto', then the console is automatically scrolled + to fit the new text only if it was already at the bottom. + """ + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if not isGuiThread: + sys.__stdout__.write(strn) + return + + cursor = self.output.textCursor() + cursor.movePosition(QtGui.QTextCursor.MoveOperation.End) + self.output.setTextCursor(cursor) + + sb = self.output.verticalScrollBar() + scroll = sb.value() + if scrollToBottom == 'auto': + atBottom = scroll == sb.maximum() + scrollToBottom = atBottom + + row = cursor.blockNumber() + if style == 'command': + self._lastCommandRow = row + + if style == 'output' and row == self._lastCommandRow + 1: + # adjust style for first line of output + firstLine, endl, strn = strn.partition('\n') + self._setTextStyle('output_first_line') + self.output.insertPlainText(firstLine + endl) + + if len(strn) > 0: + self._setTextStyle(style) + self.output.insertPlainText(strn) + # return to output style immediately to avoid seeing an extra line of command style + if style != 'output': + self._setTextStyle('output') + + if scrollToBottom: + sb.setValue(sb.maximum()) + else: + sb.setValue(scroll) + + def displayException(self): + """ + Display the current exception and stack. + """ + tb = traceback.format_exc() + lines = [] + indent = 4 + prefix = '' + for l in tb.split('\n'): + lines.append(" "*indent + prefix + l) + self.write('\n'.join(lines)) + + def _setTextStyle(self, style): + charFormat, blockFormat = self.textStyles[style] + cursor = self.output.textCursor() + cursor.setBlockFormat(blockFormat) + self.output.setCurrentCharFormat(charFormat) + + +class StdoutInterceptor: + """Used to temporarily redirect writes meant for sys.stdout and sys.stderr to a new location + """ + def __init__(self, writeFn): + self._orig_stdout = None + self._orig_stderr = None + self.writeFn = writeFn + + def realOutputFiles(self): + """Return the real sys.stdout and stderr (which are sometimes masked while running commands) + """ + return ( + self._orig_stdout or sys.stdout, + self._orig_stderr or sys.stderr + ) + + def print(self, *args): + """Print to real stdout (for debugging) + """ + self.realOutputFiles()[0].write(' '.join(map(str, args)) + "\n") + + def flush(self): + # Need to implement this since we temporarily occlude sys.stdout + pass + + def fileno(self): + # Need to implement this since we temporarily occlude sys.stdout, and someone may be looking for it (faulthandler, for example) + return 1 + + def write(self, strn): + self.writeFn(strn) + + def __enter__(self): + self._orig_stdout = sys.stdout + self._orig_stderr = sys.stderr + sys.stdout = self + sys.stderr = self + + def __exit__(self, exc_type, exc_val, exc_tb): + sys.stdout = self._orig_stdout + sys.stderr = self._orig_stderr + self._orig_stdout = None + self._orig_stderr = None + diff --git a/pyqtgraph/console/stackwidget.py b/pyqtgraph/console/stackwidget.py new file mode 100644 index 0000000000..265b83b9e5 --- /dev/null +++ b/pyqtgraph/console/stackwidget.py @@ -0,0 +1,157 @@ +import sys, traceback +from ..Qt import QtWidgets, QtGui + + +class StackWidget(QtWidgets.QTreeWidget): + def __init__(self, parent=None): + QtWidgets.QTreeWidget.__init__(self, parent) + self.setAlternatingRowColors(True) + self.setHeaderHidden(True) + + def selectedFrame(self): + """Return the currently selected stack frame (or None if there is no selection) + """ + sel = self.selectedItems() + if len(sel) == 0: + return None + else: + return sel[0].frame + + def clear(self): + QtWidgets.QTreeWidget.clear(self) + self.frames = [] + + def setException(self, exc=None, lastFrame=None): + """Display an exception chain with its tracebacks and call stack. + """ + if exc is None: + exc = sys.exc_info()[1] + + self.clear() + + exceptions = exceptionChain(exc) + for ex, cause in exceptions: + stackFrames, tbFrames = stacksFromTraceback(ex.__traceback__, lastFrame=lastFrame) + catchMsg = textItem("Exception caught here") + excStr = ''.join(traceback.format_exception_only(type(ex), ex)).strip() + items = makeItemTree(stackFrames + [catchMsg] + tbFrames, excStr) + self.addTopLevelItem(items[0]) + if cause is not None: + if cause == 'cause': + causeItem = textItem("The above exception was the direct cause of the following exception:") + elif cause == 'context': + causeItem = textItem("During handling of the above exception, another exception occurred:") + self.addTopLevelItem(causeItem) + + items[0].setExpanded(True) + + def setStack(self, frame=None, expand=True, lastFrame=None): + """Display a call stack and exception traceback. + + This allows the user to probe the contents of any frame in the given stack. + + *frame* may either be a Frame instance or None, in which case the current + frame is retrieved from ``sys._getframe()``. + + If *tb* is provided then the frames in the traceback will be appended to + the end of the stack list. If *tb* is None, then sys.exc_info() will + be checked instead. + """ + if frame is None: + frame = sys._getframe().f_back + + self.clear() + + stack = stackFromFrame(frame, lastFrame=lastFrame) + items = makeItemTree(stack, "Call stack") + self.addTopLevelItem(items[0]) + if expand: + items[0].setExpanded(True) + + +def stackFromFrame(frame, lastFrame=None): + """Return (text, stack_frame) for the entire stack ending at *frame* + + If *lastFrame* is given and present in the stack, then the stack is truncated + at that frame. + """ + lines = traceback.format_stack(frame) + frames = [] + while frame is not None: + frames.insert(0, frame) + frame = frame.f_back + if lastFrame is not None and lastFrame in frames: + frames = frames[:frames.index(lastFrame)+1] + + return list(zip(lines[:len(frames)], frames)) + + +def stacksFromTraceback(tb, lastFrame=None): + """Return (text, stack_frame) for a traceback and the stack preceding it + + If *lastFrame* is given and present in the stack, then the stack is truncated + at that frame. + """ + # get stack before tb + stack = stackFromFrame(tb.tb_frame.f_back if tb is not None else lastFrame) + if tb is None: + return stack, [] + + # walk to last frame of traceback + lines = traceback.format_tb(tb) + frames = [] + while True: + frames.append(tb.tb_frame) + if tb.tb_next is None or tb.tb_frame is lastFrame: + break + tb = tb.tb_next + + return stack, list(zip(lines[:len(frames)], frames)) + + +def makeItemTree(stack, title): + topItem = QtWidgets.QTreeWidgetItem([title]) + topItem.frame = None + font = topItem.font(0) + font.setWeight(font.Weight.Bold) + topItem.setFont(0, font) + items = [topItem] + for entry in stack: + if isinstance(entry, QtWidgets.QTreeWidgetItem): + item = entry + else: + text, frame = entry + item = QtWidgets.QTreeWidgetItem([text.rstrip()]) + item.frame = frame + topItem.addChild(item) + items.append(item) + return items + + +def exceptionChain(exc): + """Return a list of (exception, 'cause'|'context') pairs for exceptions + leading up to *exc* + """ + exceptions = [(exc, None)] + while True: + # walk through chained exceptions + if exc.__cause__ is not None: + exc = exc.__cause__ + exceptions.insert(0, (exc, 'cause')) + elif exc.__context__ is not None and exc.__suppress_context__ is False: + exc = exc.__context__ + exceptions.insert(0, (exc, 'context')) + else: + break + return exceptions + + +def textItem(text): + """Return a tree item with no associated stack frame and a darker background color + """ + item = QtWidgets.QTreeWidgetItem([text]) + item.frame = None + item.setBackground(0, QtGui.QBrush(QtGui.QColor(220, 220, 220))) + item.setForeground(0, QtGui.QBrush(QtGui.QColor(0, 0, 0))) + item.setChildIndicatorPolicy(item.ChildIndicatorPolicy.DontShowIndicator) + return item diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui deleted file mode 100644 index ac031c333e..0000000000 --- a/pyqtgraph/console/template.ui +++ /dev/null @@ -1,217 +0,0 @@ - - - Form - - - - 0 - 0 - 739 - 497 - - - - Console - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Vertical - - - - - 4 - - - - - - Courier New - PreferAntialias - - - - true - - - - - - - 6 - - - - - - - - History.. - - - true - - - - - - - Exceptions.. - - - true - - - - - - - - - - - Courier New - PreferAntialias - - - - - - Exception Handling - - - - 0 - - - 0 - - - 2 - - - 0 - - - - - false - - - Clear Stack - - - - - - - Show All Exceptions - - - true - - - - - - - Show Next Exception - - - true - - - - - - - Only Uncaught Exceptions - - - true - - - - - - - true - - - - - - - Run commands in selected stack frame - - - true - - - - - - - Stack Trace - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Filter (regex): - - - - - - - - - - - - - - - CmdInput - QLineEdit -
    .CmdInput
    -
    -
    - - -
    diff --git a/pyqtgraph/console/template_generic.py b/pyqtgraph/console/template_generic.py deleted file mode 100644 index 89302a9630..0000000000 --- a/pyqtgraph/console/template_generic.py +++ /dev/null @@ -1,119 +0,0 @@ -# Form implementation generated from reading ui file '../pyqtgraph/console/template.ui' -# -# Created by: PyQt6 UI code generator 6.1.0 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from ..Qt import QtCore, QtGui, QtWidgets - - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(739, 497) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Orientation.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setSpacing(4) - self.verticalLayout.setObjectName("verticalLayout") - self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) - font = QtGui.QFont() - font.setFamily("Courier New") - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.output.setFont(font) - self.output.setReadOnly(True) - self.output.setObjectName("output") - self.verticalLayout.addWidget(self.output) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setContentsMargins(6, -1, -1, -1) - self.horizontalLayout.setObjectName("horizontalLayout") - self.input = CmdInput(self.layoutWidget) - self.input.setObjectName("input") - self.horizontalLayout.addWidget(self.input) - self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) - self.historyBtn.setCheckable(True) - self.historyBtn.setObjectName("historyBtn") - self.horizontalLayout.addWidget(self.historyBtn) - self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.exceptionBtn.setCheckable(True) - self.exceptionBtn.setObjectName("exceptionBtn") - self.horizontalLayout.addWidget(self.exceptionBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - self.historyList = QtWidgets.QListWidget(self.splitter) - font = QtGui.QFont() - font.setFamily("Courier New") - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.historyList.setFont(font) - self.historyList.setObjectName("historyList") - self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) - self.exceptionGroup.setObjectName("exceptionGroup") - self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) - self.gridLayout_2.setHorizontalSpacing(2) - self.gridLayout_2.setVerticalSpacing(0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) - self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchAllExceptionsBtn.setCheckable(True) - self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") - self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) - self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchNextExceptionBtn.setCheckable(True) - self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") - self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) - self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.onlyUncaughtCheck.setChecked(True) - self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) - self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) - self.exceptionStackList.setAlternatingRowColors(True) - self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) - self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.runSelectedFrameCheck.setChecked(True) - self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) - self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) - self.exceptionInfoLabel.setWordWrap(True) - self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) - self.label = QtWidgets.QLabel(self.exceptionGroup) - self.label.setObjectName("label") - self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) - self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) - self.filterText.setObjectName("filterText") - self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Console")) - self.historyBtn.setText(_translate("Form", "History..")) - self.exceptionBtn.setText(_translate("Form", "Exceptions..")) - self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) - self.clearExceptionBtn.setText(_translate("Form", "Clear Stack")) - self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) - self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) - self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) - self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) - self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace")) - self.label.setText(_translate("Form", "Filter (regex):")) -from .CmdInput import CmdInput diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index a080302b83..5f9878f79f 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -1192,19 +1192,7 @@ def run(self): if id == threading.current_thread().ident: continue - # try to determine a thread name - try: - name = threading._active.get(id, None) - except: - name = None - if name is None: - try: - # QThread._names must be manually set by thread creators. - name = QtCore.QThread._names.get(id) - except: - name = None - if name is None: - name = "???" + name = threadName() printFile.write("<< thread %d \"%s\" >>\n" % (id, name)) tb = str(''.join(traceback.format_stack(frame))) @@ -1217,6 +1205,46 @@ def run(self): time.sleep(self.interval) +def threadName(threadId=None): + """Return a string name for a thread id. + + If *threadId* is None, then the current thread's id is used. + + This attempts to look up thread names either from `threading._active`, or from + QThread._names. However, note that the latter does not exist by default; rather + you must manually add id:name pairs to a dictionary there:: + + # for python threads: + t1 = threading.Thread(name="mythread") + + # for Qt threads: + class Thread(Qt.QThread): + def __init__(self, name): + self._threadname = name + if not hasattr(Qt.QThread, '_names'): + Qt.QThread._names = {} + Qt.QThread.__init__(self, *args, **kwds) + def run(self): + Qt.QThread._names[threading.current_thread().ident] = self._threadname + """ + if threadId is None: + threadId = threading.current_thread().ident + + try: + name = threading._active.get(threadId, None) + except Exception: + name = None + if name is None: + try: + # QThread._names must be manually set by thread creators. + name = QtCore.QThread._names.get(threadId) + except Exception: + name = None + if name is None: + name = "???" + return name + + class ThreadColor(object): """ Wrapper on stdout/stderr that colors text by the current thread ID. diff --git a/pyqtgraph/examples/ConsoleWidget.py b/pyqtgraph/examples/ConsoleWidget.py index 9b3875dddf..c90847c6ec 100644 --- a/pyqtgraph/examples/ConsoleWidget.py +++ b/pyqtgraph/examples/ConsoleWidget.py @@ -24,6 +24,7 @@ Go, play. """ c = pyqtgraph.console.ConsoleWidget(namespace=namespace, text=text) +c.resize(800, 400) c.show() c.setWindowTitle('pyqtgraph example: ConsoleWidget') diff --git a/pyqtgraph/examples/ExampleApp.py b/pyqtgraph/examples/ExampleApp.py index 0385ea911a..5151eadcbd 100644 --- a/pyqtgraph/examples/ExampleApp.py +++ b/pyqtgraph/examples/ExampleApp.py @@ -510,7 +510,9 @@ def currentFile(self): return os.path.join(path, item.file) return None - def loadFile(self, edited=False): + def loadFile(self, *, edited=False): + # make *edited* keyword-only so it is not confused for extra arguments + # sent by ui signals qtLib = self.ui.qtLibCombo.currentText() env = dict(os.environ, PYQTGRAPH_QT_LIB=qtLib) example_path = os.path.abspath(os.path.dirname(__file__)) diff --git a/pyqtgraph/examples/console_exception_inspection.py b/pyqtgraph/examples/console_exception_inspection.py new file mode 100644 index 0000000000..aac62f419e --- /dev/null +++ b/pyqtgraph/examples/console_exception_inspection.py @@ -0,0 +1,199 @@ +""" +Using ConsoleWidget to interactively inspect exception backtraces + + +TODO + - fix uncaught exceptions in threads (python 3.12) + - allow using qtconsole + - provide thread info for stacks + - add thread browser? + - add object browser? + - clicking on a stack frame populates list of locals? + - optional merged exception stacks + +""" + +import sys +import queue +import functools +import threading +import pyqtgraph as pg +import pyqtgraph.console +from pyqtgraph.Qt import QtWidgets +from pyqtgraph.debug import threadName + + +def raiseException(): + """Raise an exception + """ + x = "inside raiseException()" + raise Exception(f"Raised an exception {x} in {threadName()}") + + +def raiseNested(): + """Raise an exception while handling another + """ + x = "inside raiseNested()" + try: + raiseException() + except Exception: + raise Exception(f"Raised during exception handling {x} in {threadName()}") + + +def raiseFrom(): + """Raise an exception from another + """ + x = "inside raiseFrom()" + try: + raiseException() + except Exception as exc: + raise Exception(f"Raised-from during exception handling {x} in {threadName()}") from exc + + +def raiseCaughtException(): + """Raise and catch an exception + """ + x = "inside raiseCaughtException()" + try: + raise Exception(f"Raised an exception {x} in {threadName()}") + except Exception: + print(f"Raised and caught exception {x} in {threadName()} trace: {sys._getframe().f_trace}") + + +def captureStack(): + """Inspect the curent call stack + """ + x = "inside captureStack()" + global console + console.setStack() + return x + + +# Background thread for running functions +threadRunQueue = queue.Queue() +def threadRunner(): + global threadRunQueue + # This is necessary to allow installing trace functions in the thread later on + sys.settrace(lambda *args: None) + while True: + func, args = threadRunQueue.get() + try: + print(f"running {func} from thread, trace: {sys._getframe().f_trace}") + func(*args) + except Exception: + sys.excepthook(*sys.exc_info()) +thread = threading.Thread(target=threadRunner, name="background_thread", daemon=True) +thread.start() + + +# functions used to generate a stack a few items deep +def runInStack(func): + x = "inside runInStack(func)" + runInStack2(func) + return x + +def runInStack2(func): + x = "inside runInStack2(func)" + runInStack3(func) + return x + +def runInStack3(func): + x = "inside runInStack3(func)" + runInStack4(func) + return x + +def runInStack4(func): + x = "inside runInStack4(func)" + func() + return x + + +class SignalEmitter(pg.QtCore.QObject): + signal = pg.QtCore.Signal(object, object) + def __init__(self, queued): + pg.QtCore.QObject.__init__(self) + if queued: + self.signal.connect(self.run, pg.QtCore.Qt.ConnectionType.QueuedConnection) + else: + self.signal.connect(self.run) + def run(self, func, args): + func(*args) +signalEmitter = SignalEmitter(queued=False) +queuedSignalEmitter = SignalEmitter(queued=True) + + + +def runFunc(func): + if signalCheck.isChecked(): + if queuedSignalCheck.isChecked(): + func = functools.partial(queuedSignalEmitter.signal.emit, runInStack, (func,)) + else: + func = functools.partial(signalEmitter.signal.emit, runInStack, (func,)) + + if threadCheck.isChecked(): + threadRunQueue.put((runInStack, (func,))) + else: + runInStack(func) + + + +funcs = [ + raiseException, + raiseNested, + raiseFrom, + raiseCaughtException, + captureStack, +] + +app = pg.mkQApp() + +win = pg.QtWidgets.QSplitter(pg.QtCore.Qt.Orientation.Horizontal) + +ctrl = QtWidgets.QWidget() +ctrlLayout = QtWidgets.QVBoxLayout() +ctrl.setLayout(ctrlLayout) +win.addWidget(ctrl) + +btns = [] +for func in funcs: + btn = QtWidgets.QPushButton(func.__doc__) + btn.clicked.connect(functools.partial(runFunc, func)) + btns.append(btn) + ctrlLayout.addWidget(btn) + +threadCheck = QtWidgets.QCheckBox('Run in thread') +ctrlLayout.addWidget(threadCheck) + +signalCheck = QtWidgets.QCheckBox('Run from Qt signal') +ctrlLayout.addWidget(signalCheck) + +queuedSignalCheck = QtWidgets.QCheckBox('Use queued Qt signal') +ctrlLayout.addWidget(queuedSignalCheck) + +ctrlLayout.addStretch() + +console = pyqtgraph.console.ConsoleWidget(text=""" +Use ConsoleWidget to interactively inspect exception tracebacks and call stacks! + +- Enable "Show next exception" and the next unhandled exception will be displayed below. +- Click any of the buttons to the left to generate an exception. +- When an exception traceback is shown, you can select any of the stack frames and then run commands from that context, + allowing you to inspect variables along the stack. (hint: most of the functions called by the buttons to the left + have a variable named "x" in their local scope) +- Note that this is not like a typical debugger--the program is not paused when an exception is caught; we simply keep + a reference to the stack frames and continue on. +- By default, we only catch unhandled exceptions. If you need to inspect a handled exception (one that is caught by + a try:except block), then uncheck the "Only handled exceptions" box. Note, however that this incurs a performance + penalty and will interfere with other debuggers. + + +""") +console.catchNextException() +win.addWidget(console) + +win.resize(1400, 800) +win.setSizes([300, 1100]) +win.show() + +if __name__ == '__main__': + pg.exec() diff --git a/pyqtgraph/examples/utils.py b/pyqtgraph/examples/utils.py index 1a624e7203..e8d5dc090b 100644 --- a/pyqtgraph/examples/utils.py +++ b/pyqtgraph/examples/utils.py @@ -5,20 +5,21 @@ examples_ = OrderedDict([ ('Command-line usage', 'CLIexample.py'), ('Basic Plotting', Namespace(filename='Plotting.py', recommended=True)), + ('ImageView', Namespace(filename='ImageView.py', recommended=True)), + ('ParameterTree', Namespace(filename='parametertree.py', recommended=True)), ('Plotting Datasets', 'MultiDataPlot.py'), - ('ImageView', 'ImageView.py'), - ('ParameterTree', 'parametertree.py'), ('Parameter-Function Interaction', 'InteractiveParameter.py'), ('Crosshair / Mouse interaction', 'crosshair.py'), ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), ('Timestamps on x axis', 'DateAxisItem.py'), - ('Image Analysis', 'imageAnalysis.py'), + ('Image Analysis', Namespace(filename='imageAnalysis.py', recommended=True)), ('Matrix Display', 'MatrixDisplayExample.py'), ('ViewBox Features', Namespace(filename='ViewBoxFeatures.py', recommended=True)), ('Dock widgets', 'dockarea.py'), - ('Rich Jupyter Console', Namespace(filename='jupyter_console_example.py', recommended=True)), ('Console', 'ConsoleWidget.py'), + ('Console - Exception inspection', 'console_exception_inspection.py'), + ('Rich Jupyter Console', 'jupyter_console_example.py'), ('Histograms', 'histogram.py'), ('Beeswarm plot', 'beeswarm.py'), ('Symbols', 'Symbols.py'), From 7642e18e1d1ff569b1ef6db844c09280fc5fcddf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 08:13:21 -0700 Subject: [PATCH 215/306] Bump pytest from 7.2.2 to 7.3.0 in /.github/workflows/etc (#2682) Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.2 to 7.3.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.2...7.3.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 0a868edc0a..61cad8f935 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -18,6 +18,6 @@ matplotlib==3.7.1 h5py==3.8.0 # testing -pytest==7.2.2 +pytest==7.3.0 pytest-xdist==3.2.1 pytest-xvfb==2.0.0; sys_platform == 'linux' From e5e9f9611af22ba9a3e66acc055f9a40d32736de Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Mon, 10 Apr 2023 20:47:56 -0500 Subject: [PATCH 216/306] Avoid changing background colors of text and rows for group parameter items --- pyqtgraph/parametertree/parameterTypes/basetypes.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/basetypes.py b/pyqtgraph/parametertree/parameterTypes/basetypes.py index 02af608f99..ba9a27c921 100644 --- a/pyqtgraph/parametertree/parameterTypes/basetypes.py +++ b/pyqtgraph/parametertree/parameterTypes/basetypes.py @@ -337,22 +337,11 @@ def updateDepth(self, depth): """ Change set the item font to bold and increase the font size on outermost groups. """ - app = mkQApp() - palette = app.palette() - background = palette.base().color() - h, s, l, a = background.getHslF() - lightness = 0.5 + (l - 0.5) * .8 - altBackground = QtGui.QColor.fromHslF(h, s, lightness, a) - for c in [0, 1]: font = self.font(c) font.setBold(True) if depth == 0: font.setPointSize(self.pointSize() + 1) - self.setBackground(c, background) - else: - self.setBackground(c, altBackground) - self.setForeground(c, palette.text().color()) self.setFont(c, font) self.titleChanged() # sets the size hint for column 0 which is based on the new font From 8b7a4f23e7de5a02a61b38630336dc2c53ec2c47 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 13 Apr 2023 14:16:16 -0700 Subject: [PATCH 217/306] Allow macOS to have fudge factor in test_polyROI --- tests/graphicsItems/test_ROI.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/graphicsItems/test_ROI.py b/tests/graphicsItems/test_ROI.py index 796359be0a..89a4f82bcb 100644 --- a/tests/graphicsItems/test_ROI.py +++ b/tests/graphicsItems/test_ROI.py @@ -3,6 +3,7 @@ import numpy as np import pytest +from packaging.version import Version, parse import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui, QtTest @@ -326,8 +327,12 @@ def test_PolyLineROI(): # call setPoints r.setPoints(initState['points']) - assertImageApproved(plt, 'roi/polylineroi/' + name + '_setpoints', - 'Reset points to initial state.') + assertImageApproved( + plt, + f'roi/polylineroi/{name}_setpoints', + 'Reset points to initial state.', + pxCount=1 if platform.system() == "Darwin" and parse(platform.mac_ver()[0]) >= Version("13.0") else 0 + ) assert len(r.getState()['points']) == 3 # call setState From 35a897bb51d360583bf73363ef046f563d95ee53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Apr 2023 14:25:17 -0700 Subject: [PATCH 218/306] Bump sphinx-design from 0.3.0 to 0.4.1 in /doc (#2686) Bumps [sphinx-design](https://github.com/executablebooks/sphinx-design) from 0.3.0 to 0.4.1. - [Release notes](https://github.com/executablebooks/sphinx-design/releases) - [Changelog](https://github.com/executablebooks/sphinx-design/blob/main/CHANGELOG.md) - [Commits](https://github.com/executablebooks/sphinx-design/compare/v0.3.0...v0.4.1) --- updated-dependencies: - dependency-name: sphinx-design dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index aa2ab42d04..fe92a32d3b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,7 @@ PyQt6==6.4.2 sphinx==5.3.0 pydata-sphinx-theme==0.13.3 -sphinx-design==0.3.0 +sphinx-design==0.4.1 sphinxcontrib-images==0.9.4 sphinx-favicon==1.0.1 sphinx-qt-documentation From 384724ab9c0a0b217ae9cd15b841b361bd247ad3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:26:10 +0000 Subject: [PATCH 219/306] Bump sphinx from 5.3.0 to 6.1.3 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.3.0 to 6.1.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.3.0...v6.1.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index fe92a32d3b..0d01348d5c 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.4.2 -sphinx==5.3.0 +sphinx==6.1.3 pydata-sphinx-theme==0.13.3 sphinx-design==0.4.1 sphinxcontrib-images==0.9.4 From 198f8d84b32c3c7d42ac568c3b444d229408211e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:35:31 +0000 Subject: [PATCH 220/306] Bump pyqt6 from 6.4.2 to 6.5.0 in /doc Bumps [pyqt6](https://www.riverbankcomputing.com/software/pyqt/) from 6.4.2 to 6.5.0. --- updated-dependencies: - dependency-name: pyqt6 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 0d01348d5c..14f3014c74 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -PyQt6==6.4.2 +PyQt6==6.5.0 sphinx==6.1.3 pydata-sphinx-theme==0.13.3 sphinx-design==0.4.1 From 33f8da2094999a50994c21f46d821b7c6ea3899c Mon Sep 17 00:00:00 2001 From: koenstrien <66589602+koenstrien@users.noreply.github.com> Date: Fri, 14 Apr 2023 23:06:37 +0200 Subject: [PATCH 221/306] Only apply nan mask workaround for cp version below 10.0. (#2689) --- pyqtgraph/functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index ffa50f7195..836f806d86 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1531,8 +1531,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, maskNans=Tr # apply nan mask through alpha channel if nanMask is not None: alpha = True - # Workaround for https://github.com/cupy/cupy/issues/4693 - if xp == cp: + # Workaround for https://github.com/cupy/cupy/issues/4693, fixed in cupy 10.0.0 + if xp == cp and tuple(map(int, cp.__version__.split("."))) < (10, 0): imgData[nanMask, :, dst_order[3]] = 0 else: imgData[nanMask, dst_order[3]] = 0 From af563a47227bc7e13514f8c47b3889602693ea88 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 14 Apr 2023 14:23:01 -0700 Subject: [PATCH 222/306] Prep for 0.13.3 release (#2692) * Update README, remove test matrix * Update Changelog for 0.13.3 release * Set version to release version --- CHANGELOG | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 22 +--------------------- pyqtgraph/__init__.py | 2 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 06ca63859f..d323da6031 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,41 @@ +pyqtgraph-0.13.3 + +## What's Changed + +### Highlights + +* PySide6 6.5 Compatability + +### Bug Fixes + +* Return float values from QColor in getByIndex by @nickdimitroff in https://github.com/pyqtgraph/pyqtgraph/pull/2648 +* GLViewWidget: don't assume mouse tracking is disabled by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2653 +* Tolerate an empty BarGraphItem by @jmakovicka in https://github.com/pyqtgraph/pyqtgraph/pull/2658 +* Only apply nan mask workaround for cp version below 10.0. by @koenstrien in https://github.com/pyqtgraph/pyqtgraph/pull/2689 + +### Misc + +* unify ndarray_from_qpolygonf implementation by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2654 +* re-enable tests taking gui thread on PySide6 by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2657 +* inherit GraphicsWidgetAnchor on the left-hand-side by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2662 +* Prepare support for PySide6 drawLines and friends by @pijyoi in https://github.com/pyqtgraph/pyqtgraph/pull/2596 +* Avoid changing background colors of text and rows for group parameter… by @ntjess in https://github.com/pyqtgraph/pyqtgraph/pull/2683 + +### Testing + +* Allow macOS to have fudge factor in test_polyROI by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2687 + +### Docs +* Update pydata-sphinx-theme and fix warnings by @j9ac9k in https://github.com/pyqtgraph/pyqtgraph/pull/2643 +* Bump sphinx-design from 0.3.0 to 0.4.1 in /doc by @dependabot in https://github.com/pyqtgraph/pyqtgraph/pull/2686 +* Bump sphinx from 5.3.0 to 6.1.3 in /doc by @dependabot in https://github.com/pyqtgraph/pyqtgraph/pull/2585 + +## New Contributors +* @nickdimitroff made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2648 +* @koenstrien made their first contribution in https://github.com/pyqtgraph/pyqtgraph/pull/2689 + +**Full Changelog**: https://github.com/pyqtgraph/pyqtgraph/compare/pyqtgraph-0.13.2...pyqtgraph-0.13.3 + pyqtgraph-0.13.2 ## What's Changed diff --git a/README.md b/README.md index 40398205b7..edbc32f61f 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,10 @@ PyQtGraph [![Build Status](https://github.com/pyqtgraph/pyqtgraph/workflows/main/badge.svg)](https://github.com/pyqtgraph/pyqtgraph/actions/?query=workflow%3Amain) [![CodeQL Status](https://github.com/pyqtgraph/pyqtgraph/workflows/codeql/badge.svg)](https://github.com/pyqtgraph/pyqtgraph/actions/?query=workflow%3Acodeql) [![Documentation Status](https://readthedocs.org/projects/pyqtgraph/badge/?version=latest)](https://pyqtgraph.readthedocs.io/en/latest/?badge=latest) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/pyqtgraph/pyqtgraph.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/pyqtgraph/pyqtgraph/alerts/) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/pyqtgraph/pyqtgraph.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/pyqtgraph/pyqtgraph/context:python) [![Discord](https://img.shields.io/discord/946624673200893953.svg?label=PyQtGraph&logo=discord)](https://discord.gg/ufTVNNreAZ) A pure-Python graphics library for PyQt5/PyQt6/PySide2/PySide6 -Copyright 2022 PyQtGraph developers +Copyright 2023 PyQtGraph developers @@ -67,24 +65,6 @@ Through 3rd part libraries, additional functionality may be added to PyQtGraph, [`cupy`]: https://docs.cupy.dev/en/stable/install.html [`jupyter_rfb`]: https://github.com/vispy/jupyter_rfb -Qt Bindings Test Matrix ------------------------ - -The following table represents the python environments we test in our CI system. Our CI system uses Ubuntu 20.04, Windows Server 2019, and macOS 10.15 base images. - -| Qt-Bindings |Python 3.8 | Python 3.9 | Python 3.10 | -| :------------- |:---------------------: | :---------------------: | :---------------------: | -| PySide2-5.15 | :white_check_mark: | :white_check_mark: | | -| PyQt5-5.15 | :white_check_mark: | :white_check_mark: | | -| PySide6-6.2 | | :white_check_mark: | | -| PyQt6-6.2 | | :white_check_mark: | | -| PySide6-6.3 | | | :white_check_mark: | -| PyQt6-6.3 | | | :white_check_mark: | - -* :x: - Not compatible -* :white_check_mark: - Tested -* No icon means supported configuration but we do not explicitely test it - Support ------- diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 29aee874e0..eccbc5406f 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.3.dev0' +__version__ = '0.13.3' ### import all the goodies and add some helper functions for easy CLI use From 4f642c9bf1abb3e66d89dbf5ef441e49a5ededf1 Mon Sep 17 00:00:00 2001 From: ntjess Date: Fri, 14 Apr 2023 23:45:57 -0500 Subject: [PATCH 223/306] bump to dev version (#2693) --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index eccbc5406f..112cbe4c9c 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -3,7 +3,7 @@ www.pyqtgraph.org """ -__version__ = '0.13.3' +__version__ = '0.13.4.dev0' ### import all the goodies and add some helper functions for easy CLI use From 6186e24b574f39e51c593d38648eaec8458d083a Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 15 Apr 2023 16:12:36 +0800 Subject: [PATCH 224/306] WidgetGroup: avoid lambda by using self.sender() --- pyqtgraph/WidgetGroup.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py index 51677cc41e..8a1f0f3755 100644 --- a/pyqtgraph/WidgetGroup.py +++ b/pyqtgraph/WidgetGroup.py @@ -172,7 +172,7 @@ def addWidget(self, w, name=None, scale=None): if signal is not None: if inspect.isfunction(signal) or inspect.ismethod(signal): signal = signal(w) - signal.connect(self.mkChangeCallback(w)) + signal.connect(self.widgetChanged) else: self.uncachedWidgets[w] = None @@ -217,10 +217,8 @@ def setScale(self, widget, scale): self.scales[widget] = scale self.setWidget(widget, val) - def mkChangeCallback(self, w): - return lambda *args: self.widgetChanged(w, *args) - - def widgetChanged(self, w, *args): + def widgetChanged(self, *args): + w = self.sender() n = self.widgetList[w] v1 = self.cache[n] v2 = self.readWidget(w) From 25b87078bbaba268a12977dd0d260d5177bca23b Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 15 Apr 2023 21:17:06 +0800 Subject: [PATCH 225/306] drop to Python 3.10 for conda CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 95e02cd68e..486c9acad3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -169,7 +169,7 @@ jobs: miniforge-variant: Mambaforge environment-file: ${{ matrix.environment-file }} auto-update-conda: false - python-version: "3.11" + python-version: "3.10" use-mamba: true - name: "Install Test Framework" run: pip install pytest pytest-xdist From b416dfdbae2f8b7cb08940f6177fd2af5ea45c0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:05:14 -0700 Subject: [PATCH 226/306] Bump pytest from 7.3.0 to 7.3.1 in /.github/workflows/etc (#2696) --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 61cad8f935..00220c8b30 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -18,6 +18,6 @@ matplotlib==3.7.1 h5py==3.8.0 # testing -pytest==7.3.0 +pytest==7.3.1 pytest-xdist==3.2.1 pytest-xvfb==2.0.0; sys_platform == 'linux' From 9ef200673a2e1ff17413da33a2d02fdd44ab3a5a Mon Sep 17 00:00:00 2001 From: pijyoi Date: Wed, 19 Apr 2023 21:10:04 +0800 Subject: [PATCH 227/306] fix mouse pos jumps when mouse tracking is off (#2698) --- pyqtgraph/opengl/GLViewWidget.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 56d3fbf2e0..a0715748df 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -417,7 +417,11 @@ def pixelSize(self, pos): dist = (pos-cam).length() xDist = dist * 2. * tan(0.5 * radians(self.opts['fov'])) return xDist / self.width() - + + def mousePressEvent(self, ev): + lpos = ev.position() if hasattr(ev, 'position') else ev.localPos() + self.mousePos = lpos + def mouseMoveEvent(self, ev): lpos = ev.position() if hasattr(ev, 'position') else ev.localPos() if not hasattr(self, 'mousePos'): From efc762031e0274f0bee47f89f1dd5f884a06f2be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 07:06:19 -0700 Subject: [PATCH 228/306] Bump sphinx from 6.1.3 to 6.2.1 in /doc (#2705) --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 14f3014c74..172d12dba0 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.5.0 -sphinx==6.1.3 +sphinx==6.2.1 pydata-sphinx-theme==0.13.3 sphinx-design==0.4.1 sphinxcontrib-images==0.9.4 From 347ebdec602f2acd3887ebedba1afac2d78a594a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Bissinger?= Date: Tue, 2 May 2023 08:35:52 +0200 Subject: [PATCH 229/306] Don't draw InfiniteLine anti-aliased if vertical or horizontal --- pyqtgraph/graphicsItems/InfiniteLine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index e3697eafdd..3e73e870d6 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -337,7 +337,8 @@ def boundingRect(self): return self._boundingRect def paint(self, p, *args): - p.setRenderHint(p.RenderHint.Antialiasing) + if self.angle % 180 not in (0, 90): + p.setRenderHint(p.RenderHint.Antialiasing) left, right = self._endPoints pen = self.currentPen From e445c0cc3add83619ef83eab94d4c5b491dd82d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 14:04:51 +0000 Subject: [PATCH 230/306] Bump numba from 0.56.4 to 0.57.0 in /.github/workflows/etc Bumps [numba](https://github.com/numba/numba) from 0.56.4 to 0.57.0. - [Release notes](https://github.com/numba/numba/releases) - [Commits](https://github.com/numba/numba/compare/0.56.4...0.57.0) --- updated-dependencies: - dependency-name: numba dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 00220c8b30..3e2872c27f 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -8,7 +8,7 @@ numpy~=1.20.0; python_version == '3.8' scipy==1.10.1 # optional high performance paths -numba==0.56.4; python_version == '3.9' +numba==0.57.0; python_version == '3.9' # optional 3D pyopengl==3.1.6 From 3f628a8e0890814cf35190659b6219d0ab71bf19 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 11 May 2023 20:05:16 +0800 Subject: [PATCH 231/306] bypass finite check if already known --- pyqtgraph/graphicsItems/PlotDataItem.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 94c19a6baa..72a2acd67f 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -978,7 +978,14 @@ def _getDisplayDataset(self): if self.opts['autoDownsample']: # this option presumes that x-values have uniform spacing - finite_x = x[np.isfinite(x)] # ignore infinite and nan values + if containsNonfinite is False: + finite_x = x + else: # True or None + # True: (we checked and found non-finites) + # None: (we haven't performed a check for non-finites yet) + # we also land here if x is finite but y has non-finites + finite_x = x[np.isfinite(x)] # ignore infinite and nan values + if view_range is not None and len(finite_x) > 1: dx = float(finite_x[-1]-finite_x[0]) / (len(finite_x)-1) if dx != 0.0: From e36566378350a8a6fe31574f98a76b2f99f3cc15 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 11 May 2023 20:09:32 +0800 Subject: [PATCH 232/306] use bisect_left instead of np.searchsorted np.searchsorted performs poorly when the array.dtype does not match the type of the value (float) being searched. --- pyqtgraph/graphicsItems/PlotDataItem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 72a2acd67f..e624c25813 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1,5 +1,6 @@ import math import warnings +import bisect import numpy as np @@ -1005,12 +1006,14 @@ def _getDisplayDataset(self): # find first in-view value (left edge) and first out-of-view value (right edge) # since we want the curve to go to the edge of the screen, we need to preserve # one down-sampled point on the left and one of the right, so we extend the interval - x0 = np.searchsorted(x, view_range.left()) - ds + x0 = bisect.bisect_left(x, view_range.left()) - ds x0 = fn.clip_scalar(x0, 0, len(x)) # workaround + # x0 = np.searchsorted(x, view_range.left()) - ds # x0 = np.clip(x0, 0, len(x)) - x1 = np.searchsorted(x, view_range.right()) + ds + x1 = bisect.bisect_left(x, view_range.right()) + ds x1 = fn.clip_scalar(x1, x0, len(x)) + # x1 = np.searchsorted(x, view_range.right()) + ds # x1 = np.clip(x1, 0, len(x)) x = x[x0:x1] y = y[x0:x1] From ca15c87346212b0be76a0795f9b4d1d32d86106b Mon Sep 17 00:00:00 2001 From: adrianlubitz Date: Thu, 11 May 2023 21:21:07 +0200 Subject: [PATCH 233/306] TypeError when using multiple pens for different lines (#2704) * Update GraphItem.py comparison with None will always cause ``` if np.any(pen != lastPen): TypeError: Cannot compare structured or void to non-void arrays. ``` This does not really follow DRY principle but was the best solution I could come up with using static types in structured array * fix for multiple different pens --- pyqtgraph/graphicsItems/GraphItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index 65793dc031..a01098a74b 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -111,7 +111,7 @@ def generatePicture(self): lastPen = None for i in range(pts.shape[0]): pen = self.pen[i] - if np.any(pen != lastPen): + if lastPen is None or np.any(pen != lastPen): lastPen = pen if pen.dtype.fields is None: p.setPen(fn.mkPen(color=(pen[0], pen[1], pen[2], pen[3]), width=1)) From 2767d33fbedbead816f9bfbff2985bc51d30932e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 13 May 2023 09:49:31 +0800 Subject: [PATCH 234/306] keep track of respective finiteness of x and y integer types are always finite, no need to check for backwards compatibility, "containsNonfinite" is converted to a property. --- pyqtgraph/graphicsItems/PlotDataItem.py | 92 +++++++++++++------------ 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index e624c25813..8f0b1e44fd 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -28,7 +28,7 @@ class PlotDataset(object): For internal use in :class:`PlotDataItem `, this class should not be instantiated when no data is available. """ - def __init__(self, x, y): + def __init__(self, x, y, xAllFinite=None, yAllFinite=None): """ Parameters ---------- @@ -40,9 +40,22 @@ def __init__(self, x, y): super().__init__() self.x = x self.y = y + self.xAllFinite = xAllFinite + self.yAllFinite = yAllFinite self._dataRect = None - self.containsNonfinite = None - + + if isinstance(x, np.ndarray) and x.dtype.kind in 'iu': + self.xAllFinite = True + if isinstance(y, np.ndarray) and y.dtype.kind in 'iu': + self.yAllFinite = True + + @property + def containsNonfinite(self): + if self.xAllFinite is None or self.yAllFinite is None: + # don't know for sure yet + return None + return not (self.xAllFinite and self.yAllFinite) + def _updateDataRect(self): """ Finds bounds of plotable data and stores them as ``dataset._dataRect``, @@ -50,31 +63,27 @@ def _updateDataRect(self): """ if self.y is None or self.x is None: return None - if self.containsNonfinite is False: # all points are directly usable. - ymin = np.min( self.y ) # find minimum of all values - ymax = np.max( self.y ) # find maximum of all values - xmin = np.min( self.x ) # find minimum of all values - xmax = np.max( self.x ) # find maximum of all values - else: # This may contain NaN values and infinites. - selection = np.isfinite(self.y) # We are looking for the bounds of the plottable data set. Infinite and Nan are ignored. - all_y_are_finite = selection.all() # False if all values are finite, True if there are any non-finites - try: - ymin = np.min( self.y[selection] ) # find minimum of all finite values - ymax = np.max( self.y[selection] ) # find maximum of all finite values - except ValueError: # is raised when there are no finite values - ymin = np.nan - ymax = np.nan - selection = np.isfinite(self.x) # We are looking for the bounds of the plottable data set. Infinite and Nan are ignored. - all_x_are_finite = selection.all() # False if all values are finite, True if there are any non-finites - try: - xmin = np.min( self.x[selection] ) # find minimum of all finite values - xmax = np.max( self.x[selection] ) # find maximum of all finite values - except ValueError: # is raised when there are no finite values - xmin = np.nan - xmax = np.nan - self.containsNonfinite = not (all_x_are_finite and all_y_are_finite) # This always yields a definite True/False answer + xmin, xmax, self.xAllFinite = self._getArrayBounds(self.x, self.xAllFinite) + ymin, ymax, self.yAllFinite = self._getArrayBounds(self.y, self.yAllFinite) self._dataRect = QtCore.QRectF( QtCore.QPointF(xmin,ymin), QtCore.QPointF(xmax,ymax) ) - + + def _getArrayBounds(self, arr, all_finite): + # here all_finite could be [None, False, True] + if not all_finite: # This may contain NaN values and infinites. + selection = np.isfinite(arr) # We are looking for the bounds of the plottable data set. Infinite and Nan are ignored. + all_finite = selection.all() # True if all values are finite, False if there are any non-finites + if not all_finite: + arr = arr[selection] + # here all_finite could be [False, True] + + try: + amin = np.min( arr ) # find minimum of all finite values + amax = np.max( arr ) # find maximum of all finite values + except ValueError: # is raised when there are no finite values + amin = np.nan + amax = np.nan + return amin, amax, all_finite + def dataRect(self): """ Returns a bounding rectangle (as :class:`QtCore.QRectF`) for the finite subset of data. @@ -96,7 +105,6 @@ def applyLogMapping(self, logMode): logmode: tuple or list of two bool A `True` value requests log-scale mapping for the x and y axis (in this order). """ - all_x_finite = False if logMode[0]: with warnings.catch_warnings(): warnings.simplefilter("ignore", RuntimeWarning) @@ -104,10 +112,11 @@ def applyLogMapping(self, logMode): nonfinites = ~np.isfinite( self.x ) if nonfinites.any(): self.x[nonfinites] = np.nan # set all non-finite values to NaN - self.containsNonfinite = True + all_x_finite = False else: all_x_finite = True - all_y_finite = False + self.xAllFinite = all_x_finite + if logMode[1]: with warnings.catch_warnings(): warnings.simplefilter("ignore", RuntimeWarning) @@ -115,11 +124,10 @@ def applyLogMapping(self, logMode): nonfinites = ~np.isfinite( self.y ) if nonfinites.any(): self.y[nonfinites] = np.nan # set all non-finite values to NaN - self.containsNonfinite = True + all_y_finite = False else: all_y_finite = True - if all_x_finite and all_y_finite: - self.containsNonfinite = False # mark as False only if both axes were checked. + self.yAllFinite = all_y_finite class PlotDataItem(GraphicsObject): """ @@ -951,8 +959,7 @@ def _getDisplayDataset(self): x = self._dataset.y[:-1] y = np.diff(self._dataset.y)/np.diff(self._dataset.x) - dataset = PlotDataset(x,y) - dataset.containsNonfinite = self._dataset.containsNonfinite + dataset = PlotDataset(x, y, self._dataset.xAllFinite, self._dataset.yAllFinite) if True in self.opts['logMode']: dataset.applyLogMapping( self.opts['logMode'] ) # Apply log scaling for x and/or y axis @@ -962,7 +969,8 @@ def _getDisplayDataset(self): # apply processing that affects the on-screen display of data: x = self._datasetMapped.x y = self._datasetMapped.y - containsNonfinite = self._datasetMapped.containsNonfinite + xAllFinite = self._datasetMapped.xAllFinite + yAllFinite = self._datasetMapped.yAllFinite view = self.getViewBox() if view is None: @@ -979,12 +987,11 @@ def _getDisplayDataset(self): if self.opts['autoDownsample']: # this option presumes that x-values have uniform spacing - if containsNonfinite is False: + if xAllFinite: finite_x = x - else: # True or None - # True: (we checked and found non-finites) - # None: (we haven't performed a check for non-finites yet) - # we also land here if x is finite but y has non-finites + else: + # False: (we checked and found non-finites) + # None : (we haven't performed a check for non-finites yet) finite_x = x[np.isfinite(x)] # ignore infinite and nan values if view_range is not None and len(finite_x) > 1: @@ -1074,8 +1081,7 @@ def _getDisplayDataset(self): # y = np.clip(y, a_min=min_val, a_max=max_val) y = fn.clip_array(y, min_val, max_val) self._drlLastClip = (min_val, max_val) - self._datasetDisplay = PlotDataset( x,y ) - self._datasetDisplay.containsNonfinite = containsNonfinite + self._datasetDisplay = PlotDataset(x, y, xAllFinite, yAllFinite) self.setProperty('xViewRangeWasChanged', False) self.setProperty('yViewRangeWasChanged', False) From 438fe95eaeda51527537895883ee52d3a20599b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 May 2023 14:10:18 +0000 Subject: [PATCH 235/306] Bump pytest-xdist from 3.2.1 to 3.3.0 in /.github/workflows/etc Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 3.2.1 to 3.3.0. - [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.2.1...v3.3.0) --- updated-dependencies: - dependency-name: pytest-xdist dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 3e2872c27f..513fe805af 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -19,5 +19,5 @@ h5py==3.8.0 # testing pytest==7.3.1 -pytest-xdist==3.2.1 +pytest-xdist==3.3.0 pytest-xvfb==2.0.0; sys_platform == 'linux' From 1da2ec61d2618d2854f7b9a50c3780e8bca7b4ba Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 13 May 2023 21:23:06 +0800 Subject: [PATCH 236/306] refactor code into GLViewMixin --- pyqtgraph/opengl/GLViewWidget.py | 46 +++++++++++++++++++------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index a0715748df..9d9e27b14d 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -9,31 +9,18 @@ from .. import getConfigOption from ..Qt import QtCore, QtGui, QtWidgets -##Vector = QtGui.QVector3D - - -class GLViewWidget(QtWidgets.QOpenGLWidget): - - def __init__(self, parent=None, devicePixelRatio=None, rotationMethod='euler'): - """ - Basic widget for displaying 3D data - - Rotation/scale controls - - Axis/grid display - - Export options +class GLViewMixin: + def __init__(self, *args, rotationMethod='euler', **kwargs): + """ + Mixin class providing functionality for GLViewWidget ================ ============================================================== **Arguments:** - parent (QObject, optional): Parent QObject. Defaults to None. - devicePixelRatio No longer in use. High-DPI displays should automatically - detect the correct resolution. - rotationMethod (str): Mechanimsm to drive the rotation method, options are + rotationMethod (str): Mechanism to drive the rotation method, options are 'euler' and 'quaternion'. Defaults to 'euler'. ================ ============================================================== """ - - QtWidgets.QOpenGLWidget.__init__(self, parent) - - self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + super().__init__(*args, **kwargs) if rotationMethod not in ["euler", "quaternion"]: raise ValueError("Rotation method should be either 'euler' or 'quaternion'") @@ -572,3 +559,24 @@ def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE, textureSize glDeleteRenderbuffers(1, [depth_buf]) return output + + +class GLViewWidget(GLViewMixin, QtWidgets.QOpenGLWidget): + def __init__(self, *args, devicePixelRatio=None, **kwargs): + """ + Basic widget for displaying 3D data + - Rotation/scale controls + - Axis/grid display + - Export options + + ================ ============================================================== + **Arguments:** + parent (QObject, optional): Parent QObject. Defaults to None. + devicePixelRatio No longer in use. High-DPI displays should automatically + detect the correct resolution. + rotationMethod (str): Mechanism to drive the rotation method, options are + 'euler' and 'quaternion'. Defaults to 'euler'. + ================ ============================================================== + """ + super().__init__(*args, **kwargs) + self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) From 8cb649ab6ff7b05ef275da873265f3c929f839be Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 17 May 2023 21:39:40 +0800 Subject: [PATCH 237/306] update GLViewWidget rst docs --- .../api_reference/3dgraphics/glviewwidget.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/source/api_reference/3dgraphics/glviewwidget.rst b/doc/source/api_reference/3dgraphics/glviewwidget.rst index 6f89213ec6..d7dd009819 100644 --- a/doc/source/api_reference/3dgraphics/glviewwidget.rst +++ b/doc/source/api_reference/3dgraphics/glviewwidget.rst @@ -3,5 +3,19 @@ GLViewWidget .. autoclass:: pyqtgraph.opengl.GLViewWidget :members: + :show-inheritance: + :inherited-members: QOpenGLWidget .. automethod:: pyqtgraph.opengl.GLViewWidget.__init__ + + +.. autoclass:: pyqtgraph.opengl.GLViewWidget::GLViewMixin + + .. warning:: The intention of this class is to provide users who want to use + ``QOpenGLWindow`` instead of ``QOpenGLWidget`` but retain the benefits of + ``GLViewWidget``. Usage of this class should be considered experimental + for the time being as it may change without warning in future releases. + + :members: + + .. automethod:: pyqtgraph.opengl.GLViewWidget::GLViewMixin.__init__ From 8766e5afd0e3972667249a7b5f2a60f2e364298e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 14:14:17 +0000 Subject: [PATCH 238/306] Bump pytest-xdist from 3.3.0 to 3.3.1 in /.github/workflows/etc Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 3.3.0 to 3.3.1. - [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.3.0...v3.3.1) --- updated-dependencies: - dependency-name: pytest-xdist dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 513fe805af..31419b62c1 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -19,5 +19,5 @@ h5py==3.8.0 # testing pytest==7.3.1 -pytest-xdist==3.3.0 +pytest-xdist==3.3.1 pytest-xvfb==2.0.0; sys_platform == 'linux' From 9a978fc2cdd6190e04ff837d8250dd981dab9354 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 21 May 2023 21:05:54 +0800 Subject: [PATCH 239/306] guard against auto-downsample oscillation --- pyqtgraph/graphicsItems/PlotDataItem.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 8f0b1e44fd..47b9ebc7a2 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -447,6 +447,7 @@ def setLogMode(self, xState, yState): self.opts['logMode'] = [xState, yState] self._datasetMapped = None # invalidate mapped data self._datasetDisplay = None # invalidate display data + self._adsLastValue = 1 # reset auto-downsample value self.updateItems(styleUpdate=False) self.informViewBoundsChanged() @@ -461,6 +462,7 @@ def setDerivativeMode(self, state): self.opts['derivativeMode'] = state self._datasetMapped = None # invalidate mapped data self._datasetDisplay = None # invalidate display data + self._adsLastValue = 1 # reset auto-downsample value self.updateItems(styleUpdate=False) self.informViewBoundsChanged() @@ -476,6 +478,7 @@ def setPhasemapMode(self, state): self.opts['phasemapMode'] = state self._datasetMapped = None # invalidate mapped data self._datasetDisplay = None # invalidate display data + self._adsLastValue = 1 # reset auto-downsample value self.updateItems(styleUpdate=False) self.informViewBoundsChanged() @@ -622,6 +625,7 @@ def setDownsampling(self, ds=None, auto=None, method=None): if changed: self._datasetMapped = None # invalidata mapped data self._datasetDisplay = None # invalidate display data + self._adsLastValue = 1 # reset auto-downsample value self.updateItems(styleUpdate=False) def setClipToView(self, state): @@ -822,6 +826,7 @@ def setData(self, *args, **kargs): self._dataset = PlotDataset( xData, yData ) self._datasetMapped = None # invalidata mapped data , will be generated in getData() / _getDisplayDataset() self._datasetDisplay = None # invalidate display data, will be generated in getData() / _getDisplayDataset() + self._adsLastValue = 1 # reset auto-downsample value profiler('set data') @@ -1004,6 +1009,12 @@ def _getDisplayDataset(self): ds = int(ds_float) ## downsampling is expensive; delay until after clipping. + # use the last computed value if our new value is not too different. + # this guards against an infinite cycle where the plot never stabilizes. + if math.isclose(ds, self._adsLastValue, rel_tol=0.01): + ds = self._adsLastValue + self._adsLastValue = ds + if self.opts['clipToView']: if view is None or view.autoRangeEnabled()[0]: pass # no ViewBox to clip to, or view will autoscale to data range. From 8f8b5664b140bb937032e5c17cb687705b5d7127 Mon Sep 17 00:00:00 2001 From: stephan Date: Wed, 24 May 2023 10:03:15 +0200 Subject: [PATCH 240/306] Adapt hide icon (invisible eye) to style of other icons --- pyqtgraph/icons/invisibleEye.svg | 83 ++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/pyqtgraph/icons/invisibleEye.svg b/pyqtgraph/icons/invisibleEye.svg index 5a67832861..dbb1475af4 100644 --- a/pyqtgraph/icons/invisibleEye.svg +++ b/pyqtgraph/icons/invisibleEye.svg @@ -1,35 +1,68 @@ + xmlns:svg="http://www.w3.org/2000/svg"> + + - - - - + transform="matrix(1.3011758,0,0,1.3175188,37.063439,44.322322)" + id="g5" /> + x="53.386093" + y="39.182629" /> + + + + + From e0e920e7b6d5e5abfdf504ac828c672f111bdd2a Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 24 May 2023 08:55:55 -0700 Subject: [PATCH 241/306] Update .readthedocs.yaml Add libopengl0 apt package to RTD config --- .readthedocs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4746100396..d5ba108e30 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,8 @@ build: os: ubuntu-22.04 tools: python: "3" + apt_packages: + - libopengl0 sphinx: fail_on_warning: true From 9dcb11e2e2beeddc2a54b87236062a401a207385 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 16:12:33 +0000 Subject: [PATCH 242/306] Bump pyopengl from 3.1.6 to 3.1.7 in /.github/workflows/etc Bumps [pyopengl](https://github.com/mcfletch/pyopengl) from 3.1.6 to 3.1.7. - [Commits](https://github.com/mcfletch/pyopengl/commits/release-3.1.7) --- updated-dependencies: - dependency-name: pyopengl dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 31419b62c1..18064db2e1 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -11,7 +11,7 @@ scipy==1.10.1 numba==0.57.0; python_version == '3.9' # optional 3D -pyopengl==3.1.6 +pyopengl==3.1.7 # supplimental tools matplotlib==3.7.1 From df3e21322b911000bfe0c4f3ee1d1b1c67b2a4f8 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 27 May 2023 20:07:24 +0800 Subject: [PATCH 243/306] avoid using inner functions as slots the inner function actually captures both "self" and "self.pen" --- pyqtgraph/parametertree/parameterTypes/pen.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/pen.py b/pyqtgraph/parametertree/parameterTypes/pen.py index ab57158d55..91d2ddad3c 100644 --- a/pyqtgraph/parametertree/parameterTypes/pen.py +++ b/pyqtgraph/parametertree/parameterTypes/pen.py @@ -174,20 +174,13 @@ def _makeChildren(self, boundPen=None): name = name.title().strip() p.setOpts(title=name, default=default) - def penPropertyWrapper(propertySetter): - def tiePenPropToParam(_, value): - propertySetter(value) - self.sigValueChanging.emit(self, self.pen) - - return tiePenPropToParam - if boundPen is not None: self.updateFromPen(param, boundPen) for p in param: - setter, setName = self._setterForParam(p.name(), boundPen, returnName=True) + setName = f'set{p.name().capitalize()}' # Instead, set the parameter which will signal the old setter setattr(boundPen, setName, p.setValue) - newSetter = penPropertyWrapper(setter) + newSetter = self.penPropertySetter # Edge case: color picker uses a dialog with user interaction, so wait until full change there if p.type() != 'color': p.sigValueChanging.connect(newSetter) @@ -198,13 +191,13 @@ def tiePenPropToParam(_, value): return param - @staticmethod - def _setterForParam(paramName, obj, returnName=False): - formatted = paramName[0].upper() + paramName[1:] - setter = getattr(obj, f'set{formatted}') - if returnName: - return setter, formatted - return setter + def penPropertySetter(self, p, value): + boundPen = self.pen + setName = f'set{p.name().capitalize()}' + # boundPen.setName has been monkey-patched + # so we get the original setter from the class + getattr(boundPen.__class__, setName)(boundPen, value) + self.sigValueChanging.emit(self, boundPen) @staticmethod def updateFromPen(param, pen): From aaf40099f050b3cecb361cc0bc263052b155a0a2 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 28 May 2023 11:24:00 +0800 Subject: [PATCH 244/306] connect compatible signals directly --- pyqtgraph/graphicsItems/GradientEditorItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 0da7f268ef..f508113206 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -494,7 +494,7 @@ def __init__(self, *args, **kargs): self.linkedGradients = {} self.sigTicksChanged.connect(self._updateGradientIgnoreArgs) - self.sigTicksChangeFinished.connect(self.sigGradientChangeFinished.emit) + self.sigTicksChangeFinished.connect(self.sigGradientChangeFinished) def showTicks(self, show=True): for tick in self.ticks.keys(): From cdc8e641786fdfd11895c47c60b2a9639f64c9fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 14:08:59 +0000 Subject: [PATCH 245/306] Bump pytest-xvfb from 2.0.0 to 3.0.0 in /.github/workflows/etc Bumps [pytest-xvfb](https://github.com/The-Compiler/pytest-xvfb) from 2.0.0 to 3.0.0. - [Changelog](https://github.com/The-Compiler/pytest-xvfb/blob/master/CHANGELOG.rst) - [Commits](https://github.com/The-Compiler/pytest-xvfb/compare/v2.0.0...v3.0.0) --- updated-dependencies: - dependency-name: pytest-xvfb dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 18064db2e1..fa01932f33 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -20,4 +20,4 @@ h5py==3.8.0 # testing pytest==7.3.1 pytest-xdist==3.3.1 -pytest-xvfb==2.0.0; sys_platform == 'linux' +pytest-xvfb==3.0.0; sys_platform == 'linux' From 8da2bd86a55f81747229ef3c4ae6fc5aabb97779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Ollenschl=C3=A4ger?= <56151847+MalteOlle@users.noreply.github.com> Date: Sat, 3 Jun 2023 18:15:12 +0200 Subject: [PATCH 246/306] Numpy > 1.20 (#2739) * Support numpy >1.20 * Remove unnecessary test * Add MaD GUI as package that uses pyqtgraph --------- Co-authored-by: fischmalte --- README.md | 1 + pyqtgraph/functions.py | 2 +- tests/test_functions.py | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index edbc32f61f..efdab400fe 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Here is a partial listing of some of the applications that make use of PyQtGraph * [GraPhysio](https://github.com/jaj42/GraPhysio) * [HussariX](https://github.com/sem-geologist/HussariX) * [Joulescope](https://www.joulescope.com/) +* [MaD GUI](https://github.com/mad-lab-fau/mad-gui) * [neurotic](https://neurotic.readthedocs.io) * [Orange3](https://orangedatamining.com/) * [PatchView](https://github.com/ZeitgeberH/patchview) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 836f806d86..6d1ca73bec 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -546,7 +546,7 @@ def colorDistance(colors, metric='CIE76'): The `N-1` sequential distances between `N` colors. """ metric = metric.upper() - if len(colors) < 1: return np.array([], dtype=np.float) + if len(colors) < 1: return np.array([], dtype=float) if metric == 'CIE76': dist = [] lab1 = None diff --git a/tests/test_functions.py b/tests/test_functions.py index c327b423c4..7f0d81ffd3 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -400,3 +400,7 @@ def test_ndarray_from_qimage(): qimg.fill(0) arr = pg.functions.ndarray_from_qimage(qimg) assert arr.shape == (h, w) + +def test_colorDistance(): + pg.colorDistance([pg.Qt.QtGui.QColor(0,0,0), pg.Qt.QtGui.QColor(255,0,0)]) + pg.colorDistance([]) From 785e6413b51dbf2ba4b3050aaf11873fd90280e2 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 4 Jun 2023 14:10:12 -0700 Subject: [PATCH 247/306] Drop python 3.8 and numpy 1.20 support --- .github/workflows/etc/requirements.txt | 1 - .github/workflows/main.yml | 10 +++++----- README.md | 5 ++--- pyqtgraph/debug.py | 9 --------- setup.py | 8 ++++---- tox.ini | 6 +++--- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index fa01932f33..b08cacb9b6 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -2,7 +2,6 @@ numpy; python_version == '3.10' numpy; python_version == '3.11' numpy~=1.21.0; python_version == '3.9' -numpy~=1.20.0; python_version == '3.8' # image testing scipy==1.10.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 486c9acad3..30087c143a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,18 +23,18 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] qt-lib: [pyqt, pyside] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11"] include: - - python-version: "3.8" + - python-version: "3.9" qt-lib: "pyqt" qt-version: "PyQt5~=5.15.0" - - python-version: "3.8" + - python-version: "3.9" qt-lib: "pyside" qt-version: "PySide2~=5.15.0" - - python-version: "3.9" + - python-version: "3.10" qt-lib: "pyqt" qt-version: "PyQt6~=6.2.0 PyQt6-Qt6~=6.2.0" - - python-version: "3.9" + - python-version: "3.10" qt-lib: "pyside" qt-version: "PySide6~=6.2.0" - python-version: "3.10" diff --git a/README.md b/README.md index efdab400fe..7643c293af 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ This project supports: Currently this means: -* Python 3.8+ +* Python 3.9+ * Qt 5.15, 6.2+ * [PyQt5](https://www.riverbankcomputing.com/software/pyqt/), [PyQt6](https://www.riverbankcomputing.com/software/pyqt/), [PySide2](https://wiki.qt.io/Qt_for_Python), or [PySide6](https://wiki.qt.io/Qt_for_Python) -* [`numpy`](https://github.com/numpy/numpy) 1.20+ +* [`numpy`](https://github.com/numpy/numpy) 1.21+ ### Optional added functionalities @@ -122,5 +122,4 @@ Here is a partial listing of some of the applications that make use of PyQtGraph * [Semi-Supervised Semantic Annotator](https://gitlab.com/s3a/s3a) * [STDF-Viewer](https://github.com/noonchen/STDF-Viewer) - Do you use PyQtGraph in your own project, and want to add it to the list? Submit a pull request to update this listing! diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 5f9878f79f..36d15522c7 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -26,15 +26,6 @@ from .Qt import QT_LIB, QtCore from .util import cprint - -if sys.version.startswith("3.8") and QT_LIB == "PySide2": - from .Qt import PySide2 - if tuple(map(int, PySide2.__version__.split("."))) < (5, 14) \ - and PySide2.__version__.startswith(QtCore.__version__): - warnings.warn( - "Due to PYSIDE-1140, ThreadChase and ThreadColor will not work" + - " on pip-installed PySide2 bindings < 5.14" - ) from .util.mutex import Mutex diff --git a/setup.py b/setup.py index 782363b34c..2d21d69aeb 100644 --- a/setup.py +++ b/setup.py @@ -39,11 +39,11 @@ import os import re import sys +from distutils.command import build + from setuptools import find_namespace_packages, setup from setuptools.command import install -from distutils.command import build - path = os.path.split(__file__)[0] import tools.setupHelpers as helpers @@ -122,7 +122,7 @@ def run(self): 'style': helpers.StyleCommand }, packages=find_namespace_packages(include=['pyqtgraph', 'pyqtgraph.*']), - python_requires=">=3.8", + python_requires=">=3.9", package_dir={"pyqtgraph": "pyqtgraph"}, package_data={ 'pyqtgraph.examples': ['optics/*.gz', 'relativity/presets/*.cfg'], @@ -134,7 +134,7 @@ def run(self): ], }, install_requires = [ - 'numpy>=1.20.0', + 'numpy>=1.21.0', ], **setupOpts ) diff --git a/tox.ini b/tox.ini index abc2c85859..c01345ad7a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] envlist = ; qt 5.15.x - py{38,39,310}-{pyqt5,pyside2}_515 + py{39,310,311}-{pyqt5,pyside2}_515 ; qt 6.2 - py{38,39,310}-{pyqt6,pyside6}_62 + py{39,310,311}-{pyqt6,pyside6}_62 ; qt 6-newest - py{38,39,310}-{pyqt6,pyside6} + py{39,310,311}-{pyqt6,pyside6} [base] deps = From 4e8618fb5be3c7024d1dd560128f2c25cc562e9f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 19 May 2023 15:31:43 -0700 Subject: [PATCH 248/306] Remove unused PlotItem method, has been unused since b78662c33e64dc86b723ef96bf6b5765f4c596bf --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 82 -------------------- 1 file changed, 82 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index fb4824ff8b..5c9decb523 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -771,88 +771,6 @@ def updateParamList(self): self.paramList[p] = (i.checkState() == QtCore.Qt.CheckState.Checked) - def writeSvgCurves(self, fileName=None): - if fileName is None: - self._chooseFilenameDialog(handler=self.writeSvg) - return - - if isinstance(fileName, tuple): - raise Exception("Not implemented yet..") - fileName = str(fileName) - PlotItem.lastFileDir = os.path.dirname(fileName) - - rect = self.vb.viewRect() - xRange = rect.left(), rect.right() - - dx = max(rect.right(),0) - min(rect.left(),0) - ymn = min(rect.top(), rect.bottom()) - ymx = max(rect.top(), rect.bottom()) - dy = max(ymx,0) - min(ymn,0) - sx = 1. - sy = 1. - while dx*sx < 10: - sx *= 1000 - while dy*sy < 10: - sy *= 1000 - sy *= -1 - - with open(fileName, 'w') as fh: - # fh.write('\n' % (rect.left() * sx, - # rect.top() * sx, - # rect.width() * sy, - # rect.height()*sy)) - fh.write('\n') - fh.write('\n' % ( - rect.left() * sx, rect.right() * sx)) - fh.write('\n' % ( - rect.top() * sy, rect.bottom() * sy)) - - for item in self.curves: - if isinstance(item, PlotCurveItem): - color = item.pen.color() - hrrggbb, opacity = color.name(), color.alphaF() - x, y = item.getData() - mask = (x > xRange[0]) * (x < xRange[1]) - mask[:-1] += mask[1:] - m2 = mask.copy() - mask[1:] += m2[:-1] - x = x[mask] - y = y[mask] - - x *= sx - y *= sy - - # fh.write('\n' % ( - # color, )) - fh.write('') - # fh.write("") - - for item in self.dataItems: - if isinstance(item, ScatterPlotItem): - for point in item.points(): - pos = point.pos() - if not rect.contains(pos): - continue - color = point.brush.color() - hrrggbb, opacity = color.name(), color.alphaF() - x = pos.x() * sx - y = pos.y() * sy - - fh.write('\n' % ( - x, y, hrrggbb, opacity)) - - fh.write("\n") - def writeSvg(self, fileName=None): if fileName is None: self._chooseFilenameDialog(handler=self.writeSvg) From 8a6cdd7eb5bbbf2de623c6d3c6eefdcdf194e8a9 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 19 May 2023 15:43:45 -0700 Subject: [PATCH 249/306] Shift PlotCurveItem so it is centered about (0, 0) --- pyqtgraph/exporters/SVGExporter.py | 160 ++++++++++++++++------------- 1 file changed, 90 insertions(+), 70 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index e95e19e2c0..054d1531ff 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -3,7 +3,7 @@ import numpy as np -from .. import debug +from .. import PlotCurveItem, ScatterPlotItem, debug from .. import functions as fn from ..parametertree import Parameter from ..Qt import QtCore, QtGui, QtSvg, QtWidgets @@ -21,10 +21,7 @@ def __init__(self, item): Exporter.__init__(self, item) tr = self.getTargetRect() - if isinstance(item, QtWidgets.QGraphicsItem): - scene = item.scene() - else: - scene = item + scene = item.scene() if isinstance(item, QtWidgets.QGraphicsItem) else item bgbrush = scene.views()[0].backgroundBrush() bg = bgbrush.color() if bgbrush.style() == QtCore.Qt.BrushStyle.NoBrush: @@ -79,7 +76,7 @@ def export(self, fileName=None, toBytes=False, copy=False): QtWidgets.QApplication.clipboard().setMimeData(md) else: with open(fileName, 'wb') as fh: - fh.write(str(xml).encode('utf-8')) + fh.write(xml.encode('utf-8')) # Includes space for extra attributes xmlHeader = """\ @@ -96,10 +93,13 @@ def export(self, fileName=None, toBytes=False, copy=False): """ -def generateSvg(item, options={}): +def generateSvg(item, options=None): + if options is None: + options = {} global xmlHeader try: node, defs = _generateItemSvg(item, options=options) + finally: ## reset export mode for all items in the tree if isinstance(item, QtWidgets.QGraphicsScene): @@ -111,7 +111,6 @@ def generateSvg(item, options={}): for i in items: if hasattr(i, 'setExportMode'): i.setExportMode(False) - cleanXml(node) defsXml = "\n" @@ -123,8 +122,7 @@ def generateSvg(item, options={}): backgroundtag = '\n' % (c.red(), c.green(), c.blue(), c.alphaF()) return (xmlHeader % svgAttributes) + backgroundtag + defsXml + node.toprettyxml(indent=' ') + "\n\n" - -def _generateItemSvg(item, nodes=None, root=None, options={}): +def _generateItemSvg(item, nodes=None, root=None, options=None): ## 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. @@ -153,25 +151,25 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): ## inkscape only supports 1.1. ## ## Both 2 and 3 can be addressed by drawing all items in world coordinates. - + profiler = debug.Profiler() - + if options is None: + options = {} + if nodes is None: ## nodes maps all node IDs to their XML element. ## this allows us to ensure all elements receive unique names. nodes = {} - + if root is None: root = item - + ## Skip hidden items if hasattr(item, 'isVisible') and not item.isVisible(): return None - + ## If this item defines its own SVG generator, use that. if hasattr(item, 'generateSvg'): return item.generateSvg(nodes) - - ## Generate SVG text for just this item (exclude its children; we'll handle them later) if isinstance(item, QtWidgets.QGraphicsScene): xmlStr = "\n\n" @@ -183,8 +181,34 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): childs = item.childItems() else: childs = item.childItems() + + dx = dy = 0. + sx = sy = 1. + + if isinstance(item, PlotCurveItem): + # Workaround for lack of precision in SVG numbers + # We shift curves to be centered about (0, 0) and scaled such that + # they fit within the region of float32 values that we can + # distinguish between similar but different values + rect = item.viewRect() + x_range = rect.right() - rect.left() + dx = rect.left() + (x_range / 2) + + y_range = rect.top() - rect.bottom() + dy = rect.bottom() + y_range / 2 + + sx = 1 / abs(x_range) + sy = 1 / abs(y_range) + + item.blockSignals(True) + xDataOriginal = item.xData + yDataOriginal = item.yData + # use deepcopy of data to not mess with other references... + item.setData((item.xData.copy() - dx) * sx, (item.yData.copy() - dy) * sy) + item.blockSignals(False) + + manipulate = QtGui.QTransform(1 / sx, 0, 0, 1 / sy, dx, dy) tr = itemTransform(item, item.scene()) - ## offset to corner of root item if isinstance(root, QtWidgets.QGraphicsScene): rootPos = QtCore.QPoint(0,0) @@ -192,7 +216,7 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): rootPos = root.scenePos() tr2 = QtGui.QTransform() tr2.translate(-rootPos.x(), -rootPos.y()) - tr = tr * tr2 + tr = manipulate * tr * tr2 arr = QtCore.QByteArray() buf = QtCore.QBuffer(arr) @@ -215,14 +239,12 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): p.end() ## Can't do this here--we need to wait until all children have painted as well. ## this is taken care of in generateSvg instead. - #if hasattr(item, 'setExportMode'): - #item.setExportMode(False) + # if hasattr(item, 'setExportMode'): + # item.setExportMode(False) doc = xml.parseString(arr.data()) - try: ## Get top-level group for this item g1 = doc.getElementsByTagName('g')[0] - defs = doc.getElementsByTagName('defs') if len(defs) > 0: defs = [n for n in defs[0].childNodes if isinstance(n, xml.Element)] @@ -231,12 +253,11 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): raise profiler('render') - ## Get rid of group transformation matrices by applying ## transformation to inner coordinates correctCoordinates(g1, defs, item, options) profiler('correct') - + ## decide on a name for this item baseName = item.__class__.__name__ i = 1 @@ -247,33 +268,34 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): i += 1 nodes[name] = g1 g1.setAttribute('id', name) - + ## If this item clips its children, we need to take care of that. childGroup = g1 ## add children directly to this node unless we are clipping - if not isinstance(item, QtWidgets.QGraphicsScene): - ## See if this item clips its children - if item.flags() & item.GraphicsItemFlag.ItemClipsChildrenToShape: - ## Generate svg for just the path - path = QtWidgets.QGraphicsPathItem(item.mapToScene(item.shape())) - item.scene().addItem(path) - try: - pathNode = _generateItemSvg(path, root=root, options=options)[0].getElementsByTagName('path')[0] - # assume for this path is empty.. possibly problematic. - finally: - item.scene().removeItem(path) - + if ( + not isinstance(item, QtWidgets.QGraphicsScene) and + item.flags() & item.GraphicsItemFlag.ItemClipsChildrenToShape + ): + ## Generate svg for just the path + path = QtWidgets.QGraphicsPathItem(item.mapToScene(item.shape())) + item.scene().addItem(path) + try: + pathNode = _generateItemSvg(path, root=root, options=options)[0].getElementsByTagName('path')[0] + # assume for this path is empty.. possibly problematic. + finally: + item.scene().removeItem(path) + ## and for the clipPath element - clip = name + '_clip' - clipNode = g1.ownerDocument.createElement('clipPath') - clipNode.setAttribute('id', clip) - clipNode.appendChild(pathNode) - g1.appendChild(clipNode) - - childGroup = g1.ownerDocument.createElement('g') - childGroup.setAttribute('clip-path', 'url(#%s)' % clip) - g1.appendChild(childGroup) + clip = f'{name}_clip' + clipNode = g1.ownerDocument.createElement('clipPath') + clipNode.setAttribute('id', clip) + clipNode.appendChild(pathNode) + g1.appendChild(clipNode) + + childGroup = g1.ownerDocument.createElement('g') + childGroup.setAttribute('clip-path', f'url(#{clip})') + g1.appendChild(childGroup) profiler('clipping') - + ## Add all child items as sub-elements. childs.sort(key=lambda c: c.zValue()) for ch in childs: @@ -290,12 +312,12 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): def correctCoordinates(node, defs, item, options): # TODO: correct gradient coordinates inside defs - + ## Remove transformation matrices from tags by applying matrix to coordinates inside. ## Each item is represented by a single top-level group with one or more groups inside. ## Each inner group contains one or more drawing primitives, possibly of different types. groups = node.getElementsByTagName('g') - + ## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart. ## (if at some point we start correcting text transforms as well, then it should be safe to remove this) groups2 = [] @@ -320,8 +342,7 @@ def correctCoordinates(node, defs, item, options): node.insertBefore(sg, grp) node.removeChild(grp) groups = groups2 - - + for grp in groups: matrix = grp.getAttribute('transform') match = re.match(r'matrix\((.*)\)', matrix) @@ -330,7 +351,7 @@ def correctCoordinates(node, defs, item, options): else: vals = [float(a) for a in match.groups()[0].split(',')] tr = np.array([[vals[0], vals[2], vals[4]], [vals[1], vals[3], vals[5]]]) - + removeTransform = False for ch in grp.childNodes: if not isinstance(ch, xml.Element): @@ -358,22 +379,22 @@ def correctCoordinates(node, defs, item, options): # 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:] + newCoords = f'M{newCoords[1:]}' ch.setAttribute('d', newCoords) elif ch.tagName == 'text': removeTransform = False ## leave text alone for now. Might need this later to correctly render text with outline. - #c = np.array([ - #[float(ch.getAttribute('x')), float(ch.getAttribute('y'))], - #[float(ch.getAttribute('font-size')), 0], - #[0,0]]) - #c = fn.transformCoordinates(tr, c, transpose=True) - #ch.setAttribute('x', str(c[0,0])) - #ch.setAttribute('y', str(c[0,1])) - #fs = c[1]-c[2] - #fs = (fs**2).sum()**0.5 - #ch.setAttribute('font-size', str(fs)) - + # c = np.array([ + # [float(ch.getAttribute('x')), float(ch.getAttribute('y'))], + # [float(ch.getAttribute('font-size')), 0], + # [0,0]]) + # c = fn.transformCoordinates(tr, c, transpose=True) + # ch.setAttribute('x', str(c[0,0])) + # ch.setAttribute('y', str(c[0,1])) + # fs = c[1]-c[2] + # fs = (fs**2).sum()**0.5 + # ch.setAttribute('font-size', str(fs)) + ## Correct some font information families = ch.getAttribute('font-family').split(',') if len(families) == 1: @@ -385,14 +406,14 @@ def correctCoordinates(node, defs, item, options): elif font.styleHint() == font.StyleHint.Courier: families.append('monospace') 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') != '': 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 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') @@ -407,14 +428,13 @@ def correctCoordinates(node, defs, item, options): def itemTransform(item, root): ## Return the transformation mapping item to root ## (actually to parent coordinate system of root) - + if item is root: tr = QtGui.QTransform() tr.translate(*item.pos()) tr = tr * item.transform() return tr - - + if item.flags() & item.GraphicsItemFlag.ItemIgnoresTransformations: pos = item.pos() parent = item.parentItem() From 4ad733e418a0d6f0180ab4aa5af30855f13b538c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 4 Jun 2023 10:27:17 -0700 Subject: [PATCH 250/306] Add GraphicsItem.generateSvg method which SVG exporter was checking for --- pyqtgraph/exporters/SVGExporter.py | 18 ++++++++------ pyqtgraph/graphicsItems/GraphicsItem.py | 31 ++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 054d1531ff..a59e9a07ed 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -1,3 +1,6 @@ +__all__ = ['SVGExporter'] + +import contextlib import re import xml.dom.minidom as xml @@ -11,8 +14,6 @@ translate = QtCore.QCoreApplication.translate -__all__ = ['SVGExporter'] - class SVGExporter(Exporter): Name = "Scalable Vector Graphics (SVG)" allowCopy=True @@ -99,7 +100,6 @@ def generateSvg(item, options=None): global xmlHeader try: node, defs = _generateItemSvg(item, options=options) - finally: ## reset export mode for all items in the tree if isinstance(item, QtWidgets.QGraphicsScene): @@ -167,8 +167,8 @@ def _generateItemSvg(item, nodes=None, root=None, options=None): if hasattr(item, 'isVisible') and not item.isVisible(): return None - ## If this item defines its own SVG generator, use that. - if hasattr(item, 'generateSvg'): + with contextlib.suppress(NotImplementedError, AttributeError): + # If this item defines its own SVG generator, use that. return item.generateSvg(nodes) ## Generate SVG text for just this item (exclude its children; we'll handle them later) if isinstance(item, QtWidgets.QGraphicsScene): @@ -199,12 +199,15 @@ def _generateItemSvg(item, nodes=None, root=None, options=None): sx = 1 / abs(x_range) sy = 1 / abs(y_range) - + item.blockSignals(True) + xDataOriginal = item.xData yDataOriginal = item.yData # use deepcopy of data to not mess with other references... - item.setData((item.xData.copy() - dx) * sx, (item.yData.copy() - dy) * sy) + item.setData( + (xDataOriginal.copy() - dx) * sx, (yDataOriginal.copy() - dy) * sy + ) item.blockSignals(False) manipulate = QtGui.QTransform(1 / sx, 0, 0, 1 / sy, dx, dy) @@ -242,6 +245,7 @@ def _generateItemSvg(item, nodes=None, root=None, options=None): # if hasattr(item, 'setExportMode'): # item.setExportMode(False) doc = xml.parseString(arr.data()) + try: ## Get top-level group for this item g1 = doc.getElementsByTagName('g')[0] diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 98e73a5d68..757db05c79 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -1,15 +1,18 @@ +__all__ = ['GraphicsItem'] + import operator import weakref +import xml.dom.minidom as xml from collections import OrderedDict from functools import reduce from math import hypot +from typing import Optional from .. import functions as fn from ..GraphicsScene import GraphicsScene from ..Point import Point from ..Qt import QtCore, QtWidgets, isQObjectAlive -__all__ = ['GraphicsItem'] # Recipe from https://docs.python.org/3.8/library/collections.html#collections.OrderedDict # slightly adapted for Python 3.7 compatibility @@ -152,8 +155,6 @@ def viewTransform(self): return self.sceneTransform() #return self.deviceTransform(view.viewportTransform()) - - def getBoundingParents(self): """Return a list of parents to this item that have child clipping enabled.""" p = self @@ -606,3 +607,27 @@ def setExportMode(self, export, opts=None): def getContextMenus(self, event): return [self.getMenu()] if hasattr(self, "getMenu") else [] + + def generateSvg(self, nodes: dict[str, xml.Element]) -> Optional[tuple[xml.Element, list[xml.Element]]]: + """Method to overwrite to manually specify the SVG writer mechanism + + Parameters + ---------- + nodes : dict[str, xml.Element] + Dictionary keyed by the name of graphics items and the XML + representation of the the item that can be written as valid + SVG. + + Returns + ------- + Optional[tuple[xml.Element, list[xml.Element]]] + First element is the top level group for this item + Second element is a list of xml Elements corresponding to the + child nodes of the item. + + Raises + ------ + NotImplementedError + Overwrite method to implement in subclasses of GraphicsItem + """ + raise NotImplementedError From 27039d346f99e85064f33c4871d9ac744c89ed2c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 4 Jun 2023 13:51:39 -0700 Subject: [PATCH 251/306] Add documentation for SVG Exporter and abstract method --- doc/requirements.txt | 1 + .../api_reference/exporters/Exporter.rst | 5 ++ .../api_reference/exporters/SVGExporter.rst | 9 ++ doc/source/api_reference/exporters/index.rst | 13 +++ doc/source/api_reference/index.rst | 1 + doc/source/conf.py | 2 + pyqtgraph/exporters/SVGExporter.py | 86 +++++++++++++------ pyqtgraph/graphicsItems/GraphicsItem.py | 27 ++++-- 8 files changed, 108 insertions(+), 36 deletions(-) create mode 100644 doc/source/api_reference/exporters/Exporter.rst create mode 100644 doc/source/api_reference/exporters/SVGExporter.rst create mode 100644 doc/source/api_reference/exporters/index.rst diff --git a/doc/requirements.txt b/doc/requirements.txt index 172d12dba0..e2a8ed5045 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,6 +4,7 @@ pydata-sphinx-theme==0.13.3 sphinx-design==0.4.1 sphinxcontrib-images==0.9.4 sphinx-favicon==1.0.1 +sphinx-autodoc-typehints sphinx-qt-documentation sphinxext-rediraffe numpy diff --git a/doc/source/api_reference/exporters/Exporter.rst b/doc/source/api_reference/exporters/Exporter.rst new file mode 100644 index 0000000000..c84f856aae --- /dev/null +++ b/doc/source/api_reference/exporters/Exporter.rst @@ -0,0 +1,5 @@ +Exporter +============ + +.. autoclass:: pyqtgraph.exporters.Exporter + :members: diff --git a/doc/source/api_reference/exporters/SVGExporter.rst b/doc/source/api_reference/exporters/SVGExporter.rst new file mode 100644 index 0000000000..fd9fb962e3 --- /dev/null +++ b/doc/source/api_reference/exporters/SVGExporter.rst @@ -0,0 +1,9 @@ +SVGExporter +============ + +.. autoclass:: pyqtgraph.exporters.SVGExporter + :members: + +.. autofunction:: pyqtgraph.exporters.SVGExporter.generateSvg + +.. autofunction:: pyqtgraph.exporters.SVGExporter._generateItemSvg diff --git a/doc/source/api_reference/exporters/index.rst b/doc/source/api_reference/exporters/index.rst new file mode 100644 index 0000000000..8d6c5a043c --- /dev/null +++ b/doc/source/api_reference/exporters/index.rst @@ -0,0 +1,13 @@ +.. _exporter: + + +API Reference +============= + +Contents: + +.. toctree:: + :maxdepth: 2 + + Exporter + SVGExporter diff --git a/doc/source/api_reference/index.rst b/doc/source/api_reference/index.rst index 7a91d2762e..3b097eba50 100644 --- a/doc/source/api_reference/index.rst +++ b/doc/source/api_reference/index.rst @@ -17,6 +17,7 @@ Contents: parametertree/index dockarea graphicsscene/index + exporters/index flowchart/index point transform3d diff --git a/doc/source/conf.py b/doc/source/conf.py index f61625d989..7d26cc9c16 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -42,6 +42,7 @@ "sphinx_favicon", "sphinxext.rediraffe", "sphinxcontrib.images", + "sphinx_autodoc_typehints" ] # Add any paths that contain templates here, relative to this directory. @@ -133,6 +134,7 @@ "matplotlib", ] + # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index a59e9a07ed..e745c66bbc 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -123,34 +123,64 @@ def generateSvg(item, options=None): return (xmlHeader % svgAttributes) + backgroundtag + defsXml + node.toprettyxml(indent=' ') + "\n\n" def _generateItemSvg(item, nodes=None, root=None, options=None): - ## 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. - ## The solution is to let Qt generate SVG for each item independently, - ## then glue them together manually with clipping. - ## - ## The format Qt generates for all items looks like this: - ## - ## - ## - ## one or more of: or or - ## - ## - ## one or more of: or or - ## - ## . . . - ## - ## - ## 2) There seems to be wide disagreement over whether path strokes - ## should be scaled anisotropically. - ## see: http://web.mit.edu/jonas/www/anisotropy/ - ## Given that both inkscape and illustrator seem to prefer isotropic - ## scaling, we will optimize for those cases. - ## - ## 3) Qt generates paths using non-scaling-stroke from SVG 1.2, but - ## inkscape only supports 1.1. - ## - ## Both 2 and 3 can be addressed by drawing all items in world coordinates. + """This function is intended to work around some issues with Qt's SVG generator + and SVG in general. + + .. warning:: + This function, while documented, is not considered part of the public + API. The reason for its documentation is for ease of referencing by + :func:`~pyqtgraph.GraphicsItem.generateSvg`. There should be no need + to call this function explicitly. + + 1. Qt SVG does not implement clipping paths. This is absurd. + The solution is to let Qt generate SVG for each item independently, + then glue them together manually with clipping. The format Qt generates + for all items looks like this: + + .. code-block:: xml + + + + one or more of: or or + + + one or more of: or or + + . . . + + + 2. There seems to be wide disagreement over whether path strokes + should be scaled anisotropically. Given that both inkscape and + illustrator seem to prefer isotropic scaling, we will optimize for + those cases. + + .. note:: + + see: http://web.mit.edu/jonas/www/anisotropy/ + + 3. Qt generates paths using non-scaling-stroke from SVG 1.2, but + inkscape only supports 1.1. + + Both 2 and 3 can be addressed by drawing all items in world coordinates. + + Parameters + ---------- + item : :class:`~pyqtgraph.GraphicsItem` + GraphicsItem to generate SVG of + nodes : dict of str, optional + dictionary keyed on graphics item names, values contains the + XML elements, by default None + root : :class:`~pyqtgraph.GraphicsItem`, optional + root GraphicsItem, if none, assigns to `item`, by default None + options : dict of str, optional + Options to be applied to the generated XML, by default None + + Returns + ------- + tuple + tuple where first element is XML element, second element is + a list of child GraphicItems XML elements + """ profiler = debug.Profiler() if options is None: diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 757db05c79..da0ff38d74 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -2,11 +2,11 @@ import operator import weakref -import xml.dom.minidom as xml from collections import OrderedDict from functools import reduce from math import hypot from typing import Optional +from xml.etree.ElementTree import Element from .. import functions as fn from ..GraphicsScene import GraphicsScene @@ -608,26 +608,37 @@ def setExportMode(self, export, opts=None): def getContextMenus(self, event): return [self.getMenu()] if hasattr(self, "getMenu") else [] - def generateSvg(self, nodes: dict[str, xml.Element]) -> Optional[tuple[xml.Element, list[xml.Element]]]: - """Method to overwrite to manually specify the SVG writer mechanism + def generateSvg( + self, + nodes: dict[str, Element] + ) -> Optional[tuple[Element, list[Element]]]: + """Method to override to manually specify the SVG writer mechanism. Parameters ---------- - nodes : dict[str, xml.Element] + nodes Dictionary keyed by the name of graphics items and the XML representation of the the item that can be written as valid SVG. Returns ------- - Optional[tuple[xml.Element, list[xml.Element]]] - First element is the top level group for this item - Second element is a list of xml Elements corresponding to the + tuple + First element is the top level group for this item. The + second element is a list of xml Elements corresponding to the child nodes of the item. + None + Return None if no XML is needed for rendering Raises ------ NotImplementedError - Overwrite method to implement in subclasses of GraphicsItem + override method to implement in subclasses of GraphicsItem + + See Also + -------- + pyqtgraph.exporters.SVGExporter._generateItemSvg + The generic and default implementation + """ raise NotImplementedError From 760d85c6cd2a5f3892bf2f242158bb949df3d80f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 4 Jun 2023 23:26:35 -0700 Subject: [PATCH 252/306] Correctly save background color and size SVG --- pyqtgraph/exporters/SVGExporter.py | 31 ++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index e745c66bbc..77d7c7be73 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -36,8 +36,15 @@ def __init__(self, item): 'limits': (0, None)}, #{'name': 'viewbox clipping', 'type': 'bool', 'value': True}, #{'name': 'normalize coordinates', 'type': 'bool', 'value': True}, - {'name': 'scaling stroke', 'title': translate("Exporter", '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."}, + { + 'name': 'scaling stroke', + 'title': translate("Exporter", '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) @@ -68,7 +75,6 @@ def export(self, fileName=None, toBytes=False, copy=False): options['width'] = self.params['width'] options['height'] = self.params['height'] xml = generateSvg(self.item, options) - if toBytes: return xml.encode('UTF-8') elif copy: @@ -117,9 +123,9 @@ def generateSvg(item, options=None): for d in defs: defsXml += d.toprettyxml(indent=' ') defsXml += "\n" - svgAttributes = ' viewBox ="0 0 %f %f"' % (options["width"], options["height"]) + svgAttributes = f' viewBox ="0 0 {int(options["width"])} {int(options["height"])}"' c = options['background'] - backgroundtag = '\n' % (c.red(), c.green(), c.blue(), c.alphaF()) + backgroundtag = f'\n' return (xmlHeader % svgAttributes) + backgroundtag + defsXml + node.toprettyxml(indent=' ') + "\n\n" def _generateItemSvg(item, nodes=None, root=None, options=None): @@ -239,16 +245,22 @@ def _generateItemSvg(item, nodes=None, root=None, options=None): (xDataOriginal.copy() - dx) * sx, (yDataOriginal.copy() - dy) * sy ) item.blockSignals(False) - + manipulate = QtGui.QTransform(1 / sx, 0, 0, 1 / sy, dx, dy) tr = itemTransform(item, item.scene()) - ## offset to corner of root item + # offset to corner of root item if isinstance(root, QtWidgets.QGraphicsScene): rootPos = QtCore.QPoint(0,0) else: rootPos = root.scenePos() - tr2 = QtGui.QTransform() - tr2.translate(-rootPos.x(), -rootPos.y()) + + # handle rescaling from the export dialog + if hasattr(root, 'boundingRect'): + resize_x = options["width"] / root.boundingRect().width() + resize_y = options["height"] / root.boundingRect().height() + else: + resize_x = resize_y = 1 + tr2 = QtGui.QTransform(resize_x, 0, 0, resize_y, -rootPos.x(), -rootPos.y()) tr = manipulate * tr * tr2 arr = QtCore.QByteArray() @@ -257,7 +269,6 @@ def _generateItemSvg(item, nodes=None, root=None, options=None): svg.setOutputDevice(buf) dpi = QtGui.QGuiApplication.primaryScreen().logicalDotsPerInchX() svg.setResolution(int(dpi)) - p = QtGui.QPainter() p.begin(svg) if hasattr(item, 'setExportMode'): From b70e8e4743ddaa27112cbb31d7c4744301e97b63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:15:49 +0000 Subject: [PATCH 253/306] Bump pyqt6 from 6.5.0 to 6.5.1 in /doc Bumps [pyqt6](https://www.riverbankcomputing.com/software/pyqt/) from 6.5.0 to 6.5.1. --- updated-dependencies: - dependency-name: pyqt6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 172d12dba0..cf13a4bd6b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -PyQt6==6.5.0 +PyQt6==6.5.1 sphinx==6.2.1 pydata-sphinx-theme==0.13.3 sphinx-design==0.4.1 From dcee8963b169f55f553c2956b86a9c75cfec8cef Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Mon, 5 Jun 2023 13:53:16 -0700 Subject: [PATCH 254/306] SVGExporter correctly sets spacing on linearGradients --- pyqtgraph/exporters/SVGExporter.py | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 77d7c7be73..575c228fb3 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -296,7 +296,6 @@ def _generateItemSvg(item, nodes=None, root=None, options=None): except: print(doc.toxml()) raise - profiler('render') ## Get rid of group transformation matrices by applying ## transformation to inner coordinates @@ -355,8 +354,34 @@ def _generateItemSvg(item, nodes=None, root=None, options=None): return g1, defs -def correctCoordinates(node, defs, item, options): - # TODO: correct gradient coordinates inside defs +def correctCoordinates(node, defs, item, options): + # correct the defs in the linearGradient + for d in defs: + if d.tagName == "linearGradient": + # reset "gradientUnits" attribute to SVG default value + d.removeAttribute("gradientUnits") + + # replace with percentages + for coord in ("x1", "x2", "y1", "y2"): + if coord.startswith("x"): + denominator = item.boundingRect().width() + else: + denominator = item.boundingRect().height() + percentage = round(float(d.getAttribute(coord)) * 100 / denominator) + d.setAttribute(coord, f"{percentage}%") + + # replace stops with percentages + for child in filter( + lambda e: isinstance(e, xml.Element) and e.tagName == "stop", + d.childNodes + ): + offset = child.getAttribute("offset") + try: + child.setAttribute("offset", f"{round(float(offset) * 100)}%") + except ValueError: + # offset attribute could not be converted to float + # must be one of the other SVG accepted formats + continue ## Remove transformation matrices from tags by applying matrix to coordinates inside. ## Each item is represented by a single top-level group with one or more groups inside. From 5f9d341b479fb2a53713343d3455d32dffaa111f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:03:14 +0000 Subject: [PATCH 255/306] Bump pytest from 7.3.1 to 7.3.2 in /.github/workflows/etc Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.3.1...7.3.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index b08cacb9b6..525a6cf692 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -17,6 +17,6 @@ matplotlib==3.7.1 h5py==3.8.0 # testing -pytest==7.3.1 +pytest==7.3.2 pytest-xdist==3.3.1 pytest-xvfb==3.0.0; sys_platform == 'linux' From dd593136bfa42d0e706826a386e76c9c89740082 Mon Sep 17 00:00:00 2001 From: Scott Talbert Date: Thu, 15 Jun 2023 10:27:29 -0400 Subject: [PATCH 256/306] Fix NumPy warning in test_functions Fixes the below warning about integer conversion by using astype() - overflow seems OK/desired in this case. tests/test_functions.py:290 /home/runner/work/pyqtgraph/pyqtgraph/tests/test_functions.py:290: DeprecationWarning: NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of -1 to uint32 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows). np.arange(6, dtype=dtype), np.arange(0, -6, step=-1, dtype=dtype), 'all', --- tests/test_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_functions.py b/tests/test_functions.py index 7f0d81ffd3..e0b079202c 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -287,7 +287,7 @@ def _handle_underflow(dtype, *elements): "xs, ys, connect, expected", [ *( ( - np.arange(6, dtype=dtype), np.arange(0, -6, step=-1, dtype=dtype), 'all', + np.arange(6, dtype=dtype), np.arange(0, -6, step=-1).astype(dtype), 'all', _handle_underflow(dtype, (MoveToElement, 0.0, 0.0), (LineToElement, 1.0, -1.0), @@ -300,7 +300,7 @@ def _handle_underflow(dtype, *elements): ), *( ( - np.arange(6, dtype=dtype), np.arange(0, -6, step=-1, dtype=dtype), 'pairs', + np.arange(6, dtype=dtype), np.arange(0, -6, step=-1).astype(dtype), 'pairs', _handle_underflow(dtype, (MoveToElement, 0.0, 0.0), (LineToElement, 1.0, -1.0), @@ -313,7 +313,7 @@ def _handle_underflow(dtype, *elements): ), *( ( - np.arange(5, dtype=dtype), np.arange(0, -5, step=-1, dtype=dtype), 'pairs', + np.arange(5, dtype=dtype), np.arange(0, -5, step=-1).astype(dtype), 'pairs', _handle_underflow(dtype, (MoveToElement, 0.0, 0.0), (LineToElement, 1.0, -1.0), @@ -344,7 +344,7 @@ def _handle_underflow(dtype, *elements): ), *( ( - np.arange(5, dtype=dtype), np.arange(0, -5, step=-1, dtype=dtype), np.array([0, 1, 0, 1, 0]), + np.arange(5, dtype=dtype), np.arange(0, -5, step=-1).astype(dtype), np.array([0, 1, 0, 1, 0]), _handle_underflow(dtype, (MoveToElement, 0.0, 0.0), (MoveToElement, 1.0, -1.0), From 1cf3f2c6a8c1c3efd2b8b2bdba168132dafb3f62 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 17 Jun 2023 10:38:56 +0800 Subject: [PATCH 257/306] generate random integers directly --- tests/test_functions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_functions.py b/tests/test_functions.py index e0b079202c..426d757348 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -118,10 +118,15 @@ def test_subArray(): def test_rescaleData(): + rng = np.random.default_rng(12345) dtypes = map(np.dtype, ('ubyte', 'uint16', 'byte', 'int16', 'int', 'float')) for dtype1 in dtypes: for dtype2 in dtypes: - data = (np.random.random(size=10) * 2**32 - 2**31).astype(dtype1) + if dtype1.kind in 'iu': + lim = np.iinfo(dtype1) + data = rng.integers(lim.min, lim.max, size=10, dtype=dtype1, endpoint=True) + else: + data = (rng.random(size=10) * 2**32 - 2**31).astype(dtype1) for scale, offset in [(10, 0), (10., 0.), (1, -50), (0.2, 0.5), (0.001, 0)]: if dtype2.kind in 'iu': lim = np.iinfo(dtype2) From 9b9e313e037c6d6c12c07b41590c3ade42191c49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:10:55 +0000 Subject: [PATCH 258/306] Bump h5py from 3.8.0 to 3.9.0 in /.github/workflows/etc Bumps [h5py](https://github.com/h5py/h5py) from 3.8.0 to 3.9.0. - [Release notes](https://github.com/h5py/h5py/releases) - [Changelog](https://github.com/h5py/h5py/blob/master/docs/release_guide.rst) - [Commits](https://github.com/h5py/h5py/compare/3.8.0...3.9.0) --- updated-dependencies: - dependency-name: h5py dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 525a6cf692..5259db2245 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -14,7 +14,7 @@ pyopengl==3.1.7 # supplimental tools matplotlib==3.7.1 -h5py==3.8.0 +h5py==3.9.0 # testing pytest==7.3.2 From 843541cfcf7264c387cb5cbb1c55c0ac20d6d6fc Mon Sep 17 00:00:00 2001 From: redruin1 Date: Wed, 21 Jun 2023 11:03:03 -0400 Subject: [PATCH 259/306] Add `dataBounds()` method to `TextItem` to make autoRange more predictable (#2646) * Add `dataBounds()` method to `TextItem` Sacrifices some visual polish to make the autorange functionality with TextItems much more predictable. * added init param `ensureInBounds` also updated clause in ViewBox to handle `None` case * Delete to_delete.py whoops --- pyqtgraph/graphicsItems/TextItem.py | 58 +++++++++++++++------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index ce43721b6c..b2060f3017 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -12,26 +12,29 @@ class TextItem(GraphicsObject): GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox). """ def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), - border=None, fill=None, angle=0, rotateAxis=None): + border=None, fill=None, angle=0, rotateAxis=None, ensureInBounds=False): """ - ============== ================================================================================= + ================ ================================================================================= **Arguments:** - *text* The text to display - *color* The color of the text (any format accepted by pg.mkColor) - *html* If specified, this overrides both *text* and *color* - *anchor* A QPointF or (x,y) sequence indicating what region of the text box will - be anchored to the item's position. A value of (0,0) sets the upper-left corner - of the text box to be at the position specified by setPos(), while a value of (1,1) - sets the lower-right corner. - *border* A pen to use when drawing the border - *fill* A brush to use when filling within the border - *angle* Angle in degrees to rotate text. Default is 0; text will be displayed upright. - *rotateAxis* If None, then a text angle of 0 always points along the +x axis of the scene. - If a QPointF or (x,y) sequence is given, then it represents a vector direction - in the parent's coordinate system that the 0-degree line will be aligned to. This - Allows text to follow both the position and orientation of its parent while still - discarding any scale and shear factors. - ============== ================================================================================= + *text* The text to display + *color* The color of the text (any format accepted by pg.mkColor) + *html* If specified, this overrides both *text* and *color* + *anchor* A QPointF or (x,y) sequence indicating what region of the text box will + be anchored to the item's position. A value of (0,0) sets the upper-left corner + of the text box to be at the position specified by setPos(), while a value of (1,1) + sets the lower-right corner. + *border* A pen to use when drawing the border + *fill* A brush to use when filling within the border + *angle* Angle in degrees to rotate text. Default is 0; text will be displayed upright. + *rotateAxis* If None, then a text angle of 0 always points along the +x axis of the scene. + If a QPointF or (x,y) sequence is given, then it represents a vector direction + in the parent's coordinate system that the 0-degree line will be aligned to. This + Allows text to follow both the position and orientation of its parent while still + discarding any scale and shear factors. + *ensureInBounds* Ensures that the entire TextItem will be visible when using autorange, but may + produce runaway scaling in certain circumstances (See issue #2642). Setting to + "True" retains legacy behavior. + ================ ================================================================================= The effects of the `rotateAxis` and `angle` arguments are added independently. So for example: @@ -51,6 +54,13 @@ def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), self.textItem.setParentItem(self) self._lastTransform = None self._lastScene = None + + # Note: The following is pretty scuffed; ideally there would likely be + # some inheritance changes, But this is the least-intrusive thing that + # works for now + if ensureInBounds: + self.dataBounds = None + self._bounds = QtCore.QRectF() if html is None: self.setColor(color) @@ -150,6 +160,18 @@ def updateTextPos(self): offset = (br - tl) * self.anchor self.textItem.setPos(-offset) + def dataBounds(self, ax, frac=1.0, orthoRange=None): + """ + Returns only the anchor point for when calulating view ranges. + + Sacrifices some visual polish for fixing issue #2642. + """ + if orthoRange: + range_min, range_max = orthoRange[0], orthoRange[1] + if not range_min <= self.anchor[ax] <= range_max: + return [None, None] + + return [self.anchor[ax], self.anchor[ax]] def boundingRect(self): return self.textItem.mapRectToParent(self.textItem.boundingRect()) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 2b8a0d5038..6bbe505584 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1441,7 +1441,7 @@ def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): useX = True useY = True - if hasattr(item, 'dataBounds'): + if hasattr(item, 'dataBounds') and item.dataBounds is not None: if frac is None: frac = (1.0, 1.0) xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) From 308b6c381a0426939c953d400bc9c892bff31b61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:00:43 -0700 Subject: [PATCH 260/306] Bump numba from 0.57.0 to 0.57.1 in /.github/workflows/etc (#2751) --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 5259db2245..6d32545428 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -7,7 +7,7 @@ numpy~=1.21.0; python_version == '3.9' scipy==1.10.1 # optional high performance paths -numba==0.57.0; python_version == '3.9' +numba==0.57.1; python_version == '3.9' # optional 3D pyopengl==3.1.7 From 47e2f425790fae92d3a0cdc9ed63ecca72c9b4f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 13:12:03 +0000 Subject: [PATCH 261/306] Bump pytest from 7.3.2 to 7.4.0 in /.github/workflows/etc Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.2 to 7.4.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.3.2...7.4.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 6d32545428..6104a84528 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -17,6 +17,6 @@ matplotlib==3.7.1 h5py==3.9.0 # testing -pytest==7.3.2 +pytest==7.4.0 pytest-xdist==3.3.1 pytest-xvfb==3.0.0; sys_platform == 'linux' From 1e12ea436304698e5b4597944dff5d797530a403 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 24 Jun 2023 20:05:53 -0700 Subject: [PATCH 262/306] Update tox.ini to not run on incompatible configs --- tox.ini | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index c01345ad7a..a8b1314d39 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,12 @@ [tox] -envlist = +envlist = ; qt 5.15.x - py{39,310,311}-{pyqt5,pyside2}_515 + py{39,310,311}-pyqt5_515 + py{39,310}-pyside2_515 ; qt 6.2 - py{39,310,311}-{pyqt6,pyside6}_62 + py{39,310,311}-pyqt6_62 + py{39,310}-pyside6_62 ; qt 6-newest py{39,310,311}-{pyqt6,pyside6} @@ -19,7 +21,7 @@ deps = h5py [testenv] -passenv = DISPLAY XAUTHORITY, PYTHON_VERSION +passenv = DISPLAY,XAUTHORITY,PYTHON_VERSION setenv = PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command deps= {[base]deps} From bb158edfa0bc5f8b412f51fe948f4ec5974758d9 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 24 Jun 2023 21:06:02 -0700 Subject: [PATCH 263/306] Remove use of np.product as it is deprecated --- pyqtgraph/functions.py | 2 +- pyqtgraph/opengl/items/GLMeshItem.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6d1ca73bec..d8542ad5d4 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -994,7 +994,7 @@ def interpolateArray(data, x, default=0.0, order=1): sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax]) sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim)) s[ax] = sax - s = np.product(s, axis=0) + s = np.prod(s, axis=0) result = fieldData * s for i in range(md): result = result.sum(axis=0) diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index f333e5d58a..d5b605b105 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -192,7 +192,7 @@ def paint(self): glNormalPointerf(norms) if faces is None: - glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1])) + glDrawArrays(GL_TRIANGLES, 0, np.prod(verts.shape[:-1])) else: faces = faces.astype(np.uint32).flatten() glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces) From 62170e0c7bccd1b186dba806ec90e0340af833b2 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 24 Jun 2023 21:08:07 -0700 Subject: [PATCH 264/306] bump minimum numpy to 1.22 --- .github/workflows/etc/requirements.txt | 2 +- README.md | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 6d32545428..3304c48ad2 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -1,7 +1,7 @@ # numpy based on python version and NEP-29 requirements numpy; python_version == '3.10' numpy; python_version == '3.11' -numpy~=1.21.0; python_version == '3.9' +numpy~=1.22.0; python_version == '3.9' # image testing scipy==1.10.1 diff --git a/README.md b/README.md index 7643c293af..d058bcd030 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Currently this means: [PyQt6](https://www.riverbankcomputing.com/software/pyqt/), [PySide2](https://wiki.qt.io/Qt_for_Python), or [PySide6](https://wiki.qt.io/Qt_for_Python) -* [`numpy`](https://github.com/numpy/numpy) 1.21+ +* [`numpy`](https://github.com/numpy/numpy) 1.22+ ### Optional added functionalities diff --git a/setup.py b/setup.py index 2d21d69aeb..d8d6907ba1 100644 --- a/setup.py +++ b/setup.py @@ -134,7 +134,7 @@ def run(self): ], }, install_requires = [ - 'numpy>=1.21.0', + 'numpy>=1.22.0', ], **setupOpts ) From 6b736acfe5f572b211b793076b5e96b25cf15ea3 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 24 Jun 2023 22:23:33 -0700 Subject: [PATCH 265/306] workaround for numpy 1.22.4 issue --- pyqtgraph/Qt/internals.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/Qt/internals.py b/pyqtgraph/Qt/internals.py index 2e8c2c3c8b..02623a7711 100644 --- a/pyqtgraph/Qt/internals.py +++ b/pyqtgraph/Qt/internals.py @@ -1,8 +1,9 @@ import ctypes import itertools + import numpy as np -from . import QT_LIB, QtCore, QtGui -from . import compat + +from . import QT_LIB, QtCore, QtGui, compat __all__ = ["get_qpainterpath_element_array"] @@ -174,7 +175,11 @@ def __len__(self): def ndarray(self): # ndarray views are cheap to recreate each time if self.use_sip_array: - if sip.SIP_VERSION >= 0x60708: + if ( + sip.SIP_VERSION >= 0x60708 and + np.__version__ != "1.22.4" # TODO: remove me after numpy 1.23+ + ): # workaround for numpy/sip compatability issue + # see https://github.com/numpy/numpy/issues/21612 mv = self._siparray else: # sip.array prior to SIP_VERSION 6.7.8 had a buggy buffer protocol From bfa86116f49f0482ebd8044b99ef000184727a4e Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sun, 25 Jun 2023 07:52:02 -0700 Subject: [PATCH 266/306] Remove np.clip performance workaround for windows --- pyqtgraph/functions.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d8542ad5d4..2f3476e9be 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1192,12 +1192,6 @@ def clip_scalar(val, vmin, vmax): """ convenience function to avoid using np.clip for scalar values """ return vmin if val < vmin else vmax if val > vmax else val -# umath.clip was slower than umath.maximum(umath.minimum). -# See https://github.com/numpy/numpy/pull/20134 for details. -_win32_clip_workaround_needed = ( - sys.platform == 'win32' and - tuple(map(int, np.__version__.split(".")[:2])) < (1, 22) -) def clip_array(arr, vmin, vmax, out=None): # replacement for np.clip due to regression in @@ -1212,12 +1206,6 @@ def clip_array(arr, vmin, vmax, out=None): return np.core.umath.minimum(arr, vmax, out=out) elif vmax is None: return np.core.umath.maximum(arr, vmin, out=out) - elif _win32_clip_workaround_needed: - if out is None: - out = np.empty(arr.shape, dtype=np.find_common_type([arr.dtype], [type(vmax)])) - out = np.core.umath.minimum(arr, vmax, out=out) - return np.core.umath.maximum(out, vmin, out=out) - else: return np.core.umath.clip(arr, vmin, vmax, out=out) From 6eceb9743260c7f3c662aecdf68bfb9bd8d3a5c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:54:26 +0000 Subject: [PATCH 267/306] Bump scipy from 1.10.1 to 1.11.0 in /.github/workflows/etc Bumps [scipy](https://github.com/scipy/scipy) from 1.10.1 to 1.11.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.10.1...v1.11.0) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 3304c48ad2..a59f0aed67 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -4,7 +4,7 @@ numpy; python_version == '3.11' numpy~=1.22.0; python_version == '3.9' # image testing -scipy==1.10.1 +scipy==1.11.0 # optional high performance paths numba==0.57.1; python_version == '3.9' From 43b188badbe2b5e8126eea0e63070c4a48c65f1b Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 22 Jun 2023 22:42:03 +0800 Subject: [PATCH 268/306] create FrameCounter class --- pyqtgraph/examples/utils.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pyqtgraph/examples/utils.py b/pyqtgraph/examples/utils.py index e8d5dc090b..0ce23ae75c 100644 --- a/pyqtgraph/examples/utils.py +++ b/pyqtgraph/examples/utils.py @@ -1,5 +1,7 @@ from argparse import Namespace from collections import OrderedDict +from time import perf_counter +from pyqtgraph.Qt import QtCore # Avoid clash with module name examples_ = OrderedDict([ @@ -135,3 +137,28 @@ skiptest = dict([ ('ProgressDialog', 'ProgressDialog.py'), # modal dialog ]) + + +class FrameCounter(QtCore.QObject): + sigFpsUpdate = QtCore.Signal(object) + + def __init__(self, interval=1000): + super().__init__() + self.count = 0 + self.last_update = 0 + self.interval = interval + + def update(self): + self.count += 1 + + if self.last_update == 0: + self.last_update = perf_counter() + self.startTimer(self.interval) + + def timerEvent(self, evt): + now = perf_counter() + elapsed = now - self.last_update + fps = self.count / elapsed + self.last_update = now + self.count = 0 + self.sigFpsUpdate.emit(fps) From 66d0c3cf9b3d8682d3640ac13793ac49ad5646cc Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 22 Jun 2023 22:43:15 +0800 Subject: [PATCH 269/306] PlotSpeedTest: count frames --- pyqtgraph/examples/PlotSpeedTest.py | 34 ++++++----------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/pyqtgraph/examples/PlotSpeedTest.py b/pyqtgraph/examples/PlotSpeedTest.py index 2d7d09e3e2..a92edbfaba 100644 --- a/pyqtgraph/examples/PlotSpeedTest.py +++ b/pyqtgraph/examples/PlotSpeedTest.py @@ -4,8 +4,6 @@ """ import argparse -from collections import deque -from time import perf_counter import numpy as np @@ -14,6 +12,8 @@ import pyqtgraph.parametertree as ptree from pyqtgraph.Qt import QtCore, QtGui, QtWidgets +from utils import FrameCounter + # defaults here result in the same configuration as the original PlotSpeedTest parser = argparse.ArgumentParser() parser.add_argument('--noise', dest='noise', action='store_true') @@ -42,7 +42,6 @@ sfmt.setSwapInterval(0) QtGui.QSurfaceFormat.setDefaultFormat(sfmt) - class MonkeyCurveItem(pg.PlotCurveItem): def __init__(self, *args, **kwds): super().__init__(*args, **kwds) @@ -83,12 +82,6 @@ def paint(self, painter, opt, widget): curve = MonkeyCurveItem(pen=default_pen, brush='b') pw.addItem(curve) -rollingAverageSize = 1000 -elapsed = deque(maxlen=rollingAverageSize) - -def resetTimings(*args): - elapsed.clear() - @interactor.decorate( nest=True, nsamples={'limits': [0, None]}, @@ -123,25 +116,14 @@ def update( connect='all', skipFiniteCheck=False ): - global curve, data, ptr, elapsed, fpsLastUpdate + global ptr if connect == 'array': connect = connect_array - # Measure - t_start = perf_counter() curve.setData(data[ptr], antialias=antialias, connect=connect, skipFiniteCheck=skipFiniteCheck) - app.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents) - t_end = perf_counter() - elapsed.append(t_end - t_start) ptr = (ptr + 1) % data.shape[0] - - # update fps at most once every 0.2 secs - if t_end - fpsLastUpdate > 0.2: - fpsLastUpdate = t_end - average = np.mean(elapsed) - fps = 1 / average - pw.setTitle('%0.2f fps - %0.1f ms avg' % (fps, average * 1_000)) + framecnt.update() @interactor.decorate( useOpenGL={'readonly': not args.allow_opengl_toggle}, @@ -161,16 +143,14 @@ def updateOptions( curve.setFillLevel(0.0 if fillLevel else None) curve.setMethod(plotMethod) -params.sigTreeStateChanged.connect(resetTimings) - makeData() - -fpsLastUpdate = perf_counter() - timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) +framecnt = FrameCounter() +framecnt.sigFpsUpdate.connect(lambda fps: pw.setTitle(f'{fps:.1f} fps')) + if __name__ == '__main__': # Splitter by default gives too small of a width to the parameter tree, # so fix that right before the event loop From 593c5314cc0436f5cf3ad7a2c2160c7a64cd68f0 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 22 Jun 2023 22:13:51 +0800 Subject: [PATCH 270/306] ScatterPlotSpeedTest: count frames --- pyqtgraph/examples/ScatterPlotSpeedTest.py | 26 ++++++++-------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/examples/ScatterPlotSpeedTest.py b/pyqtgraph/examples/ScatterPlotSpeedTest.py index 35307f88cb..33b3cc1448 100644 --- a/pyqtgraph/examples/ScatterPlotSpeedTest.py +++ b/pyqtgraph/examples/ScatterPlotSpeedTest.py @@ -9,9 +9,10 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtWidgets import pyqtgraph.parametertree as ptree -from time import perf_counter import re +from utils import FrameCounter + translate = QtCore.QCoreApplication.translate app = pg.mkQApp() @@ -30,11 +31,8 @@ item = pg.ScatterPlotItem() hoverBrush = pg.mkBrush("y") ptr = 0 -lastTime = perf_counter() -fps = None timer = QtCore.QTimer() - def fmt(name): replace = r"\1 \2" name = re.sub(r"(\w)([A-Z])", replace, name) @@ -52,7 +50,7 @@ def fmt(name): size=dict(limits=[1, None]), ) def mkDataAndItem(count=500, size=10): - global data, fps + global data scale = 100 data = { "pos": np.random.normal(size=(50, count), scale=scale), @@ -97,7 +95,7 @@ def getData(randomize=False): ), ) def update(mode="Reuse Item"): - global ptr, lastTime, fps + global ptr if mode == "New Item": mkItem() elif mode == "Reuse Item": @@ -114,17 +112,7 @@ def update(mode="Reuse Item"): new.setBrush(hoverBrush) ptr += 1 - now = perf_counter() - dt = now - lastTime - lastTime = now - if fps is None: - fps = 1.0 / dt - else: - s = np.clip(dt * 3.0, 0, 1) - fps = fps * (1 - s) + (1.0 / dt) * s - p.setTitle("%0.2f fps" % fps) - p.repaint() - # app.processEvents() # force complete redraw for every plot + framecnt.update() @interactor.decorate() @@ -138,5 +126,9 @@ def pausePlot(paused=False): mkDataAndItem() timer.timeout.connect(update) timer.start(0) + +framecnt = FrameCounter() +framecnt.sigFpsUpdate.connect(lambda fps : p.setTitle(f'{fps:.1f} fps')) + if __name__ == "__main__": pg.exec() From 556020cf9596d09542c4589bac0bd20590eee0f7 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 22 Jun 2023 22:19:50 +0800 Subject: [PATCH 271/306] VideoSpeedTest: count frames --- pyqtgraph/examples/VideoSpeedTest.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/examples/VideoSpeedTest.py b/pyqtgraph/examples/VideoSpeedTest.py index 992332e7b4..00d2270e36 100644 --- a/pyqtgraph/examples/VideoSpeedTest.py +++ b/pyqtgraph/examples/VideoSpeedTest.py @@ -7,13 +7,14 @@ import argparse import sys -from time import perf_counter import numpy as np import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui, QtWidgets +from utils import FrameCounter + pg.setConfigOption('imageAxisOrder', 'row-major') import VideoTemplate_generic as ui_template @@ -240,12 +241,9 @@ def noticeNumbaCheck(): ui.cudaCheck.toggled.connect(noticeCudaCheck) ui.numbaCheck.toggled.connect(noticeNumbaCheck) - ptr = 0 -lastTime = perf_counter() -fps = None def update(): - global ui, ptr, lastTime, fps, LUT, img + global ptr if ui.lutCheck.isChecked(): useLut = LUT else: @@ -276,19 +274,14 @@ def update(): #img.setImage(data[ptr%data.shape[0]], autoRange=False) ptr += 1 - now = perf_counter() - dt = now - lastTime - lastTime = now - if fps is None: - fps = 1.0/dt - else: - s = np.clip(dt*3., 0, 1) - fps = fps * (1-s) + (1.0/dt) * s - ui.fpsLabel.setText('%0.2f fps' % fps) - app.processEvents() ## force complete redraw for every plot + framecnt.update() + timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) +framecnt = FrameCounter() +framecnt.sigFpsUpdate.connect(lambda fps: ui.fpsLabel.setText(f'{fps:.1f} fps')) + if __name__ == '__main__': pg.exec() From 6f0696ff41d7d75d7de9465a85dd39c82abd47dd Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 22 Jun 2023 23:18:30 +0800 Subject: [PATCH 272/306] MultiPlotSpeedTest: count frames --- pyqtgraph/examples/MultiPlotSpeedTest.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/examples/MultiPlotSpeedTest.py b/pyqtgraph/examples/MultiPlotSpeedTest.py index eddf8deeb4..2b2adac2c6 100644 --- a/pyqtgraph/examples/MultiPlotSpeedTest.py +++ b/pyqtgraph/examples/MultiPlotSpeedTest.py @@ -3,12 +3,11 @@ Test the speed of rapidly updating multiple plot curves """ -from time import perf_counter - import numpy as np import pyqtgraph as pg from pyqtgraph.Qt import QtCore +from utils import FrameCounter # pg.setConfigOptions(useOpenGL=True) app = pg.mkQApp("MultiPlot Speed Test") @@ -36,30 +35,21 @@ data = np.random.normal(size=(nPlots*23,nSamples)) ptr = 0 -lastTime = perf_counter() -fps = None -count = 0 def update(): - global curve, data, ptr, plot, lastTime, fps, nPlots, count - count += 1 + global ptr for i in range(nPlots): curves[i].setData(data[(ptr+i)%data.shape[0]]) ptr += nPlots - now = perf_counter() - dt = now - lastTime - lastTime = now - if fps is None: - fps = 1.0/dt - else: - s = np.clip(dt*3., 0, 1) - fps = fps * (1-s) + (1.0/dt) * s - plot.setTitle('%0.2f fps' % fps) - #app.processEvents() ## force complete redraw for every plot + framecnt.update() + timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) +framecnt = FrameCounter() +framecnt.sigFpsUpdate.connect(lambda fps: plot.setTitle(f'{fps:.1f} fps')) + if __name__ == '__main__': pg.exec() From dfdc8abc24c9dd3b8c2dd3c0e37d12c48e729580 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 22 Jun 2023 23:22:51 +0800 Subject: [PATCH 273/306] infiniteline_performance: count frames --- .../examples/infiniteline_performance.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/examples/infiniteline_performance.py b/pyqtgraph/examples/infiniteline_performance.py index 5425505edb..b42fb02caa 100644 --- a/pyqtgraph/examples/infiniteline_performance.py +++ b/pyqtgraph/examples/infiniteline_performance.py @@ -1,11 +1,10 @@ #!/usr/bin/python -from time import perf_counter - import numpy as np import pyqtgraph as pg from pyqtgraph.Qt import QtCore +from utils import FrameCounter app = pg.mkQApp("Infinite Line Performance") @@ -22,29 +21,20 @@ data = np.random.normal(size=(50, 5000)) ptr = 0 -lastTime = perf_counter() -fps = None - def update(): - global curve, data, ptr, p, lastTime, fps + global ptr curve.setData(data[ptr % 10]) ptr += 1 - now = perf_counter() - dt = now - lastTime - lastTime = now - if fps is None: - fps = 1.0/dt - else: - s = np.clip(dt*3., 0, 1) - fps = fps * (1-s) + (1.0/dt) * s - p.setTitle('%0.2f fps' % fps) - app.processEvents() # force complete redraw for every plot + framecnt.update() timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) +framecnt = FrameCounter() +framecnt.sigFpsUpdate.connect(lambda fps: p.setTitle(f'{fps:.1f} fps')) + if __name__ == '__main__': pg.exec() From 2827d1be7e47fdab0ed5e0f8084fa23552e826e1 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 22 Jun 2023 23:31:53 +0800 Subject: [PATCH 274/306] RemoteSpeedTest: count frames --- pyqtgraph/examples/RemoteSpeedTest.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/examples/RemoteSpeedTest.py b/pyqtgraph/examples/RemoteSpeedTest.py index 4addeb395a..2696f8799d 100644 --- a/pyqtgraph/examples/RemoteSpeedTest.py +++ b/pyqtgraph/examples/RemoteSpeedTest.py @@ -10,13 +10,13 @@ remote case is much faster. """ -from time import perf_counter - import numpy as np import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtWidgets +from utils import FrameCounter + app = pg.mkQApp() view = pg.widgets.RemoteGraphicsView.RemoteGraphicsView() @@ -45,11 +45,7 @@ rplt._setProxyOptions(deferGetattr=True) ## speeds up access to rplt.plot view.setCentralItem(rplt) -lastUpdate = perf_counter() -avgFps = 0.0 - def update(): - global check, label, plt, lastUpdate, avgFps, rpltfunc data = np.random.normal(size=(10000,50)).sum(axis=1) data += 5 * np.sin(np.linspace(0, 10, data.shape[0])) @@ -61,16 +57,15 @@ def update(): ## process. if lcheck.isChecked(): lplt.plot(data, clear=True) - - now = perf_counter() - fps = 1.0 / (now - lastUpdate) - lastUpdate = now - avgFps = avgFps * 0.8 + fps * 0.2 - label.setText("Generating %0.2f fps" % avgFps) + + framecnt.update() timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) +framecnt = FrameCounter() +framecnt.sigFpsUpdate.connect(lambda fps : label.setText(f"Generating {fps:.1f}")) + if __name__ == '__main__': pg.exec() From c4e8c5f28ea89104988d48426b67f527498f176d Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Sat, 17 Sep 2022 10:35:51 -0700 Subject: [PATCH 275/306] Add iterations argparse argument to PlotSpeedTest Add functionality to allow for the benchmark to stop after a specific number of iterations. By default, will run indefinitely as it currently does, however if an iteration argument is passed with a numerical value, the update timer will stop, and the example will exit cleanly. --- pyqtgraph/examples/MultiPlotSpeedTest.py | 16 +++++++++++-- pyqtgraph/examples/PlotSpeedTest.py | 26 +++++++++++++++++--- pyqtgraph/examples/RemoteSpeedTest.py | 16 ++++++++++++- pyqtgraph/examples/ScatterPlotSpeedTest.py | 28 ++++++++++++++++------ pyqtgraph/examples/VideoSpeedTest.py | 14 +++++++++-- 5 files changed, 85 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/examples/MultiPlotSpeedTest.py b/pyqtgraph/examples/MultiPlotSpeedTest.py index 2b2adac2c6..7f03ebecf3 100644 --- a/pyqtgraph/examples/MultiPlotSpeedTest.py +++ b/pyqtgraph/examples/MultiPlotSpeedTest.py @@ -2,12 +2,21 @@ """ Test the speed of rapidly updating multiple plot curves """ +import argparse +import itertools import numpy as np +from utils import FrameCounter import pyqtgraph as pg from pyqtgraph.Qt import QtCore -from utils import FrameCounter + +parser = argparse.ArgumentParser() +parser.add_argument('--iterations', default=float('inf'), type=float, + help="Number of iterations to run before exiting" +) +args = parser.parse_args() +iterations_counter = itertools.count() # pg.setConfigOptions(useOpenGL=True) app = pg.mkQApp("MultiPlot Speed Test") @@ -37,7 +46,10 @@ ptr = 0 def update(): global ptr - + if next(iterations_counter) > args.iterations: + timer.stop() + app.quit() + return None for i in range(nPlots): curves[i].setData(data[(ptr+i)%data.shape[0]]) diff --git a/pyqtgraph/examples/PlotSpeedTest.py b/pyqtgraph/examples/PlotSpeedTest.py index a92edbfaba..e077870938 100644 --- a/pyqtgraph/examples/PlotSpeedTest.py +++ b/pyqtgraph/examples/PlotSpeedTest.py @@ -4,16 +4,16 @@ """ import argparse +import itertools import numpy as np +from utils import FrameCounter import pyqtgraph as pg import pyqtgraph.functions as fn import pyqtgraph.parametertree as ptree from pyqtgraph.Qt import QtCore, QtGui, QtWidgets -from utils import FrameCounter - # defaults here result in the same configuration as the original PlotSpeedTest parser = argparse.ArgumentParser() parser.add_argument('--noise', dest='noise', action='store_true') @@ -30,6 +30,9 @@ parser.add_argument('--allow-opengl-toggle', action='store_true', help="""Allow on-the-fly change of OpenGL setting. This may cause unwanted side effects. """) +parser.add_argument('--iterations', default=float('inf'), type=float, + help="Number of iterations to run before exiting" +) args = parser.parse_args() if args.use_opengl is not None: @@ -42,6 +45,7 @@ sfmt.setSwapInterval(0) QtGui.QSurfaceFormat.setDefaultFormat(sfmt) + class MonkeyCurveItem(pg.PlotCurveItem): def __init__(self, *args, **kwds): super().__init__(*args, **kwds) @@ -81,6 +85,7 @@ def paint(self, painter, opt, widget): pw.setLabel('bottom', 'Index', units='B') curve = MonkeyCurveItem(pen=default_pen, brush='b') pw.addItem(curve) +iterations_counter = itertools.count() @interactor.decorate( nest=True, @@ -108,6 +113,7 @@ def makeData( params.child('makeData').setOpts(title='Plot Options') + @interactor.decorate( connect={'type': 'list', 'limits': ['all', 'pairs', 'finite', 'array']} ) @@ -118,13 +124,25 @@ def update( ): global ptr + if next(iterations_counter) > args.iterations: + # cleanly close down benchmark + timer.stop() + app.quit() + return None + if connect == 'array': connect = connect_array - curve.setData(data[ptr], antialias=antialias, connect=connect, skipFiniteCheck=skipFiniteCheck) + curve.setData( + data[ptr], + antialias=antialias, + connect=connect, + skipFiniteCheck=skipFiniteCheck + ) ptr = (ptr + 1) % data.shape[0] framecnt.update() + @interactor.decorate( useOpenGL={'readonly': not args.allow_opengl_toggle}, plotMethod={'limits': ['pyqtgraph', 'drawPolyline'], 'type': 'list'}, @@ -143,7 +161,9 @@ def updateOptions( curve.setFillLevel(0.0 if fillLevel else None) curve.setMethod(plotMethod) + makeData() + timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) diff --git a/pyqtgraph/examples/RemoteSpeedTest.py b/pyqtgraph/examples/RemoteSpeedTest.py index 2696f8799d..f369aedde3 100644 --- a/pyqtgraph/examples/RemoteSpeedTest.py +++ b/pyqtgraph/examples/RemoteSpeedTest.py @@ -10,12 +10,21 @@ remote case is much faster. """ +import argparse +import itertools + import numpy as np +from utils import FrameCounter import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtWidgets -from utils import FrameCounter +parser = argparse.ArgumentParser() +parser.add_argument('--iterations', default=float('inf'), type=float, + help="Number of iterations to run before exiting" +) +args = parser.parse_args() +iterations_counter = itertools.count() app = pg.mkQApp() @@ -46,6 +55,11 @@ view.setCentralItem(rplt) def update(): + if next(iterations_counter) > args.iterations: + timer.stop() + app.quit() + return None + data = np.random.normal(size=(10000,50)).sum(axis=1) data += 5 * np.sin(np.linspace(0, 10, data.shape[0])) diff --git a/pyqtgraph/examples/ScatterPlotSpeedTest.py b/pyqtgraph/examples/ScatterPlotSpeedTest.py index 33b3cc1448..f5d50d0cf5 100644 --- a/pyqtgraph/examples/ScatterPlotSpeedTest.py +++ b/pyqtgraph/examples/ScatterPlotSpeedTest.py @@ -5,16 +5,26 @@ (Scatter plots are still rather slow to draw; expect about 20fps) """ -import numpy as np -import pyqtgraph as pg -from pyqtgraph.Qt import QtCore, QtWidgets -import pyqtgraph.parametertree as ptree +import argparse +import itertools import re +import numpy as np from utils import FrameCounter +import pyqtgraph as pg +import pyqtgraph.parametertree as ptree +from pyqtgraph.Qt import QtCore, QtWidgets + translate = QtCore.QCoreApplication.translate +parser = argparse.ArgumentParser() +parser.add_argument('--iterations', default=float('inf'), type=float, + help="Number of iterations to run before exiting" +) +args = parser.parse_args() +iterations_counter = itertools.count() + app = pg.mkQApp() pt = ptree.ParameterTree(showHeader=False) @@ -29,6 +39,7 @@ data = {} item = pg.ScatterPlotItem() + hoverBrush = pg.mkBrush("y") ptr = 0 timer = QtCore.QTimer() @@ -37,7 +48,7 @@ def fmt(name): replace = r"\1 \2" name = re.sub(r"(\w)([A-Z])", replace, name) name = name.replace("_", " ") - return translate("ScatterPlot", name.title().strip() + ": ") + return translate("ScatterPlot", f"{name.title().strip()}: ") interactor = ptree.Interactor( @@ -96,6 +107,11 @@ def getData(randomize=False): ) def update(mode="Reuse Item"): global ptr + + if next(iterations_counter) > args.iterations: + timer.stop() + app.quit() + return None if mode == "New Item": mkItem() elif mode == "Reuse Item": @@ -110,7 +126,6 @@ def update(mode="Reuse Item"): item.pointsAt(new.pos()) old.resetBrush() # reset old's brush before setting new's to better simulate hovering new.setBrush(hoverBrush) - ptr += 1 framecnt.update() @@ -122,7 +137,6 @@ def pausePlot(paused=False): else: timer.start() - mkDataAndItem() timer.timeout.connect(update) timer.start(0) diff --git a/pyqtgraph/examples/VideoSpeedTest.py b/pyqtgraph/examples/VideoSpeedTest.py index 00d2270e36..949bcede7c 100644 --- a/pyqtgraph/examples/VideoSpeedTest.py +++ b/pyqtgraph/examples/VideoSpeedTest.py @@ -6,15 +6,15 @@ """ import argparse +import itertools import sys import numpy as np +from utils import FrameCounter import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui, QtWidgets -from utils import FrameCounter - pg.setConfigOption('imageAxisOrder', 'row-major') import VideoTemplate_generic as ui_template @@ -48,7 +48,11 @@ parser.add_argument('--lut', default=False, action='store_true', help="Use color lookup table") parser.add_argument('--lut-alpha', default=False, action='store_true', help="Use alpha color lookup table", dest='lut_alpha') parser.add_argument('--size', default='512x512', type=lambda s: tuple([int(x) for x in s.split('x')]), help="WxH image dimensions default='512x512'") +parser.add_argument('--iterations', default=float('inf'), type=float, + help="Number of iterations to run before exiting" +) args = parser.parse_args(sys.argv[1:]) +iterations_counter = itertools.count() if RawImageGLWidget is not None: # don't limit frame rate to vsync @@ -244,6 +248,12 @@ def noticeNumbaCheck(): ptr = 0 def update(): global ptr + if next(iterations_counter) > args.iterations: + # cleanly close down benchmark + timer.stop() + app.quit() + return None + if ui.lutCheck.isChecked(): useLut = LUT else: From 551161079e00303bbf52efcd38fb862ffa8e3d8a Mon Sep 17 00:00:00 2001 From: Josef Scheuer Date: Thu, 29 Sep 2022 14:04:53 +0200 Subject: [PATCH 276/306] compare items by "is" rather than "==" (operator not implemented!) --- pyqtgraph/GraphicsScene/exportDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index e98acb32fe..c39a6d760b 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -104,7 +104,7 @@ def updateFormatList(self): for exp in exporters.listExporters(): item = FormatExportListWidgetItem(exp, QtCore.QCoreApplication.translate('Exporter', exp.Name)) self.ui.formatList.addItem(item) - if item == current: + if item is current: self.ui.formatList.setCurrentRow(self.ui.formatList.count() - 1) gotCurrent = True From 6534a3f51d97a5f51ad1ea284923beea5a9ec8be Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 27 Jun 2023 21:32:50 +0800 Subject: [PATCH 277/306] xor away unwanted bit value --- pyqtgraph/graphicsItems/ROI.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 53ab90806e..39fb143298 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1542,7 +1542,13 @@ def mouseDragEvent(self, ev): if ev.isStart(): if ev.button() == QtCore.Qt.MouseButton.LeftButton: roi.setSelected(True) - mods = ev.modifiers() & ~self.snapModifier + mods = ev.modifiers() + try: + mods &= ~self.snapModifier + except ValueError: + # workaround bug in Python 3.11.4 that affects PyQt + if mods & self.snapModifier: + mods ^= self.snapModifier if roi.translatable and mods == self.translateModifier: self.dragMode = 'translate' elif roi.rotatable and mods == self.rotateModifier: From df4b01265dd0c02b0144f8328580b3ff1d438621 Mon Sep 17 00:00:00 2001 From: dingo9 Date: Thu, 29 Jun 2023 13:13:45 +0800 Subject: [PATCH 278/306] Update TextItem.py fix floating-point precision issues with QTransform.inverted() --- pyqtgraph/graphicsItems/TextItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index b2060f3017..f59039e995 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -224,7 +224,7 @@ def updateTransform(self, force=False): if not force and pt == self._lastTransform: return - t = pt.inverted()[0] + t = fn.invertQTransform(pt) # reset translation t.setMatrix(t.m11(), t.m12(), t.m13(), t.m21(), t.m22(), t.m23(), 0, 0, t.m33()) From 4384de5d92377b65574bcd7ad3ef81b337aca03a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:23:16 +0000 Subject: [PATCH 279/306] Bump scipy from 1.11.0 to 1.11.1 in /.github/workflows/etc Bumps [scipy](https://github.com/scipy/scipy) from 1.11.0 to 1.11.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.11.0...v1.11.1) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index ca5a73636f..841a86798c 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -4,7 +4,7 @@ numpy; python_version == '3.11' numpy~=1.22.0; python_version == '3.9' # image testing -scipy==1.11.0 +scipy==1.11.1 # optional high performance paths numba==0.57.1; python_version == '3.9' From 14b662aa0261cfdd69f02b11464f35e110716fa2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 13:13:24 +0000 Subject: [PATCH 280/306] Bump matplotlib from 3.7.1 to 3.7.2 in /.github/workflows/etc Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.7.1 to 3.7.2. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.7.1...v3.7.2) --- updated-dependencies: - dependency-name: matplotlib dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 841a86798c..68c35c1f90 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -13,7 +13,7 @@ numba==0.57.1; python_version == '3.9' pyopengl==3.1.7 # supplimental tools -matplotlib==3.7.1 +matplotlib==3.7.2 h5py==3.9.0 # testing From 1ff2f92852a8c1aa093ab0061144a3046bcfe6a7 Mon Sep 17 00:00:00 2001 From: bbc131 <36670201+bbc131@users.noreply.github.com> Date: Thu, 6 Jul 2023 18:13:03 +0200 Subject: [PATCH 281/306] Fix invisible InfiniteLine at plot edges (#2762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix invisible InfiniteLine at plot edges * Undo change of boundingRect * Fix boundingRect of ViewBox * Let GraphicsWidget.boundingRect() return a copy --------- Co-authored-by: Björn Bissinger --- pyqtgraph/graphicsItems/AxisItem.py | 16 ++++++++++++---- pyqtgraph/graphicsItems/GraphicsWidget.py | 4 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 ++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 854808ce39..7fafc69151 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -936,26 +936,34 @@ def generateDrawSpecs(self, p): else: tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) + left_offset = -1.0 + right_offset = 1.0 + top_offset = -1.0 + bottom_offset = 1.0 if self.orientation == 'left': - span = (bounds.topRight(), bounds.bottomRight()) + span = (bounds.topRight() + Point(left_offset, top_offset), + bounds.bottomRight() + Point(left_offset, bottom_offset)) tickStart = tickBounds.right() tickStop = bounds.right() tickDir = -1 axis = 0 elif self.orientation == 'right': - span = (bounds.topLeft(), bounds.bottomLeft()) + span = (bounds.topLeft() + Point(right_offset, top_offset), + bounds.bottomLeft() + Point(right_offset, bottom_offset)) tickStart = tickBounds.left() tickStop = bounds.left() tickDir = 1 axis = 0 elif self.orientation == 'top': - span = (bounds.bottomLeft(), bounds.bottomRight()) + span = (bounds.bottomLeft() + Point(left_offset, top_offset), + bounds.bottomRight() + Point(right_offset, top_offset)) tickStart = tickBounds.bottom() tickStop = bounds.bottom() tickDir = -1 axis = 1 elif self.orientation == 'bottom': - span = (bounds.topLeft(), bounds.topRight()) + span = (bounds.topLeft() + Point(left_offset, bottom_offset), + bounds.topRight() + Point(right_offset, bottom_offset)) tickStart = tickBounds.top() tickStop = bounds.top() tickDir = 1 diff --git a/pyqtgraph/graphicsItems/GraphicsWidget.py b/pyqtgraph/graphicsItems/GraphicsWidget.py index d86b6d1fc9..57f83f21ca 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidget.py +++ b/pyqtgraph/graphicsItems/GraphicsWidget.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui, QtWidgets +from ..Qt import QtCore, QtGui, QtWidgets from .GraphicsItem import GraphicsItem __all__ = ['GraphicsWidget'] @@ -64,7 +64,7 @@ def boundingRect(self): self._previousGeometry = geometry else: br = self._boundingRectCache - return br + return QtCore.QRectF(br) def shape(self): p = self._painterPathCache diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6bbe505584..6ea6e431d8 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -475,6 +475,10 @@ def resizeEvent(self, ev): self.sigStateChanged.emit(self) self.sigResized.emit(self) + def boundingRect(self): + br = super().boundingRect() + return br.adjusted(0, 0, +0.5, +0.5) + def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" return [x[:] for x in self.state['viewRange']] ## return copy From 810cfcc47a72a5edb556cd4b6378416cd16f305c Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 4 Jul 2023 20:19:00 +0800 Subject: [PATCH 282/306] fix: use accessor instead of direct attribute access --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index e9b83e3552..59c186fd19 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -351,7 +351,7 @@ def imageChanged(self, autoLevel=False, autoRange=False): self.region.setRegion([mn, mx]) profiler('set region') else: - mn, mx = self.imageItem().levels + mn, mx = self.imageItem().getLevels() self.region.setRegion([mn, mx]) else: # plot one histogram for each channel From be156c43bbddc9719ad8b727b26cdafd890a1547 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 5 Jul 2023 14:21:33 +0800 Subject: [PATCH 283/306] don't assume presence of sigImageChanged --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 59c186fd19..3bff15b06a 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -282,7 +282,8 @@ def setImageItem(self, img): HistogramLUTItem. """ self.imageItem = weakref.ref(img) - img.sigImageChanged.connect(self.imageChanged) + if hasattr(img, 'sigImageChanged'): + img.sigImageChanged.connect(self.imageChanged) self._setImageLookupTable() self.regionChanged() self.imageChanged(autoLevel=True) From 664fbe3fe193852af088938a9db7ed4c012eb140 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 6 Jul 2023 20:08:31 +0800 Subject: [PATCH 284/306] NonUniformImage: vectorize with drawRects --- pyqtgraph/graphicsItems/NonUniformImage.py | 161 +++++++++++++-------- 1 file changed, 101 insertions(+), 60 deletions(-) diff --git a/pyqtgraph/graphicsItems/NonUniformImage.py b/pyqtgraph/graphicsItems/NonUniformImage.py index 7c18f567bc..8986970ffe 100644 --- a/pyqtgraph/graphicsItems/NonUniformImage.py +++ b/pyqtgraph/graphicsItems/NonUniformImage.py @@ -1,12 +1,12 @@ -import math - +import warnings import numpy as np from .. import functions as fn -from .. import mkBrush, mkPen from ..colormap import ColorMap +from .. import Qt from ..Qt import QtCore, QtGui from .GraphicsObject import GraphicsObject +from .HistogramLUTItem import HistogramLUTItem __all__ = ['NonUniformImage'] @@ -18,7 +18,6 @@ class NonUniformImage(GraphicsObject): commonly used to display 2-d or slices of higher dimensional data that have a regular but non-uniform grid e.g. measurements or simulation results. """ - def __init__(self, x, y, z, border=None): GraphicsObject.__init__(self) @@ -38,28 +37,37 @@ def __init__(self, x, y, z, border=None): raise Exception("The length of x and y must match the shape of z.") # default colormap (black - white) - self.cmap = ColorMap(pos=[0.0, 1.0], color=[(0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)]) + self.cmap = ColorMap(pos=[0.0, 1.0], color=[(0, 0, 0), (255, 255, 255)]) self.data = (x, y, z) + self.levels = None self.lut = None self.border = border - self.generatePicture() + self.picture = None - def setLookupTable(self, lut, autoLevel=False): - lut.sigLevelsChanged.connect(self.generatePicture) - lut.gradient.sigGradientChanged.connect(self.generatePicture) - self.lut = lut + self.update() - if autoLevel: - _, _, z = self.data - f = z[np.isfinite(z)] - lut.setLevels(f.min(), f.max()) + def setLookupTable(self, lut, update=True, **kwargs): + # backwards compatibility hack + if isinstance(lut, HistogramLUTItem): + warnings.warn( + "NonUniformImage::setLookupTable(HistogramLUTItem) is deprecated " + "and will be removed in a future version of PyQtGraph. " + "use HistogramLUTItem::setImageItem(NonUniformImage) instead", + DeprecationWarning, stacklevel=2 + ) + lut.setImageItem(self) + return - self.generatePicture() + self.lut = lut + self.picture = None + if update: + self.update() def setColorMap(self, cmap): self.cmap = cmap - self.generatePicture() + self.picture = None + self.update() def getHistogram(self, **kwds): """Returns x and y arrays containing the histogram values for the current image. @@ -72,62 +80,95 @@ def getHistogram(self, **kwds): return hist[1][:-1], hist[0] - def generatePicture(self): - - x, y, z = self.data - - self.picture = QtGui.QPicture() - p = QtGui.QPainter(self.picture) - p.setPen(mkPen(None)) - - # normalize - if self.lut is not None: - mn, mx = self.lut.getLevels() - else: - f = z[np.isfinite(z)] - mn = f.min() - mx = f.max() + def setLevels(self, levels): + self.levels = levels + self.picture = None + self.update() - # draw the tiles - for i in range(x.size): - for j in range(y.size): + def getLevels(self): + if self.levels is None: + z = self.data[2] + z = z[np.isfinite(z)] + self.levels = z.min(), z.max() + return self.levels - value = z[i, j] + def generatePicture(self): + x, y, z = self.data - if np.isneginf(value): - value = 0.0 - elif np.isposinf(value): - value = 1.0 - elif math.isnan(value): - continue # ignore NaN - else: - value = (value - mn) / (mx - mn) # normalize + # pad x and y so that we don't need to special-case the edges + x = np.pad(x, 1, mode='edge') + y = np.pad(y, 1, mode='edge') - if self.lut: - color = self.lut.gradient.getColor(value) - else: - color = self.cmap.mapToQColor(value) + x = (x[:-1] + x[1:]) / 2 + y = (y[:-1] + y[1:]) / 2 - p.setBrush(mkBrush(color)) + X, Y = np.meshgrid(x[:-1], y[:-1], indexing='ij') + W, H = np.meshgrid(np.diff(x), np.diff(y), indexing='ij') + Z = z - # left, right, bottom, top - l = x[0] if i == 0 else (x[i - 1] + x[i]) / 2 - r = (x[i] + x[i + 1]) / 2 if i < x.size - 1 else x[-1] - b = y[0] if j == 0 else (y[j - 1] + y[j]) / 2 - t = (y[j] + y[j + 1]) / 2 if j < y.size - 1 else y[-1] + # get colormap, lut has precedence over cmap + if self.lut is None: + lut = self.cmap.getLookupTable() + elif callable(self.lut): + lut = self.lut(z) + else: + lut = self.lut + + # normalize and quantize + mn, mx = self.getLevels() + rng = mx - mn + if rng == 0: + rng = 1 + scale = len(lut) / rng + Z = fn.rescaleData(Z, scale, mn, dtype=int, clip=(0, len(lut)-1)) + + # replace nans positions with invalid lut index + invalid_coloridx = len(lut) + Z[np.isnan(z)] = invalid_coloridx + + # pre-allocate to the largest array needed + color_indices, counts = np.unique(Z, return_counts=True) + rectarray = Qt.internals.PrimitiveArray(QtCore.QRectF, 4) + rectarray.resize(counts.max()) + + # sorted_indices effectively groups together the + # (flattened) indices of the same coloridx together. + sorted_indices = np.argsort(Z, axis=None) + for arr in X, Y, W, H: + arr.shape = -1 # in-place unravel - p.drawRect(QtCore.QRectF(l, t, r - l, b - t)) + self.picture = QtGui.QPicture() + painter = QtGui.QPainter(self.picture) + painter.setPen(fn.mkPen(None)) + + # draw the tiles grouped by coloridx + offset = 0 + for coloridx, cnt in zip(color_indices, counts): + if coloridx == invalid_coloridx: + continue + indices = sorted_indices[offset:offset+cnt] + offset += cnt + rectarray.resize(cnt) + memory = rectarray.ndarray() + memory[:,0] = X[indices] + memory[:,1] = Y[indices] + memory[:,2] = W[indices] + memory[:,3] = H[indices] + + brush = fn.mkBrush(lut[coloridx]) + painter.setBrush(brush) + painter.drawRects(*rectarray.drawargs()) if self.border is not None: - p.setPen(self.border) - p.setBrush(fn.mkBrush(None)) - p.drawRect(self.boundingRect()) + painter.setPen(self.border) + painter.setBrush(fn.mkBrush(None)) + painter.drawRect(self.boundingRect()) - p.end() - - self.update() + painter.end() def paint(self, p, *args): + if self.picture is None: + self.generatePicture() p.drawPicture(0, 0, self.picture) def boundingRect(self): From b283293e1d68f3b264dfdde5d1e6a1d37e2fac4b Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 6 Jul 2023 20:13:36 +0800 Subject: [PATCH 285/306] NonUniformImage: change example to use ColorBarItem --- pyqtgraph/examples/NonUniformImage.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/pyqtgraph/examples/NonUniformImage.py b/pyqtgraph/examples/NonUniformImage.py index d0d74abeba..aa62e8127e 100644 --- a/pyqtgraph/examples/NonUniformImage.py +++ b/pyqtgraph/examples/NonUniformImage.py @@ -7,9 +7,7 @@ import numpy as np import pyqtgraph as pg -from pyqtgraph.graphicsItems.GradientEditorItem import Gradients from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage -from pyqtgraph.Qt import QtWidgets RPM2RADS = 2 * np.pi / 60 RADS2RPM = 1 / RPM2RADS @@ -35,37 +33,26 @@ P_mech = TAU * W P_loss[P_mech > 1.5e5] = np.NaN -# green - orange - red -Gradients['gor'] = {'ticks': [(0.0, (74, 158, 71)), (0.5, (255, 230, 0)), (1, (191, 79, 76))], 'mode': 'rgb'} - app = pg.mkQApp("NonUniform Image Example") -win = QtWidgets.QMainWindow() -cw = pg.GraphicsLayoutWidget() +win = pg.PlotWidget() win.show() win.resize(600, 400) -win.setCentralWidget(cw) win.setWindowTitle('pyqtgraph example: Non-uniform Image') -p = cw.addPlot(title="Power Losses [W]", row=0, col=0) - -lut = pg.HistogramLUTItem(orientation="horizontal") +p = win.getPlotItem() +p.setTitle("Power Losses [W]") p.setMouseEnabled(x=False, y=False) -cw.nextRow() -cw.addItem(lut) - -# load the gradient -lut.gradient.loadPreset('gor') - image = NonUniformImage(w * RADS2RPM, tau, P_loss.T) -image.setLookupTable(lut, autoLevel=True) image.setZValue(-1) p.addItem(image) -h = image.getHistogram() -lut.plot.setData(*h) +# green - orange - red +cmap = pg.ColorMap([0.0, 0.5, 1.0], [(74, 158, 71), (255, 230, 0), (191, 79, 76)]) +bar = pg.ColorBarItem(colorMap=cmap, orientation='h') +bar.setImageItem(image, insert_in=p) p.showGrid(x=True, y=True) From 6e5305f6d1bcba5bc73cff620585ba94063da7d3 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 7 Jul 2023 18:20:31 +0800 Subject: [PATCH 286/306] use 256-pt lut from cmap, like PCMI --- pyqtgraph/graphicsItems/NonUniformImage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/NonUniformImage.py b/pyqtgraph/graphicsItems/NonUniformImage.py index 8986970ffe..065e0effef 100644 --- a/pyqtgraph/graphicsItems/NonUniformImage.py +++ b/pyqtgraph/graphicsItems/NonUniformImage.py @@ -108,7 +108,7 @@ def generatePicture(self): # get colormap, lut has precedence over cmap if self.lut is None: - lut = self.cmap.getLookupTable() + lut = self.cmap.getLookupTable(nPts=256) elif callable(self.lut): lut = self.lut(z) else: From c83a2519da4f1de212e16f70c7d21224342685ed Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 7 Jul 2023 17:49:54 +0800 Subject: [PATCH 287/306] PCMI: display framerate --- pyqtgraph/examples/PColorMeshItem.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/pyqtgraph/examples/PColorMeshItem.py b/pyqtgraph/examples/PColorMeshItem.py index dc4baeec7e..bf23d7a62c 100644 --- a/pyqtgraph/examples/PColorMeshItem.py +++ b/pyqtgraph/examples/PColorMeshItem.py @@ -2,12 +2,11 @@ Demonstrates very basic use of PColorMeshItem """ -import time - import numpy as np import pyqtgraph as pg from pyqtgraph.Qt import QtCore +from utils import FrameCounter app = pg.mkQApp("PColorMesh Example") @@ -77,9 +76,6 @@ textitem = pg.TextItem(anchor=(1, 0)) view_auto_scale.addItem(textitem) -## Set the animation -fps = 25 # Frame per second of the animation - # Wave parameters wave_amplitude = 3 wave_speed = 0.3 @@ -93,10 +89,6 @@ view_auto_scale.setYRange(miny, maxy) textitem.setPos(np.max(x), maxy) -timer = QtCore.QTimer() -timer.setSingleShot(True) -# not using QTimer.singleShot() because of persistence on PyQt. see PR #1605 - textpos = None i=0 def updateData(): @@ -104,30 +96,26 @@ def updateData(): global textpos ## Display the new data set - t0 = time.perf_counter() color_noise = np.sin(i * 2*np.pi*color_noise_freq) new_x = x new_y = y+wave_amplitude*np.cos(x/wave_length+i) new_z = np.exp(-(x-np.cos(i*color_speed)*xn)**2/1000)[:-1,:-1] + color_noise - t1 = time.perf_counter() pcmi_auto.setData(new_x, new_y, new_z) pcmi_consistent.setData(new_x, new_y, new_z) - t2 = time.perf_counter() i += wave_speed + framecnt.update() - textitem.setText(f'{(t2 - t1)*1000:.1f} ms') - - # cap update rate at fps - delay = max(1000/fps - (t2 - t0), 0) - timer.start(int(delay)) - +timer = QtCore.QTimer() timer.timeout.connect(updateData) -updateData() +timer.start() + +framecnt = FrameCounter() +framecnt.sigFpsUpdate.connect(lambda fps: textitem.setText(f'{fps:.1f} fps')) if __name__ == '__main__': pg.exec() From 052d61f765776c7029170603abc4e154005ac9e2 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 7 Jul 2023 17:54:42 +0800 Subject: [PATCH 288/306] PCMI: draw by coloridx --- pyqtgraph/graphicsItems/PColorMeshItem.py | 96 ++++++++++++----------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index 107ee0b91a..734d85c9a9 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -1,4 +1,3 @@ -import itertools import warnings import numpy as np @@ -14,32 +13,40 @@ class QuadInstances: def __init__(self): - self.polys = [] + self.nrows = -1 + self.ncols = -1 + self.pointsarray = Qt.internals.PrimitiveArray(QtCore.QPointF, 2) + self.resize(0, 0) - def alloc(self, size): - self.polys.clear() - - # 2 * (size + 1) vertices, (x, y) - arr = np.empty((2 * (size + 1), 2), dtype=np.float64) - ptrs = list(map(Qt.compat.wrapinstance, - itertools.count(arr.ctypes.data, arr.strides[0]), - itertools.repeat(QtCore.QPointF, arr.shape[0]))) - - # arrange into 2 rows, (size + 1) vertices - points = [ptrs[:len(ptrs)//2], ptrs[len(ptrs)//2:]] - self.arr = arr.reshape((2, -1, 2)) - - # pre-create quads from those 2 rows of QPointF(s) - for j in range(size): - bl, tl = points[0][j:j+2] - br, tr = points[1][j:j+2] - poly = (bl, br, tr, tl) - self.polys.append(poly) + def resize(self, nrows, ncols): + if nrows == self.nrows and ncols == self.ncols: + return - def array(self, size): - if size != len(self.polys): - self.alloc(size) - return self.arr + self.nrows = nrows + self.ncols = ncols + + # (nrows + 1) * (ncols + 1) vertices, (x, y) + self.pointsarray.resize((nrows+1)*(ncols+1)) + points = self.pointsarray.instances() + # points is a flattened list of a 2d array of + # QPointF(s) of shape (nrows+1, ncols+1) + + # pre-create quads from those instances of QPointF(s). + # store the quads as a flattened list of a 2d array + # of polygons of shape (nrows, ncols) + polys = [] + for r in range(nrows): + for c in range(ncols): + bl = points[(r+0)*(ncols+1)+(c+0)] + tl = points[(r+0)*(ncols+1)+(c+1)] + br = points[(r+1)*(ncols+1)+(c+0)] + tr = points[(r+1)*(ncols+1)+(c+1)] + poly = (bl, br, tr, tl) + polys.append(poly) + self.polys = polys + + def ndarray(self): + return self.pointsarray.ndarray() def instances(self): return self.polys @@ -144,8 +151,7 @@ def __init__(self, *args, **kwargs): else: self.cmap = colormap.get('viridis') - lut_qcolor = self.cmap.getLookupTable(nPts=256, mode=self.cmap.QCOLOR) - self.lut_qbrush = [QtGui.QBrush(x) for x in lut_qcolor] + self.lut_qcolor = self.cmap.getLookupTable(nPts=256, mode=self.cmap.QCOLOR) self.quads = QuadInstances() @@ -198,7 +204,7 @@ def _prepareData(self, args): self._dataBounds = ((xmn, xmx), (ymn, ymx)) else: - ValueError('Data must been sent as (z) or (x, y, z)') + raise ValueError('Data must been sent as (z) or (x, y, z)') def setData(self, *args, **kwargs): @@ -266,7 +272,7 @@ def setData(self, *args, **kwargs): ## Prepare colormap # First we get the LookupTable - lut = self.lut_qbrush + lut = self.lut_qcolor # Second we associate each z value, that we normalize, to the lut scale = len(lut) - 1 # Decide whether to autoscale the colormap or use the same levels as before @@ -290,20 +296,23 @@ def setData(self, *args, **kwargs): else: drawConvexPolygon = painter.drawConvexPolygon - memory = self.quads.array(self.z.shape[1]) + self.quads.resize(self.z.shape[0], self.z.shape[1]) + memory = self.quads.ndarray() + memory[..., 0] = self.x.ravel() + memory[..., 1] = self.y.ravel() polys = self.quads.instances() - # Go through all the data and draw the polygons accordingly - for i in range(self.z.shape[0]): - # populate 2 rows of values into points - memory[..., 0] = self.x[i:i+2, :] - memory[..., 1] = self.y[i:i+2, :] + # group indices of same coloridx together + color_indices, counts = np.unique(norm, return_counts=True) + sorted_indices = np.argsort(norm, axis=None) - brushes = [lut[z] for z in norm[i].tolist()] - - for brush, poly in zip(brushes, polys): - painter.setBrush(brush) - drawConvexPolygon(poly) + offset = 0 + for coloridx, cnt in zip(color_indices, counts): + indices = sorted_indices[offset:offset+cnt] + offset += cnt + painter.setBrush(lut[coloridx]) + for idx in indices: + drawConvexPolygon(polys[idx]) painter.end() self.update() @@ -355,10 +364,9 @@ def getLevels(self): def setLookupTable(self, lut, update=True): - if lut is not self.lut_qbrush: - self.lut_qbrush = [QtGui.QBrush(x) for x in lut] - if update: - self._updateDisplayWithCurrentState() + self.lut_qcolor = lut[:] + if update: + self._updateDisplayWithCurrentState() From 2ac154ab5c794fec71e4f6c2be46830897b856a4 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 8 Jul 2023 08:42:49 +0800 Subject: [PATCH 289/306] remove deprecated cmap argument --- pyqtgraph/graphicsItems/PColorMeshItem.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py index 734d85c9a9..ce10b50fe5 100644 --- a/pyqtgraph/graphicsItems/PColorMeshItem.py +++ b/pyqtgraph/graphicsItems/PColorMeshItem.py @@ -1,11 +1,8 @@ -import warnings - import numpy as np from .. import Qt, colormap from .. import functions as fn from ..Qt import QtCore, QtGui -from .GradientEditorItem import Gradients # List of colormaps from .GraphicsObject import GraphicsObject __all__ = ['PColorMeshItem'] @@ -135,19 +132,6 @@ def __init__(self, *args, **kwargs): if not isinstance(cmap, colormap.ColorMap): raise ValueError('colorMap argument must be a ColorMap instance') self.cmap = cmap - elif 'cmap' in kwargs: - # legacy unadvertised argument for backwards compatibility. - # this will only use colormaps from Gradients. - # Note that the colors will be wrong for the hsv colormaps. - warnings.warn( - "The parameter 'cmap' will be removed in a version of PyQtGraph released after Nov 2022.", - DeprecationWarning, stacklevel=2 - ) - cmap = kwargs.get('cmap') - if not isinstance(cmap, str) or cmap not in Gradients: - raise NameError('Undefined colormap, should be one of the following: '+', '.join(['"'+i+'"' for i in Gradients.keys()])+'.') - pos, color = zip(*Gradients[cmap]['ticks']) - self.cmap = colormap.ColorMap(pos, color) else: self.cmap = colormap.get('viridis') From b6a564ce51fd9096da1a7231b51ac58ad572c334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:39:02 +0000 Subject: [PATCH 290/306] Bump pyqt6 from 6.5.1 to 6.5.2 in /doc Bumps [pyqt6](https://www.riverbankcomputing.com/software/pyqt/) from 6.5.1 to 6.5.2. --- updated-dependencies: - dependency-name: pyqt6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 1fc463e3cc..da2554de18 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -PyQt6==6.5.1 +PyQt6==6.5.2 sphinx==6.2.1 pydata-sphinx-theme==0.13.3 sphinx-design==0.4.1 From ce8b09c32f8a864ccabe7154a8528d4861e527e9 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 26 Jul 2023 19:19:32 +0800 Subject: [PATCH 291/306] use QGraphicsPixmapItem to draw colorbar --- pyqtgraph/graphicsItems/ColorBarItem.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py index 53c452872b..34df8972bc 100644 --- a/pyqtgraph/graphicsItems/ColorBarItem.py +++ b/pyqtgraph/graphicsItems/ColorBarItem.py @@ -5,8 +5,7 @@ from .. import colormap from .. import functions as fn -from ..Qt import QtCore -from .ImageItem import ImageItem +from ..Qt import QtCore, QtGui, QtWidgets from .LinearRegionItem import LinearRegionItem from .PColorMeshItem import PColorMeshItem from .PlotItem import PlotItem @@ -129,13 +128,12 @@ def __init__(self, values=None, width=25, colorMap=None, label=None, self.axis.unlinkFromView() self.axis.setRange( self.values[0], self.values[1] ) - self.bar = ImageItem(axisOrder='col-major') if self.horizontal: - self.bar.setImage( np.linspace(0, 1, 256).reshape( (-1,1) ) ) if label is not None: self.getAxis('bottom').setLabel(label) else: - self.bar.setImage( np.linspace(0, 1, 256).reshape( (1,-1) ) ) if label is not None: self.getAxis('left').setLabel(label) + self.bar = QtWidgets.QGraphicsPixmapItem() + self.bar.setShapeMode(self.bar.ShapeMode.BoundingRectShape) self.addItem(self.bar) if colorMap is not None: self.setColorMap(colorMap) @@ -280,7 +278,10 @@ def _update_items(self, update_cmap=False): # update color bar: self.axis.setRange( self.values[0], self.values[1] ) if update_cmap and self._colorMap is not None: - self.bar.setLookupTable( self._colorMap.getLookupTable(nPts=256) ) + lut = self._colorMap.getLookupTable(nPts=256, alpha=True) + lut = np.expand_dims(lut, axis=0 if self.horizontal else 1) + qimg = fn.ndarray_to_qimage(lut, QtGui.QImage.Format.Format_RGBA8888) + self.bar.setPixmap(QtGui.QPixmap.fromImage(qimg)) # update assigned ImageItems, too: for img_weakref in self.img_list: img = img_weakref() From ed4f4cf125ae5ecd5231c0d00d57752f9f5a9413 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:22:48 +0000 Subject: [PATCH 292/306] Bump sphinx-design from 0.4.1 to 0.5.0 in /doc Bumps [sphinx-design](https://github.com/executablebooks/sphinx-design) from 0.4.1 to 0.5.0. - [Release notes](https://github.com/executablebooks/sphinx-design/releases) - [Changelog](https://github.com/executablebooks/sphinx-design/blob/main/CHANGELOG.md) - [Commits](https://github.com/executablebooks/sphinx-design/compare/v0.4.1...v0.5.0) --- updated-dependencies: - dependency-name: sphinx-design dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index da2554de18..d6ac37701a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,7 @@ PyQt6==6.5.2 sphinx==6.2.1 pydata-sphinx-theme==0.13.3 -sphinx-design==0.4.1 +sphinx-design==0.5.0 sphinxcontrib-images==0.9.4 sphinx-favicon==1.0.1 sphinx-autodoc-typehints From b34e93ca5ac8379ce85279a50a042821b2fdf0bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:01:52 +0000 Subject: [PATCH 293/306] Bump sphinx from 6.2.1 to 7.1.0 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.1 to 7.1.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.1...v7.1.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index d6ac37701a..fa51edf7cc 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.5.2 -sphinx==6.2.1 +sphinx==7.1.0 pydata-sphinx-theme==0.13.3 sphinx-design==0.5.0 sphinxcontrib-images==0.9.4 From a689ea4c80b11e305cfb5bf127705aaa9745c41e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 28 Jul 2023 20:27:25 +0800 Subject: [PATCH 294/306] ImageItem: cache nan check --- pyqtgraph/graphicsItems/ImageItem.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 3050bd12e0..807781853f 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -52,6 +52,7 @@ def __init__(self, image=None, **kargs): self._unrenderable = False self._xp = None # either numpy or cupy, to match the image data self._defferedLevels = None + self._imageHasNans = None # None : not yet known self.axisOrder = getConfigOption('imageAxisOrder') self._dataTransform = self._inverseDataTransform = None @@ -401,6 +402,7 @@ def setImage(self, image=None, autoLevels=None, **kargs): if self.image is None or image.dtype != self.image.dtype: self._effectiveLut = None self.image = image + self._imageHasNans = None if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1: if 'autoDownsample' not in kargs: kargs['autoDownsample'] = True @@ -613,7 +615,10 @@ def _try_rescale_float(self, image, levels, lut): break # awkward, but fastest numpy native nan evaluation - if xp.isnan(image.min()): + if self._imageHasNans is None: + self._imageHasNans = xp.isnan(image.min()) + + if self._imageHasNans: # don't handle images with nans # this should be an uncommon case break From fc778fc71b3dc0cee9148d09d9f4a8f7cb82e8bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:50:55 +0000 Subject: [PATCH 295/306] Bump sphinx from 7.1.0 to 7.1.2 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.1.0 to 7.1.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.1.0...v7.1.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index fa51edf7cc..878dcaf63a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.5.2 -sphinx==7.1.0 +sphinx==7.1.2 pydata-sphinx-theme==0.13.3 sphinx-design==0.5.0 sphinxcontrib-images==0.9.4 From d4c5b46e572c3d75e61f2509ccf0d401b2417b2e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 8 Aug 2023 20:13:59 +0800 Subject: [PATCH 296/306] fix: wrong key set to _pixelVectorCache --- pyqtgraph/graphicsItems/GraphicsItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index da0ff38d74..854004534a 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -280,7 +280,7 @@ def pixelVectors(self, direction=None): #pv = Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0))) pv = Point(dti.map(normView).p2()), Point(dti.map(normOrtho).p2()) self._pixelVectorCache[1] = pv - self._pixelVectorCache[0] = dt + self._pixelVectorCache[0] = key self._pixelVectorGlobalCache[key] = pv return self._pixelVectorCache[1] From 0b66b7aefb595ee349669cbc39fa485b168c5c3e Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 8 Aug 2023 21:53:49 -0400 Subject: [PATCH 297/306] Fix #2786: proper handling of slider parameter suffix --- pyqtgraph/parametertree/parameterTypes/slider.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes/slider.py b/pyqtgraph/parametertree/parameterTypes/slider.py index a15e4fb0cc..1e27c04988 100644 --- a/pyqtgraph/parametertree/parameterTypes/slider.py +++ b/pyqtgraph/parametertree/parameterTypes/slider.py @@ -20,6 +20,7 @@ def __init__(self, param, depth): def updateDisplayLabel(self, value=None): if value is None: value = self.param.value() + self.sliderLabel.setText(self.prettyTextValue(self.slider.value())) value = str(value) if self._suffix is None: suffixTxt = '' @@ -27,9 +28,13 @@ def updateDisplayLabel(self, value=None): suffixTxt = f' {self._suffix}' self.displayLabel.setText(value + suffixTxt) + def setSuffix(self, suffix): self._suffix = suffix - self._updateLabel(self.slider.value()) + # This may be called during widget construction in which case there is no + # displayLabel yet + if hasattr(self, 'displayLabel'): + self.updateDisplayLabel(self.slider.value()) def makeWidget(self): param = self.param @@ -39,7 +44,7 @@ def makeWidget(self): self.slider = QtWidgets.QSlider() self.slider.setOrientation(QtCore.Qt.Orientation.Horizontal) - lbl = QtWidgets.QLabel() + lbl = self.sliderLabel = QtWidgets.QLabel() lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) w = QtWidgets.QWidget() @@ -54,10 +59,7 @@ def setValue(v): def getValue(): return self.span[self.slider.value()].item() - def vChanged(v): - lbl.setText(self.prettyTextValue(v)) - - self.slider.valueChanged.connect(vChanged) + self.slider.valueChanged.connect(self.updateDisplayLabel) def onMove(pos): self.sigChanging.emit(self, self.span[pos].item()) @@ -109,7 +111,6 @@ def optsChanged(self, param, opts): w.setMaximum(len(span) - 1) if 'suffix' in opts: self.setSuffix(opts['suffix']) - self.slider.valueChanged.emit(self.slider.value()) def limitsChanged(self, param, limits): self.optsChanged(param, dict(limits=limits)) From 8895a02187dce74a9e7b04f2696dafb3d9aac1f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 13:17:54 +0000 Subject: [PATCH 298/306] Bump sphinx from 7.1.2 to 7.2.2 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.1.2 to 7.2.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.1.2...v7.2.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 878dcaf63a..bc3c975abc 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.5.2 -sphinx==7.1.2 +sphinx==7.2.2 pydata-sphinx-theme==0.13.3 sphinx-design==0.5.0 sphinxcontrib-images==0.9.4 From 772347da86b2f790dd5dc97ca052b7742e44cdff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 14:06:10 +0000 Subject: [PATCH 299/306] Bump scipy from 1.11.1 to 1.11.2 in /.github/workflows/etc Bumps [scipy](https://github.com/scipy/scipy) from 1.11.1 to 1.11.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.11.1...v1.11.2) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/etc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/etc/requirements.txt b/.github/workflows/etc/requirements.txt index 68c35c1f90..b48d2b6484 100644 --- a/.github/workflows/etc/requirements.txt +++ b/.github/workflows/etc/requirements.txt @@ -4,7 +4,7 @@ numpy; python_version == '3.11' numpy~=1.22.0; python_version == '3.9' # image testing -scipy==1.11.1 +scipy==1.11.2 # optional high performance paths numba==0.57.1; python_version == '3.9' From 6793cad3c28cf3bec29596d81c8d22edc9f5e10d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 13:38:30 +0000 Subject: [PATCH 300/306] Bump sphinx from 7.2.2 to 7.2.3 in /doc Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.2.2 to 7.2.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.2.2...v7.2.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index bc3c975abc..314f7fa003 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.5.2 -sphinx==7.2.2 +sphinx==7.2.3 pydata-sphinx-theme==0.13.3 sphinx-design==0.5.0 sphinxcontrib-images==0.9.4 From 10c760765d812d7799b98f76170b44fbbd6bc912 Mon Sep 17 00:00:00 2001 From: Paul Tuemmler <136888490+ptuemmler@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:00:49 +0200 Subject: [PATCH 301/306] Add turbo colormap to local ColorMaps and GradientEditorItem (#2778) * Added turbo from https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html to default Gradients dict for GradientEditorItem. * Added turbo.csv to colors/maps for use as a local ColorMap. --- pyqtgraph/colors/maps/turbo.csv | 260 ++++++++++++++++++ pyqtgraph/graphicsItems/GradientEditorItem.py | 3 + 2 files changed, 263 insertions(+) create mode 100644 pyqtgraph/colors/maps/turbo.csv diff --git a/pyqtgraph/colors/maps/turbo.csv b/pyqtgraph/colors/maps/turbo.csv new file mode 100644 index 0000000000..aa70108497 --- /dev/null +++ b/pyqtgraph/colors/maps/turbo.csv @@ -0,0 +1,260 @@ +; turbo colormap, an improved rainbow colormap enabling high contrast and smooth visualization of data +; Published by Anton Mikhailov et. al. https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html +; SPDX-License-Identifier: Apache-2.0 +; +0.18995,0.07176,0.23217 +0.19483,0.08339,0.26149 +0.19956,0.09498,0.29024 +0.20415,0.10652,0.31844 +0.20860,0.11802,0.34607 +0.21291,0.12947,0.37314 +0.21708,0.14087,0.39964 +0.22111,0.15223,0.42558 +0.22500,0.16354,0.45096 +0.22875,0.17481,0.47578 +0.23236,0.18603,0.50004 +0.23582,0.19720,0.52373 +0.23915,0.20833,0.54686 +0.24234,0.21941,0.56942 +0.24539,0.23044,0.59142 +0.24830,0.24143,0.61286 +0.25107,0.25237,0.63374 +0.25369,0.26327,0.65406 +0.25618,0.27412,0.67381 +0.25853,0.28492,0.69300 +0.26074,0.29568,0.71162 +0.26280,0.30639,0.72968 +0.26473,0.31706,0.74718 +0.26652,0.32768,0.76412 +0.26816,0.33825,0.78050 +0.26967,0.34878,0.79631 +0.27103,0.35926,0.81156 +0.27226,0.36970,0.82624 +0.27334,0.38008,0.84037 +0.27429,0.39043,0.85393 +0.27509,0.40072,0.86692 +0.27576,0.41097,0.87936 +0.27628,0.42118,0.89123 +0.27667,0.43134,0.90254 +0.27691,0.44145,0.91328 +0.27701,0.45152,0.92347 +0.27698,0.46153,0.93309 +0.27680,0.47151,0.94214 +0.27648,0.48144,0.95064 +0.27603,0.49132,0.95857 +0.27543,0.50115,0.96594 +0.27469,0.51094,0.97275 +0.27381,0.52069,0.97899 +0.27273,0.53040,0.98461 +0.27106,0.54015,0.98930 +0.26878,0.54995,0.99303 +0.26592,0.55979,0.99583 +0.26252,0.56967,0.99773 +0.25862,0.57958,0.99876 +0.25425,0.58950,0.99896 +0.24946,0.59943,0.99835 +0.24427,0.60937,0.99697 +0.23874,0.61931,0.99485 +0.23288,0.62923,0.99202 +0.22676,0.63913,0.98851 +0.22039,0.64901,0.98436 +0.21382,0.65886,0.97959 +0.20708,0.66866,0.97423 +0.20021,0.67842,0.96833 +0.19326,0.68812,0.96190 +0.18625,0.69775,0.95498 +0.17923,0.70732,0.94761 +0.17223,0.71680,0.93981 +0.16529,0.72620,0.93161 +0.15844,0.73551,0.92305 +0.15173,0.74472,0.91416 +0.14519,0.75381,0.90496 +0.13886,0.76279,0.89550 +0.13278,0.77165,0.88580 +0.12698,0.78037,0.87590 +0.12151,0.78896,0.86581 +0.11639,0.79740,0.85559 +0.11167,0.80569,0.84525 +0.10738,0.81381,0.83484 +0.10357,0.82177,0.82437 +0.10026,0.82955,0.81389 +0.09750,0.83714,0.80342 +0.09532,0.84455,0.79299 +0.09377,0.85175,0.78264 +0.09287,0.85875,0.77240 +0.09267,0.86554,0.76230 +0.09320,0.87211,0.75237 +0.09451,0.87844,0.74265 +0.09662,0.88454,0.73316 +0.09958,0.89040,0.72393 +0.10342,0.89600,0.71500 +0.10815,0.90142,0.70599 +0.11374,0.90673,0.69651 +0.12014,0.91193,0.68660 +0.12733,0.91701,0.67627 +0.13526,0.92197,0.66556 +0.14391,0.92680,0.65448 +0.15323,0.93151,0.64308 +0.16319,0.93609,0.63137 +0.17377,0.94053,0.61938 +0.18491,0.94484,0.60713 +0.19659,0.94901,0.59466 +0.20877,0.95304,0.58199 +0.22142,0.95692,0.56914 +0.23449,0.96065,0.55614 +0.24797,0.96423,0.54303 +0.26180,0.96765,0.52981 +0.27597,0.97092,0.51653 +0.29042,0.97403,0.50321 +0.30513,0.97697,0.48987 +0.32006,0.97974,0.47654 +0.33517,0.98234,0.46325 +0.35043,0.98477,0.45002 +0.36581,0.98702,0.43688 +0.38127,0.98909,0.42386 +0.39678,0.99098,0.41098 +0.41229,0.99268,0.39826 +0.42778,0.99419,0.38575 +0.44321,0.99551,0.37345 +0.45854,0.99663,0.36140 +0.47375,0.99755,0.34963 +0.48879,0.99828,0.33816 +0.50362,0.99879,0.32701 +0.51822,0.99910,0.31622 +0.53255,0.99919,0.30581 +0.54658,0.99907,0.29581 +0.56026,0.99873,0.28623 +0.57357,0.99817,0.27712 +0.58646,0.99739,0.26849 +0.59891,0.99638,0.26038 +0.61088,0.99514,0.25280 +0.62233,0.99366,0.24579 +0.63323,0.99195,0.23937 +0.64362,0.98999,0.23356 +0.65394,0.98775,0.22835 +0.66428,0.98524,0.22370 +0.67462,0.98246,0.21960 +0.68494,0.97941,0.21602 +0.69525,0.97610,0.21294 +0.70553,0.97255,0.21032 +0.71577,0.96875,0.20815 +0.72596,0.96470,0.20640 +0.73610,0.96043,0.20504 +0.74617,0.95593,0.20406 +0.75617,0.95121,0.20343 +0.76608,0.94627,0.20311 +0.77591,0.94113,0.20310 +0.78563,0.93579,0.20336 +0.79524,0.93025,0.20386 +0.80473,0.92452,0.20459 +0.81410,0.91861,0.20552 +0.82333,0.91253,0.20663 +0.83241,0.90627,0.20788 +0.84133,0.89986,0.20926 +0.85010,0.89328,0.21074 +0.85868,0.88655,0.21230 +0.86709,0.87968,0.21391 +0.87530,0.87267,0.21555 +0.88331,0.86553,0.21719 +0.89112,0.85826,0.21880 +0.89870,0.85087,0.22038 +0.90605,0.84337,0.22188 +0.91317,0.83576,0.22328 +0.92004,0.82806,0.22456 +0.92666,0.82025,0.22570 +0.93301,0.81236,0.22667 +0.93909,0.80439,0.22744 +0.94489,0.79634,0.22800 +0.95039,0.78823,0.22831 +0.95560,0.78005,0.22836 +0.96049,0.77181,0.22811 +0.96507,0.76352,0.22754 +0.96931,0.75519,0.22663 +0.97323,0.74682,0.22536 +0.97679,0.73842,0.22369 +0.98000,0.73000,0.22161 +0.98289,0.72140,0.21918 +0.98549,0.71250,0.21650 +0.98781,0.70330,0.21358 +0.98986,0.69382,0.21043 +0.99163,0.68408,0.20706 +0.99314,0.67408,0.20348 +0.99438,0.66386,0.19971 +0.99535,0.65341,0.19577 +0.99607,0.64277,0.19165 +0.99654,0.63193,0.18738 +0.99675,0.62093,0.18297 +0.99672,0.60977,0.17842 +0.99644,0.59846,0.17376 +0.99593,0.58703,0.16899 +0.99517,0.57549,0.16412 +0.99419,0.56386,0.15918 +0.99297,0.55214,0.15417 +0.99153,0.54036,0.14910 +0.98987,0.52854,0.14398 +0.98799,0.51667,0.13883 +0.98590,0.50479,0.13367 +0.98360,0.49291,0.12849 +0.98108,0.48104,0.12332 +0.97837,0.46920,0.11817 +0.97545,0.45740,0.11305 +0.97234,0.44565,0.10797 +0.96904,0.43399,0.10294 +0.96555,0.42241,0.09798 +0.96187,0.41093,0.09310 +0.95801,0.39958,0.08831 +0.95398,0.38836,0.08362 +0.94977,0.37729,0.07905 +0.94538,0.36638,0.07461 +0.94084,0.35566,0.07031 +0.93612,0.34513,0.06616 +0.93125,0.33482,0.06218 +0.92623,0.32473,0.05837 +0.92105,0.31489,0.05475 +0.91572,0.30530,0.05134 +0.91024,0.29599,0.04814 +0.90463,0.28696,0.04516 +0.89888,0.27824,0.04243 +0.89298,0.26981,0.03993 +0.88691,0.26152,0.03753 +0.88066,0.25334,0.03521 +0.87422,0.24526,0.03297 +0.86760,0.23730,0.03082 +0.86079,0.22945,0.02875 +0.85380,0.22170,0.02677 +0.84662,0.21407,0.02487 +0.83926,0.20654,0.02305 +0.83172,0.19912,0.02131 +0.82399,0.19182,0.01966 +0.81608,0.18462,0.01809 +0.80799,0.17753,0.01660 +0.79971,0.17055,0.01520 +0.79125,0.16368,0.01387 +0.78260,0.15693,0.01264 +0.77377,0.15028,0.01148 +0.76476,0.14374,0.01041 +0.75556,0.13731,0.00942 +0.74617,0.13098,0.00851 +0.73661,0.12477,0.00769 +0.72686,0.11867,0.00695 +0.71692,0.11268,0.00629 +0.70680,0.10680,0.00571 +0.69650,0.10102,0.00522 +0.68602,0.09536,0.00481 +0.67535,0.08980,0.00449 +0.66449,0.08436,0.00424 +0.65345,0.07902,0.00408 +0.64223,0.07380,0.00401 +0.63082,0.06868,0.00401 +0.61923,0.06367,0.00410 +0.60746,0.05878,0.00427 +0.59550,0.05399,0.00453 +0.58336,0.04931,0.00486 +0.57103,0.04474,0.00529 +0.55852,0.04028,0.00579 +0.54583,0.03593,0.00638 +0.53295,0.03169,0.00705 +0.51989,0.02756,0.00780 +0.50664,0.02354,0.00863 +0.49321,0.01963,0.00955 +0.47960,0.01583,0.01055 diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index f508113206..7c67803d27 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -28,6 +28,9 @@ ('inferno', {'ticks': [(0.0, (0, 0, 3, 255)), (0.25, (87, 15, 109, 255)), (0.5, (187, 55, 84, 255)), (0.75, (249, 142, 8, 255)), (1.0, (252, 254, 164, 255))], 'mode': 'rgb'}), ('plasma', {'ticks': [(0.0, (12, 7, 134, 255)), (0.25, (126, 3, 167, 255)), (0.5, (203, 71, 119, 255)), (0.75, (248, 149, 64, 255)), (1.0, (239, 248, 33, 255))], 'mode': 'rgb'}), ('magma', {'ticks': [(0.0, (0, 0, 3, 255)), (0.25, (80, 18, 123, 255)), (0.5, (182, 54, 121, 255)), (0.75, (251, 136, 97, 255)), (1.0, (251, 252, 191, 255))], 'mode': 'rgb'}), + # turbo from https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html + ('turbo', {'ticks': [(0.0, (51, 27, 61, 255)), (0.125, (77, 110, 223, 255)), (0.25, (61, 185, 233, 255)), (0.375, (68, 238, 154, 255)), (0.5, (164, 250, 80, 255)), + (0.625, (235, 206, 76, 255)), (0.75, (247, 129, 55, 255)), (0.875, (206, 58, 32, 255)), (1.0, (119, 21, 19, 255))], 'mode': 'rgb'}), ]) def addGradientListToDocstring(): From 2f9a9a4e4ae9c7329ec97ae6d0cc158bf31ebae7 Mon Sep 17 00:00:00 2001 From: veractor <56646254+veractor@users.noreply.github.com> Date: Sun, 27 Aug 2023 00:25:54 +0800 Subject: [PATCH 302/306] Docs: Update Qt binding selection details (#2807) * Update python bindings import order in docs * Update docs to mention PYQTGRAPH_QT_LIB --- doc/source/getting_started/how_to_use.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/source/getting_started/how_to_use.rst b/doc/source/getting_started/how_to_use.rst index c957c565b4..84a0c2935e 100644 --- a/doc/source/getting_started/how_to_use.rst +++ b/doc/source/getting_started/how_to_use.rst @@ -81,18 +81,19 @@ PyQt and PySide PyQtGraph supports two popular python wrappers for the Qt library: PyQt and PySide. Both packages provide nearly identical APIs and functionality, but for various reasons (discussed elsewhere) you may prefer to use one package or the other. When -pyqtgraph is first imported, it automatically determines which library to use by making the fillowing checks: +pyqtgraph is first imported, if the environment variable ``PYQTGRAPH_QT_LIB`` is not set, it automatically determines which +library to use by making the following checks: -#. If PyQt5 is already imported, use that -#. Else, if PySide2 is already imported, use that +#. If PyQt6 is already imported, use that #. Else, if PySide6 is already imported, use that -#. Else, if PyQt6 is already imported, use that -#. Else, attempt to import PyQt5, PySide2, PySide6, PyQt6, in that order. +#. Else, if PyQt5 is already imported, use that +#. Else, if PySide2 is already imported, use that +#. Else, attempt to import PyQt6, PySide6, PyQt5, PySide2, in that order. If you have both libraries installed on your system and you wish to force pyqtgraph to use one or the other, simply make sure it is imported before pyqtgraph:: - import PySide2 ## this will force pyqtgraph to use PySide2 instead of PyQt5 + import PySide2 ## this will force pyqtgraph to use PySide2 instead of PyQt6 import pyqtgraph as pg From 391524cc37f347a67282d4d9436fb9e58a20b002 Mon Sep 17 00:00:00 2001 From: pijyoi Date: Sun, 27 Aug 2023 20:16:59 +0800 Subject: [PATCH 303/306] ImageItem: request for 256-entry lut (#2808) --- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 807781853f..9a90ceccdd 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -526,7 +526,7 @@ def render(self): if self.image.ndim == 2 or self.image.shape[2] == 1: self.lut = self._ensure_proper_substrate(self.lut, self._xp) if isinstance(self.lut, Callable): - lut = self._ensure_proper_substrate(self.lut(self.image), self._xp) + lut = self._ensure_proper_substrate(self.lut(self.image, 256), self._xp) else: lut = self.lut else: From f261280905a74f6cae4a43e39fa1732635b25c63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:35:59 -0700 Subject: [PATCH 304/306] Bump sphinx from 7.2.3 to 7.2.4 in /doc (#2809) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.2.3 to 7.2.4. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.2.3...v7.2.4) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 314f7fa003..0ed4d614a9 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.5.2 -sphinx==7.2.3 +sphinx==7.2.4 pydata-sphinx-theme==0.13.3 sphinx-design==0.5.0 sphinxcontrib-images==0.9.4 From 8495c1690ba0b1860c6ce5ba48ca79d9fd7941de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 06:56:49 -0700 Subject: [PATCH 305/306] Bump sphinx from 7.2.4 to 7.2.5 in /doc (#2813) --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 0ed4d614a9..a960cf9f6f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,5 +1,5 @@ PyQt6==6.5.2 -sphinx==7.2.4 +sphinx==7.2.5 pydata-sphinx-theme==0.13.3 sphinx-design==0.5.0 sphinxcontrib-images==0.9.4 From a10bdbcea9b341f407c12964ea85d2c6c5be6c42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 21:18:52 -0700 Subject: [PATCH 306/306] Bump actions/checkout from 3 to 4 (#2814) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 8 ++++---- .github/workflows/python-publish.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30087c143a..8c37e84893 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,7 +51,7 @@ jobs: qt-version: "PySide6-Essentials" steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -162,7 +162,7 @@ jobs: environment-file: .github/workflows/etc/environment-pyside.yml steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: conda-incubator/setup-miniconda@v2 with: miniforge-version: latest @@ -242,7 +242,7 @@ jobs: name: build wheel runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: @@ -257,7 +257,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1ab8d63eae..312b2ec77c 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: