On one side I have a tool/class WidgetFigures which lets you draw several figures on a GraphicsView. It does so by subclassing QGraphicsScene and by setting this scene to the GraphicsView object.
On the other side I have a tool/class ChartItem, which lets you plot several PlotDataItem's inside a pyqtgraph.GraphicsLayoutWidget object. The data plotted here is retrieved from an API.
These tools work perfectly fine separately.
My goal is to be able to draw the figures in the first tool on the top of the chart/s of the second tool. I have achieved this functionality as you can see in the following picture.
The problem is that when the plot changes, either when the range changes, or when the window -and therefore the plot- resizes, the figures on top won't move/resize accordingly. This can be seen in the two pictures below, where the figure -oval colored in green- is not highlighting the peak in the plot as the picture before.
Solutions that I've tried:
1 - Use a unique scene: did not work since I am not able to plot PlotDataItems on a subclassed scene. Specifically, axes are not being drawn in the first place, and the plotted data (included axes) can't be seen after.
2 - Detect the plot's ViewBox -or its QGraphicsRectItem- changes, create copies of these items -in the pictures these rects are highlighted in red-, and also apply the transformations -translations and scaling- happening on these items, to each figure added inside the rectangle. I didn't succeed with this one since the transformation applied on the figures is not matching the transformations on the plot's data. The code for this solution is added below.
3 - Instead of creating a QGraphicsRectItem as in solution 2, create some ViewBox, and instead of applying the transformation to each figure individually, just apply the new range to this viewbox. This solution would hide the PlotDataItem under it, I can't make this item transparent.
Here is the code for solution 2:
helper.py
import numpy as np
from PyQt5.QtCore import Qt, QObject, QPointF, pyqtSignal
from PyQt5.QtGui import QPen, QPainter, QTransform
from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, \
QGraphicsRectItem, QGraphicsPolygonItem, QGraphicsItem
import pyqtgraph as pg
def SMA(v, n):
i = 0
moving_averages = []
moving_averages.extend(v[:n-1])
while i < len(v) - n + 1:
window = v[i: i + n]
window_average = round(np.sum(window) / n, 2)
moving_averages.append(window_average)
i += 1
return moving_averages
class ChartItem:
plots: List[pg.PlotDataItem]
def __init__(self, widget: pg.GraphicsLayoutWidget):
self.setup_widgets(widget)
self.data = np.random.normal(size=(1, 100), scale=1)
self.paint(plot=1)
def setup_widgets(self, widget: pg.GraphicsLayoutWidget):
p1 = widget.addPlot(0, 0)
p2 = widget.addPlot(1, 0)
widget.ci.layout.setRowStretchFactor(0, 2) # row 0, stretch factor 2
widget.ci.layout.setRowStretchFactor(1, 1) # row 1, stretch factor 1
p2.setXLink(p1)
# get handle to x-axis 0
p2.getAxis('bottom').setStyle(showValues=False)
self.plots = [p1.plot(x=[], y=[]), p2.plot(x=[], y=[])]
def paint(self, plot: int = 1):
if plot == 1:
x = range(50)
y = self.data[0][:50]
else:
x = range(100)
y = self.data[0]
self.plots[0].setData(x=x, y=y)
self.plots[1].setData(x=x, y=SMA(y, 3))
class Widget(pg.GraphicsLayoutWidget):
sigPainted = pyqtSignal()
sigResized = pyqtSignal()
def __init__(self, parent, **kargs):
super(Widget, self).__init__(parent, **kargs)
def resizeEvent(self, ev):
super().resizeEvent(ev)
self.sigResized.emit()
def paintEvent(self, ev):
super().paintEvent(ev)
if ev.type() == ev.Type.Paint:
self.sigPainted.emit()
elif ev.type() != ev.Type.Timer():
print(ev.type())
class WidgetFigures:
window: QObject
def __init__(self, window=None):
self.window = window
self.chart = QGraphicsView(window.widget)
self.chart.setStyleSheet("background: transparent")
self.chart.setSceneRect(window.widget.sceneRect())
self.chart.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.chart.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.pHeight = {}
self.pWidth = {}
def resizeItem():
self.chart.setFixedSize(window.widget.size())
figures = [item for item in self.chart.items() if not isinstance(item, QGraphicsRectItem)]
widget_rects = [item for item in self.window.widget.items() if isinstance(item, QGraphicsRectItem)]
for n, rec in enumerate(widget_rects):
wid = rec.parentItem()
if hasattr(wid, 'viewRange'):
height = wid.scene().height()
width = wid.scene().width()
fHeight = height / self.pHeight.get(n, height)
self.pHeight[n] = height
fWidth = width / self.pWidth.get(n, width)
self.pWidth[n] = width
t = QTransform()
t.scale(fWidth, fHeight)
for fig in figures:
fig.setTransform(t)
def setRects():
widget_rects = [item for item in self.window.widget.items() if isinstance(item, QGraphicsRectItem)]
rects = [item for item in self.chart.items() if isinstance(item, QGraphicsRectItem)]
if len(rects) == 0:
rects = []
for rec in widget_rects:
rect = QGraphicsRectItem()
rect.setParentItem(rec.parentItem())
rect.setPen(QPen(Qt.GlobalColor.red, 2, Qt.SolidLine))
rects.append(rect)
self.chart.scene().addItem(rect)
for n, rec in enumerate(widget_rects):
rects[n].setRect(rec.sceneBoundingRect())
self.window.widget.sigPainted.connect(setRects)
self.window.widget.sigResized.connect(resizeItem)
self.setupScene(self.chart)
def setSceneRect(rect):
self.chart.setSceneRect(rect)
self.window.widget.sceneObj.sceneRectChanged.connect(setSceneRect)
def setupScene(self, obj):
sceneRect = self.window.widget.sceneObj.sceneRect()
self.scene = Scene(obj)
self.scene.setSceneRect(sceneRect)
obj.setScene(self.scene)
obj.setRenderHints(QPainter.Antialiasing)
class Scene(QGraphicsScene):
numItems: int
itemToDraw: Optional[QGraphicsPolygonItem]
parent: QObject
origPoint: QPointF
def __init__(self, parent=None):
QGraphicsScene.__init__(self, parent)
self.parent = parent
self.itemToDraw = None
def makeItemsControllable(self, areControllable: bool):
for item in self.items():
item.setFlag(QGraphicsItem.ItemIsSelectable, areControllable)
item.setFlag(QGraphicsItem.ItemIsMovable, areControllable)
def mousePressEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
self.origPoint = event.scenePos()
self.numItems = len(self.items())
print("Objects: {}".format(self.numItems))
super(Scene, self).mousePressEvent(event)
def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
self.itemToDraw = QGraphicsEllipseItem()
self.itemToDraw.setPen(QPen(Qt.GlobalColor.green, 3, Qt.SolidLine))
self.itemToDraw.setPos(self.origPoint)
self.itemToDraw.setRect(0, 0, event.scenePos().x() - self.origPoint.x(),
event.scenePos().y() - self.origPoint.y())
for item in self.items()[: None if not self.numItems else -self.numItems]:
self.removeItem(item)
self.addItem(self.itemToDraw)
example.py
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'example.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1024, 511)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
MainWindow.setSizePolicy(sizePolicy)
self.centralwidget = QtWidgets.QWidget(MainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
self.centralwidget.setSizePolicy(sizePolicy)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
self.verticalLayout_2.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth())
self.tabWidget.setSizePolicy(sizePolicy)
self.tabWidget.setAcceptDrops(True)
self.tabWidget.setToolTip("")
self.tabWidget.setTabShape(QtWidgets.QTabWidget.Triangular)
self.tabWidget.setMovable(False)
self.tabWidget.setObjectName("tabWidget")
self.tab = QtWidgets.QWidget()
self.tab.setObjectName("tab")
self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.tab)
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
self.widget = Widget(self.tab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth())
self.widget.setSizePolicy(sizePolicy)
self.widget.setObjectName("widget")
self.horizontalLayout_10.addWidget(self.widget)
self.verticalLayout_4 = QtWidgets.QVBoxLayout()
self.verticalLayout_4.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
self.verticalLayout_4.setContentsMargins(5, -1, 5, -1)
self.verticalLayout_4.setObjectName("verticalLayout_4")
self.pushButton = QtWidgets.QPushButton(self.tab)
self.pushButton.setObjectName("pushButton")
self.verticalLayout_4.addWidget(self.pushButton)
self.pushButton_2 = QtWidgets.QPushButton(self.tab)
self.pushButton_2.setObjectName("pushButton_2")
self.verticalLayout_4.addWidget(self.pushButton_2)
self.horizontalLayout_10.addLayout(self.verticalLayout_4)
self.tabWidget.addTab(self.tab, "")
self.tab_2 = QtWidgets.QWidget()
self.tab_2.setObjectName("tab_2")
self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.tab_2)
self.verticalLayout_5.setObjectName("verticalLayout_5")
self.tabWidget.addTab(self.tab_2, "")
self.tab_5 = QtWidgets.QWidget()
self.tab_5.setObjectName("tab_5")
self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.tab_5)
self.verticalLayout_7.setObjectName("verticalLayout_7")
self.tabWidget.addTab(self.tab_5, "")
self.horizontalLayout_3.addWidget(self.tabWidget)
self.verticalLayout_2.addLayout(self.horizontalLayout_3)
self.verticalLayout_2.setStretch(0, 5)
self.verticalLayout_3.addLayout(self.verticalLayout_2)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1024, 31))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
self.tabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton.setText(_translate("MainWindow", "Plot1"))
self.pushButton_2.setText(_translate("MainWindow", "Plot2"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("MainWindow", "Tab1"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("MainWindow", "Tab2"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_5), _translate("MainWindow", "Tab3"))
from helper import Widget, ChartItem, WidgetFigures
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
item = ChartItem(widget=ui.widget)
ui.pushButton.clicked.connect(lambda x: item.paint(1))
ui.pushButton_2.clicked.connect(lambda x: item.paint(2))
figures = WidgetFigures(window=ui)
MainWindow.show()
sys.exit(app.exec_())
I wonder if there's any way to achieve this functionality. Any help is highly appreciated.


