Can I share the crosshair with two graph in pyqtgraph (pyqt5) - python

I am implemented two graphs in pyqt5 with pyqtgraph, and want to share cursor between two graphs.
I have a program result at the following:
And I want to share the crosshair like:
Can pyqtgraph do it ? Many thanks.
Following is my code (update), it will show 2 graph and the cursor will only allow move in graph2 only. However, I want the cursor can move and get data from graph1 and graph2.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
from pyqtgraph import MultiPlotWidget
class GUI_create_main_window(QWidget):
def __init__(self):
super().__init__()
main_layout = QVBoxLayout(self)
self.plot1 = pg.PlotWidget()
main_layout.addWidget(self.plot1)
self. plot2 = pg.PlotWidget()
main_layout.addWidget(self.plot2)
self.draw_cursor()
self.setLayout(main_layout)
self.show()
#hair cross event
def eventFilter(self, source, event):
try:
if (event.type() == QtCore.QEvent.MouseMove and
source is self.plot2.viewport()):
pos = event.pos()
if self.plot2.sceneBoundingRect().contains(pos):
mousePoint = self.vb.mapSceneToView(pos)
self.vLine.setPos(mousePoint.x())
self.hLine.setPos(mousePoint.y())
return QtGui.QWidget.eventFilter(self, source, event)
except Exception as e:
traceback.print_exc()
err = sys.exc_info()[1]
print(str(err))
def draw_cursor(self):
#cross hair
self.vLine = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen('k', width=1))
self.hLine = pg.InfiniteLine(angle=0, movable=False, pen=pg.mkPen('k', width=1), label='{value:0.1f}',
labelOpts={'position':0.98, 'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)})
self.plot2.addItem(self.vLine, ignoreBounds=True)
self.plot2.addItem(self.hLine, ignoreBounds=True)
self.vb = self.plot2.plotItem.vb
#set mouse event
self.plot2.setMouseTracking(True)
self.plot2.viewport().installEventFilter(self)
if __name__ == '__main__':
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
app = QApplication(sys.argv)
gui = GUI_create_main_window()
currentExitCode = app.exec_()

I know that it is kind of late but after struggling with similar problem I came up with a solution.
First of all we can capture mouse movements on pyqtgraph plots by installing 'SignalProxy' on their scene.
For example:
self.proxy = pg.SignalProxy(self.p.scene().sigMouseMoved, rateLimit=120, slot=self.mouseMoved)
This will call mouseMoved method on when mouse move signal is emitted.
We can get mouse position converted to scene position with
def mouseMoved(self, evt):
pos = evt[0]
mousePoint = self.p.vb.mapSceneToView(pos)
x, y = int(mousePoint.x()), int(mousePoint.y())
now we can set vLine pos to x and hLine pos to y on if we can have access to other graph's plot we can also set their vLine pos to x (since x axis should match)
we can link their x axis together (same movement on x axis is applied on other graph) by:
plot.setXLink(other_plot)
Side notes:
I set up two graphs by subclassing pyqtgraph's GraphicsLayoutWidget and adding plots to them individually I'm sure there should be a way of doing it in a single subclass but doing something like...
self.addPlot(title="Plot 1")
self.addPlot(title="Plot 2")
...creates two plots next to each other rather than stacked on top of
each other - we want to only link vLines anyways.
we can access other graph by something like this:
self.top_graph = TopGraph()
self.bottom_graph = BottomGraph()
self.top_graph.other_graph = self.bottom_graph # set bottom graph in top graph to access it in bottom graph
self.bottom_graph.other_graph = self.top_graph # set top graph in bottom graph to access it in top graph
maybe we can do global variables as well but I'm not sure that it is a good practice.
So it all adds up to:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pyqtgraph as pg
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
main_widget = QWidget()
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
self.top_graph = TopGraph()
self.bottom_graph = BottomGraph()
self.top_graph.other_graph = self.bottom_graph # set bottom graph in top graph to access it in bottom graph
self.bottom_graph.other_graph = self.top_graph # set top graph in bottom graph to access it in top graph
self.top_graph.p.setXLink(self.bottom_graph.p)
#example plot
x = range(-50, 51)
y1 = [i**2 for i in x]
y2 = [-i**2 for i in x]
self.top_graph.p.plot(x, y1, pen=pg.mkPen("r", width=3))
self.bottom_graph.p.plot(x, y2, pen=pg.mkPen("g", width=3))
splitter = QSplitter(Qt.Vertical)
splitter.addWidget(self.top_graph)
splitter.addWidget(self.bottom_graph)
main_layout.addWidget(splitter)
class TopGraph(pg.GraphicsLayoutWidget):
def __init__(self):
super().__init__()
self.p = self.addPlot()
self.p.hideAxis("bottom")
self.vLine = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen('k', width=1))
self.hLine = pg.InfiniteLine(angle=0, movable=False, pen=pg.mkPen('k', width=1), label='{value:0.1f}',
labelOpts={'position':0.98, 'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)})
self.p.addItem(self.vLine, ignoreBounds=True)
self.p.addItem(self.hLine, ignoreBounds=True)
self.proxy = pg.SignalProxy(self.p.scene().sigMouseMoved, rateLimit=120, slot=self.mouseMoved)
def mouseMoved(self, evt):
pos = evt[0]
if self.p.sceneBoundingRect().contains(pos):
self.hLine.show() # show this graph's h line since we are now in control
mousePoint = self.p.vb.mapSceneToView(pos)
x, y = int(mousePoint.x()), int(mousePoint.y())
self.vLine.setPos(x)
self.hLine.setPos(y)
self.other_graph.vLine.setPos(x)
self.other_graph.hLine.hide() # hide other graphs h line since we don't controll it here
class BottomGraph(pg.GraphicsLayoutWidget):
def __init__(self):
super().__init__()
self.p = self.addPlot()
self.vLine = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen('k', width=1))
self.hLine = pg.InfiniteLine(angle=0, movable=False, pen=pg.mkPen('k', width=1), label='{value:0.1f}',
labelOpts={'position':0.98, 'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)})
self.p.addItem(self.vLine, ignoreBounds=True)
self.p.addItem(self.hLine, ignoreBounds=True)
self.proxy = pg.SignalProxy(self.p.scene().sigMouseMoved, rateLimit=120, slot=self.mouseMoved)
def mouseMoved(self, evt):
pos = evt[0]
if self.p.sceneBoundingRect().contains(pos):
self.hLine.show() # show this graph's h line since we are now in control
mousePoint = self.p.vb.mapSceneToView(pos)
x, y = int(mousePoint.x()), int(mousePoint.y())
self.vLine.setPos(x)
self.hLine.setPos(y)
self.other_graph.vLine.setPos(x)
self.other_graph.hLine.hide() # hide other graphs h line since we don't controll it here
def main():
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
app = QApplication([])
window = MainWindow()
window.setMinimumSize(600, 400)
window.show()
app.exec()
if __name__ == "__main__":
main()

Related

How can I make the animation to rotate a widget in PyQt5? [duplicate]

I am a new to pyqt and need help with rotating the label. I am confused and cannot understand how to rotate the whole widget on a specific angle. Not the content of the widget, but the widget itself. I am searching for the solution but cannot find anything.
A QWidget does not support rotation, but a workaround is to insert the widget into a QGraphicsProxyWidget and add it to a QGraphicsScene, and then rotate the QGraphicsProxyWidget that visually generates the same widget rotation effect.
from PyQt5 import QtCore, QtGui, QtWidgets
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
label = QtWidgets.QLabel("Stack Overflow", alignment=QtCore.Qt.AlignCenter)
graphicsview = QtWidgets.QGraphicsView()
scene = QtWidgets.QGraphicsScene(graphicsview)
graphicsview.setScene(scene)
proxy = QtWidgets.QGraphicsProxyWidget()
proxy.setWidget(label)
proxy.setTransformOriginPoint(proxy.boundingRect().center())
scene.addItem(proxy)
slider = QtWidgets.QSlider(minimum=0, maximum=359, orientation=QtCore.Qt.Horizontal)
slider.valueChanged.connect(proxy.setRotation)
label_text = QtWidgets.QLabel(
"{}°".format(slider.value()), alignment=QtCore.Qt.AlignCenter
)
slider.valueChanged.connect(
lambda value: label_text.setText("{}°".format(slider.value()))
)
slider.setValue(45)
w = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(graphicsview)
lay.addWidget(slider)
lay.addWidget(label_text)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
As #eyllanesc correctly explains, there's no "widget rotation" support in Qt (as in most standard frameworks).
There are a couple of tricks on your hand, though.
"Simple" label (not using a QLabel)
That's the "simple" solution. Since you're talking about a "label", that can be implemented using some math.
The biggest advantage in this approach is that the size hint is "simple", meaning that it's only based on the text contents (as in QFontMetrics.boundingRect()), and whenever the main font, text or alignment is changed, the size hint reflects them.
While it supports multi-line labels, the biggest problem about this approach comes in place if you need to use rich text, though; a QTextDocument can be used instead of a standard string, but that would require a more complex implementation for size hint computing.
from math import radians, sin, cos
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class AngledLabel(QtWidgets.QWidget):
_alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
def __init__(self, text='', angle=0, parent=None):
super(AngledLabel, self).__init__(parent)
self._text = text
self._angle = angle % 360
# keep radians of the current angle *and* its opposite; we're using
# rectangles to get the overall area of the text, and since they use
# right angles, that opposite is angle + 90
self._radians = radians(-angle)
self._radiansOpposite = radians(-angle + 90)
def alignment(self):
return self._alignment
def setAlignment(self, alignment):
# text alignment might affect the text size!
if alignment == self._alignment:
return
self._alignment = alignment
self.setMinimumSize(self.sizeHint())
def angle(self):
return self._angle
def setAngle(self, angle):
# the angle clearly affects the overall size
angle %= 360
if angle == self._angle:
return
self._angle = angle
# update the radians to improve optimization of sizeHint and paintEvent
self._radians = radians(-angle)
self._radiansOpposite = radians(-angle + 90)
self.setMinimumSize(self.sizeHint())
def text(self):
return self._text
def setText(self, text):
if text == self._text:
return
self._text = text
self.setMinimumSize(self.sizeHint())
def sizeHint(self):
# get the bounding rectangle of the text
rect = self.fontMetrics().boundingRect(QtCore.QRect(), self._alignment, self._text)
# use trigonometry to get the actual size of the rotated rectangle
sinWidth = abs(sin(self._radians) * rect.width())
cosWidth = abs(cos(self._radians) * rect.width())
sinHeight = abs(sin(self._radiansOpposite) * rect.height())
cosHeight = abs(cos(self._radiansOpposite) * rect.height())
return QtCore.QSize(cosWidth + cosHeight, sinWidth + sinHeight)
def minimumSizeHint(self):
return self.sizeHint()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
textRect = self.fontMetrics().boundingRect(
QtCore.QRect(), self._alignment, self._text)
width = textRect.width()
height = textRect.height()
# we have to translate the painting rectangle, and that depends on which
# "angle sector" the current angle is
if self._angle <= 90:
deltaX = 0
deltaY = sin(self._radians) * width
elif 90 < self._angle <= 180:
deltaX = cos(self._radians) * width
deltaY = sin(self._radians) * width + sin(self._radiansOpposite) * height
elif 180 < self._angle <= 270:
deltaX = cos(self._radians) * width + cos(self._radiansOpposite) * height
deltaY = sin(self._radiansOpposite) * height
else:
deltaX = cos(self._radiansOpposite) * height
deltaY = 0
qp.translate(.5 - deltaX, .5 - deltaY)
qp.rotate(-self._angle)
qp.drawText(self.rect(), self._alignment, self._text)
class TestWindow(QtWidgets.QWidget):
def __init__(self):
super(TestWindow, self).__init__()
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.randomizeButton = QtWidgets.QPushButton('Randomize!')
layout.addWidget(self.randomizeButton, 0, 0, 1, 3)
self.randomizeButton.clicked.connect(self.randomize)
layout.addWidget(QtWidgets.QLabel('Standard label'), 1, 0)
text = 'Some text'
layout.addWidget(QtWidgets.QLabel(text), 1, 2)
self.labels = []
for row, angle in enumerate([randrange(360) for _ in range(8)], 2):
angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
layout.addWidget(angleLabel, row, 0)
label = AngledLabel(text, angle)
layout.addWidget(label, row, 2)
self.labels.append((angleLabel, label))
separator = QtWidgets.QFrame()
separator.setFrameShape(separator.VLine|separator.Sunken)
layout.addWidget(separator, 1, 1, layout.rowCount() - 1, 1)
def randomize(self):
for angleLabel, label in self.labels:
angle = randrange(360)
angleLabel.setText(str(angle))
label.setAngle(angle)
self.adjustSize()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = TestWindow()
w.show()
sys.exit(app.exec_())
QGraphicsView implementation
I would also like to expand the solution proposed by eyllanesc, as it is more modular and allows to use "any" widget; unfortunately, while his answer works as expected, I'm afraid that it's an answer that is just valid "for the sake of the argument".
From the graphical point of view, the obvious issues are the QGraphicsView visual hints (borders and background). But, since we're talking about widgets that might have to be inserted in a graphical interface, the size (and its hint[s]) require some care.
The main advantage of this approach is that almost any type of widget can be added to the interface, but due to the nature of per-widget size policy and QGraphicsView implementations, if the content of the "rotated" widget changes, perfect drawing will always be something hard to achieve.
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class AngledObject(QtWidgets.QGraphicsView):
_angle = 0
def __init__(self, angle=0, parent=None):
super(AngledObject, self).__init__(parent)
# to prevent the graphics view to draw its borders or background, set the
# FrameShape property to 0 and a transparent background
self.setFrameShape(0)
self.setStyleSheet('background: transparent')
self.setScene(QtWidgets.QGraphicsScene())
# ignore scroll bars!
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
def angle(self):
return self._angle
def setAngle(self, angle):
angle %= 360
if angle == self._angle:
return
self._angle = angle
self._proxy.setTransform(QtGui.QTransform().rotate(-angle))
self.adjustSize()
def resizeEvent(self, event):
super(AngledObject, self).resizeEvent(event)
# ensure that the scene is fully visible after resizing
QtCore.QTimer.singleShot(0, lambda: self.centerOn(self.sceneRect().center()))
def sizeHint(self):
return self.scene().itemsBoundingRect().size().toSize()
def minimumSizeHint(self):
return self.sizeHint()
class AngledLabel(AngledObject):
def __init__(self, text='', angle=0, parent=None):
super(AngledLabel, self).__init__(angle, parent)
self._label = QtWidgets.QLabel(text)
self._proxy = self.scene().addWidget(self._label)
self._label.setStyleSheet('background: transparent')
self.setAngle(angle)
self.alignment = self._label.alignment
def setAlignment(self, alignment):
# text alignment might affect the text size!
if alignment == self._label.alignment():
return
self._label.setAlignment(alignment)
self.setMinimumSize(self.sizeHint())
def text(self):
return self._label.text()
def setText(self, text):
if text == self._label.text():
return
self._label.setText(text)
self.setMinimumSize(self.sizeHint())
class AngledButton(AngledObject):
def __init__(self, text='', angle=0, parent=None):
super(AngledButton, self).__init__(angle, parent)
self._button = QtWidgets.QPushButton(text)
self._proxy = self.scene().addWidget(self._button)
self.setAngle(angle)
class TestWindow(QtWidgets.QWidget):
def __init__(self):
super(TestWindow, self).__init__()
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.randomizeButton = QtWidgets.QPushButton('Randomize!')
layout.addWidget(self.randomizeButton, 0, 0, 1, 3)
self.randomizeButton.clicked.connect(self.randomize)
layout.addWidget(QtWidgets.QLabel('Standard label'), 1, 0)
text = 'Some text'
layout.addWidget(QtWidgets.QLabel(text), 1, 2)
self.labels = []
for row, angle in enumerate([randrange(360) for _ in range(4)], 2):
angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
layout.addWidget(angleLabel, row, 0)
label = AngledLabel(text, angle)
layout.addWidget(label, row, 2)
self.labels.append((angleLabel, label))
for row, angle in enumerate([randrange(360) for _ in range(4)], row + 1):
angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
layout.addWidget(angleLabel, row, 0)
label = AngledButton('Button!', angle)
layout.addWidget(label, row, 2)
self.labels.append((angleLabel, label))
separator = QtWidgets.QFrame()
separator.setFrameShape(separator.VLine|separator.Sunken)
layout.addWidget(separator, 1, 1, layout.rowCount() - 1, 1)
def randomize(self):
for angleLabel, label in self.labels:
angle = randrange(360)
angleLabel.setText(str(angle))
label.setAngle(angle)
self.adjustSize()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = TestWindow()
w.show()
sys.exit(app.exec_())
As you can see, the "randomize" functions have very different results. While the second approach allows using more complex widgets, the first one better reacts to contents changes.

PyQtgraph - Draw ROI by mouse click & drag

I would like to draw ROI's by click and drag events in the PlotWidget. The issue is that several click interactions are already reserved for the PlotWidget, second it is hard to tell the right position of the mouse in the PlotWidget - especially when the image scale has been changed or if the window scale has been changed.
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from PyQt5 import Qt
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
import vtk, sys
import numpy as np
from PIL import Image
class GUI:
def __init__(self):
self.init_gui()
def proxyWidget(self, item, width=None, height=None):
proxy = QtGui.QGraphicsProxyWidget()
if(height != None):
height = item.sizeHint().height() if height==None else height
item.setMaximumHeight(height)
if(width!=None):
width = item.sizeHint().width() if width==None else width
item.setMaximumWidth(width)
proxy.setWidget(item)
return proxy
def init_gui(self, win_height=800, win_width=1800):
pg.setConfigOptions(imageAxisOrder='row-major')
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
self.w = pg.GraphicsWindow(size=(win_width,win_height), border=True)
self.img = pg.ImageItem()
self.list_imgs = QtGui.QListWidget()
self.btn_Del_Mark = QtGui.QPushButton('Del Mark')
self.btn_MarkPed = QtGui.QPushButton('Mark ped')
self.lbl_list1 = QtGui.QLabel("List Images")
self.lbl_list2 = QtGui.QLabel("List Markings")
self.list_imgs = QtGui.QListWidget()
self.list_marks = QtGui.QListWidget()
self.layout = QtGui.QGridLayout()
self.w.setLayout(self.layout)
#self.w_3d = pg.GraphicsWindow()
self.vtkWidget = QVTKRenderWindowInteractor()
#self.w_3d.addItem(self.proxyWidget(self.vtkWidget))
self.vtkWidget.Initialize()
self.vtkWidget.Start()
self.ren = vtk.vtkRenderer()
self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
# Create source
source = vtk.vtkSphereSource()
source.SetCenter(0, 0, 0)
source.SetRadius(5.0)
# Create a mapper
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(source.GetOutputPort())
# Create an actor
actor = vtk.vtkActor()
actor.SetMapper(mapper)
self.ren.AddActor(actor)
self.ren.ResetCamera()
self.iren.Initialize()
self.iren.Start()
path = "/home/brain/uni/frustum-pointnets/dataset/KITTI/object/testing/image_2/000000.png"
imgdata = Image.open(path)
self.imgArr = np.array(imgdata)
#ToDo: undistort Image if neccessary
self.img.setImage(self.imgArr)
#self.vbLayout = self.w.addLayout(row=0, col=3, rowspan=10, colspan=20)
imageGraph = pg.PlotWidget(name='Signalgraph')
self.vb = imageGraph.plotItem.vb
self.lbl_list1.setAlignment(QtCore.Qt.AlignCenter)
self.lbl_list2.setAlignment(QtCore.Qt.AlignCenter)
self.vb.setAspectLocked()
self.vb.addItem(self.img)
self.vb.invertY(True)
self.vb.setMaximumSize(int(7/10.*win_width), int(9/20.*win_height))
self.layout.addWidget(imageGraph, 1 , 3, 10, 20)
self.layout.addWidget(self.vtkWidget , 11, 3, 10, 20)
self.layout.addWidget(self.lbl_list1 , 0, 1, 1, 1)
self.layout.addWidget(self.lbl_list2 , 0, 2, 1, 1)
self.layout.addWidget(self.list_imgs , 1, 1, 20,1)
self.layout.addWidget(self.list_marks, 1, 2, 20,1)
sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.lbl_list1.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.lbl_list2.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.list_imgs.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(18/20.*win_height))
self.list_marks.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(18/20.*win_height))
self.list_imgs.setMaximumWidth(int(1./10.*win_width))
self.list_marks.setMaximumWidth(int(1./10.*win_width))
self.vtkWidget.show()
if __name__ == "__main__":
app = QtGui.QApplication([])
guiobj = GUI()
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
I would like to start drawing the ROI by mouse click and stop drawing by mouse release... every hint would be helpful. Please consider that the content of the PlotWidget is drag-able and that it might need to be frozen while drawing a ROI.
EDIT:
I have tried to overwrite temporary the click events with the following lines, but somehow clickevents seem to be triggered somewhere else, since my functions do not get called...
def on_btn_MarkPed(self):
#self.vb.setMouseEnabled(x=False, y=False)
self.creatRoiByMouse("Pedestrian")
def on_btn_MarkCycl(self):
self.creatRoiByMouse("Cyclist")
def on_btn_MarkVehicle(self):
self.creatRoiByMouse("Vehicle")
def creatRoiByMouse(self, class2Mark):
self.img.mousePressEvent = self.ImgMousePressEvent
self.img.mouseReleaseEvent = self.ImgMouseReleaseEvent
def ImgMousePressEvent(self, event):
print(event)
pass
#
#
def ImgMouseReleaseEvent(self, event):
print(event)
pass
I know this post is old, but in case someone else finds it someday I thought I'd post my solution. It's inelegant, but it works for me. In my application, I have a button labeled "draw" that calls a function that overwrites the mouse drag event temporarily to simply draw a box instead. The overwritten mouse drag event restores the native mouse drag event with the finish signal. This code also overwrites the escape key to cancel the draw if pressed after pushing my draw button but before drawing. In my code, imageArrayItem is an existing imageItem in a plot widget, and Dialog is a QDialog containing the plot widget.
def clickDraw(self):
app.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CrossCursor))
imageArrayItem.getViewBox().setMouseMode(ViewBox.RectMode)
imageArrayItem.getViewBox().rbScaleBox.setPen(fn.mkPen((255, 255, 255), width = 1))
imageArrayItem.getViewBox().rbScaleBox.setBrush(fn.mkBrush(255, 255, 255, 100))
def mouseDragEvent(ev, axis = None): # This is a modified version of the original mouseDragEvent function in pyqtgraph.ViewBox
ev.accept() # accept all buttons
dif = (ev.pos() - ev.lastPos()) * -1
mouseEnabled = np.array(imageArrayItem.getViewBox().state['mouseEnabled'], dtype = np.float)
mask = mouseEnabled.copy()
if ev.button() & QtCore.Qt.LeftButton:
if imageArrayItem.getViewBox().state['mouseMode'] == ViewBox.RectMode:
if ev.isFinish():
QtCore.QTimer.singleShot(0, self.restoreCursor)
imageArrayItem.getViewBox().rbScaleBox.hide()
ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(ev.pos()))
ax = imageArrayItem.getViewBox().childGroup.mapRectFromParent(ax)
imageArrayItem.getViewBox().mouseDragEvent = temp # reset to original mouseDragEvent
imageArrayItem.getViewBox().setMouseMode(ViewBox.PanMode)
else:
imageArrayItem.getViewBox().updateScaleBox(ev.buttonDownPos(), ev.pos()) # update shape of scale box
elif ev.button() & QtCore.Qt.MidButton: # allow for panning with middle mouse button
tr = dif*mask
tr = imageArrayItem.getViewBox().mapToView(tr) - imageArrayItem.getViewBox().mapToView(Point(0,0))
x = tr.x() if mask[0] == 1 else None
y = tr.y() if mask[1] == 1 else None
imageArrayItem.getViewBox()._resetTarget()
if x is not None or y is not None:
imageArrayItem.getViewBox().translateBy(x=x, y=y)
imageArrayItem.getViewBox().sigRangeChangedManually.emit(imageArrayItem.getViewBox().state['mouseEnabled'])
def keyPressE_mouseDrag(event): # Override "esc" key to cancel draw
if event.key() == QtCore.Qt.Key_Escape:
QtCore.QTimer.singleShot(0, self.restoreCursor)
imageArrayItem.getViewBox().rbScaleBox.hide()
imageArrayItem.getViewBox().mouseDragEvent = temp # reset to original mouseDragEvent
imageArrayItem.getViewBox().setMouseMode(ViewBox.PanMode)
else:
QtWidgets.QDialog.keyPressEvent(Dialog, event)
Dialog.keyPressEvent = keyPressE_mouseDrag
temp = imageArrayItem.getViewBox().mouseDragEvent # save original mouseDragEvent for later
imageArrayItem.getViewBox().mouseDragEvent = mouseDragEvent # set to modified mouseDragEvent

pyqtgraph: add crosshair on mouse_x graph_y

I want to add a vertical line following the mouse_x position (working) and a horizontal line following the curve (and not mouse_y). In the pyqtgraph crosshair example, it shows how to add a crosshair following the mouse_x and mouse_y position. But that does not help that much.
The following code sets the vertical line position to mouse_x postion. But i dont know how to set the horizontal line's postion to the curves current y position (depending on where the mouse x position is).
data1 = 10000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
def mouseMoved(evt):
pos = evt[0]
if p1.sceneBoundingRect().contains(pos):
mousePoint = vb.mapSceneToView(pos)
index = int(mousePoint.x())
if index > 0 and index < len(data1):
label.setText("<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y1=%0.1f</span>, <span style='color: green'>y2=%0.1f</span>" % (mousePoint.x(), data1[index], data2[index]))
vLine.setPos(mousePoint.x()) # here i set the vertical line's position to mouse_x position
#hLinePos = vb.mapToView( vLine.pos() )
hLine.setPos(data1[mousePoint.x()]) # <-- how do i set this horizontal line so it is kind of snaped to the curve
Maybe this crosshair widget can point you in the right direction
from PyQt4 import QtCore, QtGui
import sys
import numpy as np
import pyqtgraph as pg
import random
"""Crosshair Plot Widget Example"""
class CrosshairPlotWidget(QtGui.QWidget):
"""Scrolling plot with crosshair"""
def __init__(self, parent=None):
super(CrosshairPlotWidget, self).__init__(parent)
# Use for time.sleep (s)
self.FREQUENCY = .025
# Use for timer.timer (ms)
self.TIMER_FREQUENCY = self.FREQUENCY * 1000
self.LEFT_X = -10
self.RIGHT_X = 0
self.x_axis = np.arange(self.LEFT_X, self.RIGHT_X, self.FREQUENCY)
self.buffer = int((abs(self.LEFT_X) + abs(self.RIGHT_X))/self.FREQUENCY)
self.data = []
self.crosshair_plot_widget = pg.PlotWidget()
self.crosshair_plot_widget.setXRange(self.LEFT_X, self.RIGHT_X)
self.crosshair_plot_widget.setLabel('left', 'Value')
self.crosshair_plot_widget.setLabel('bottom', 'Time (s)')
self.crosshair_color = (196,220,255)
self.crosshair_plot = self.crosshair_plot_widget.plot()
self.layout = QtGui.QGridLayout()
self.layout.addWidget(self.crosshair_plot_widget)
self.crosshair_plot_widget.plotItem.setAutoVisible(y=True)
self.vertical_line = pg.InfiniteLine(angle=90)
self.horizontal_line = pg.InfiniteLine(angle=0, movable=False)
self.vertical_line.setPen(self.crosshair_color)
self.horizontal_line.setPen(self.crosshair_color)
self.crosshair_plot_widget.setAutoVisible(y=True)
self.crosshair_plot_widget.addItem(self.vertical_line, ignoreBounds=True)
self.crosshair_plot_widget.addItem(self.horizontal_line, ignoreBounds=True)
self.crosshair_update = pg.SignalProxy(self.crosshair_plot_widget.scene().sigMouseMoved, rateLimit=60, slot=self.update_crosshair)
self.start()
def plot_updater(self):
"""Updates data buffer with data value"""
self.data_point = random.randint(1,101)
if len(self.data) >= self.buffer:
del self.data[:1]
self.data.append(float(self.data_point))
self.crosshair_plot.setData(self.x_axis[len(self.x_axis) - len(self.data):], self.data)
def update_crosshair(self, event):
"""Paint crosshair on mouse"""
coordinates = event[0]
if self.crosshair_plot_widget.sceneBoundingRect().contains(coordinates):
mouse_point = self.crosshair_plot_widget.plotItem.vb.mapSceneToView(coordinates)
index = mouse_point.x()
if index > self.LEFT_X and index <= self.RIGHT_X:
self.crosshair_plot_widget.setTitle("<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y=%0.1f</span>" % (mouse_point.x(), mouse_point.y()))
self.vertical_line.setPos(mouse_point.x())
self.horizontal_line.setPos(mouse_point.y())
def start(self):
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.plot_updater)
self.timer.start(self.get_timer_frequency())
def get_crosshair_plot_layout(self):
return self.layout
def get_timer_frequency(self):
return self.TIMER_FREQUENCY
if __name__ == '__main__':
# Create main application window
app = QtGui.QApplication([])
app.setStyle(QtGui.QStyleFactory.create("Cleanlooks"))
mw = QtGui.QMainWindow()
mw.setWindowTitle('Crosshair Plot Example')
# Create and set widget layout
# Main widget container
cw = QtGui.QWidget()
ml = QtGui.QGridLayout()
cw.setLayout(ml)
mw.setCentralWidget(cw)
# Create crosshair plot
crosshair_plot = CrosshairPlotWidget()
ml.addLayout(crosshair_plot.get_crosshair_plot_layout(),0,0)
mw.show()
## Start Qt event loop unless running in interactive mode or using pyside.
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

Insert VTK into the right spot of layout

I am trying to put the VTK 3D scene widget into the right spot of pyqtgraphics.GraphicsLayout(). But either the scene does not appear if i am not assigning the window as parent or if I do - the scene does not adapt to the free area, it is just floating in the upper left corner.
I am currently struggling to find a solution to put a viewbox and vtkrenderer into the same window.
One of my attempts was to assign the self.w (see below this paragraph)- which is my main window as a parent to the renderer but then I don't know how to tell the renderer to place itself in the lower right corner of the window instead of floating in the upper left corner - which also leads to overlapping of other elements in the window.
Creating new window and assigning this window as parent
self.vtkWidget = QVTKRenderWindowInteractor(self.w_3d)
Using refered window as parent -> leads to floating and overlapping rendered scene
self.vtkWidget = QVTKRenderWindowInteractor(self.w)
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from PyQt5 import Qt
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
import vtk, sys
class GUI:
def __init__(self):
self.init_gui()
def proxyWidget(self, item, width=None, height=None):
proxy = QtGui.QGraphicsProxyWidget()
if(height != None):
height = item.sizeHint().height() if height==None else height
item.setMaximumHeight(height)
if(width!=None):
width = item.sizeHint().width() if width==None else width
item.setMaximumWidth(width)
proxy.setWidget(item)
return proxy
def init_gui(self, win_height=800, win_width=1800):
#self.w = self
#self.w.scene().sigMouseClicked.connect(self.mousePressEvent) #mouseMoveEvent
#self.w.scene().sigMouseMoved.connect(self.mouseMoveEvent)
pg.setConfigOptions(imageAxisOrder='row-major')
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
self.w = pg.GraphicsWindow(size=(win_width,win_height), border=True)
self.img = pg.ImageItem()
self.list_imgs = QtGui.QListWidget()
self.btn_Del_Mark = QtGui.QPushButton('Del Mark')
self.btn_MarkPed = QtGui.QPushButton('Mark ped')
self.lbl_list1 = QtGui.QLabel("List Images")
self.lbl_list2 = QtGui.QLabel("List Markings")
self.list_imgs = QtGui.QListWidget()
self.list_marks = QtGui.QListWidget()
self.layout = pg.GraphicsLayout()
self.w_3d = pg.GraphicsWindow()
self.vb = pg.ViewBox()
self.lbl_list1.setAlignment(QtCore.Qt.AlignCenter)
self.lbl_list2.setAlignment(QtCore.Qt.AlignCenter)
self.vb.setAspectLocked()
self.vb.addItem(self.img)
self.vb.invertY(True)
self.vtkWidget = QVTKRenderWindowInteractor(self.w_3d)
self.w_3d.addItem(self.proxyWidget(self.vtkWidget))
self.vtkWidget.Initialize()
self.vtkWidget.Start()
self.ren = vtk.vtkRenderer()
self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
# Create source
source = vtk.vtkSphereSource()
source.SetCenter(0, 0, 0)
source.SetRadius(5.0)
# Create a mapper
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(source.GetOutputPort())
# Create an actor
actor = vtk.vtkActor()
actor.SetMapper(mapper)
self.ren.AddActor(actor)
self.ren.ResetCamera()
self.iren.Initialize()
self.iren.Start()
self.vtkWidget.show()
self.layout.addItem(self.vb , 1, 3, 20, 20)
self.layout.addItem(self.proxyWidget(self.lbl_list1 , width=(int(1./10.*win_width)), height=(int(0.9/20.*win_height))), 0,1,1,1)
self.layout.addItem(self.proxyWidget(self.lbl_list2 , width=(int(1./10.*win_width)), height=(int(0.9/20.*win_height))), 0,2,1,1)
self.layout.addItem(self.proxyWidget(self.list_imgs , width=(int(1./10.*win_width))), 1,1,20,1)
self.layout.addItem(self.proxyWidget(self.list_marks , width=(int(1./10.*win_width))), 1,2,20,1)
self.w.addItem(self.layout)
if __name__ == "__main__":
app = QtGui.QApplication([])
guiobj = GUI()
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
EDIT:
Currently I have solved it by having two windows - after several days of failed attempts. But even this looks crap - since the rendered scene floats in the window without reaction to window resize events...
I would like to have those two windows - in one:
One of the failed attempts was as follows - but then pyqt acquires space in the layout without showing the renderer scene in the window... - just empty space:
self.vtkWidget = QVTKRenderWindowInteractor() #No Parent
#...see rest of code in the section above with exception of the following 2 lines
self.layout.addItem(self.proxyWidget(self.vtkWidget), 10, 3, 10, 20)
self.vtkWidget.show()
I was able to solve it - I have replaced the QGraphicsLayout with QGridlayout and removed all proxywidget-wrappers and I found a workaround for adding a viewbox by using the plotwidget.
I just need now to remove the axes - but this is secondary
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from PyQt5 import Qt
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
import vtk, sys
import numpy as np
from PIL import Image
class GUI:
def __init__(self):
self.init_gui()
def proxyWidget(self, item, width=None, height=None):
proxy = QtGui.QGraphicsProxyWidget()
if(height != None):
height = item.sizeHint().height() if height==None else height
item.setMaximumHeight(height)
if(width!=None):
width = item.sizeHint().width() if width==None else width
item.setMaximumWidth(width)
proxy.setWidget(item)
return proxy
def init_gui(self, win_height=800, win_width=1800):
pg.setConfigOptions(imageAxisOrder='row-major')
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
self.w = pg.GraphicsWindow(size=(win_width,win_height), border=True)
self.img = pg.ImageItem()
self.list_imgs = QtGui.QListWidget()
self.btn_Del_Mark = QtGui.QPushButton('Del Mark')
self.btn_MarkPed = QtGui.QPushButton('Mark ped')
self.lbl_list1 = QtGui.QLabel("List Images")
self.lbl_list2 = QtGui.QLabel("List Markings")
self.list_imgs = QtGui.QListWidget()
self.list_marks = QtGui.QListWidget()
self.layout = QtGui.QGridLayout()
self.w.setLayout(self.layout)
#self.w_3d = pg.GraphicsWindow()
self.vtkWidget = QVTKRenderWindowInteractor()
#self.w_3d.addItem(self.proxyWidget(self.vtkWidget))
self.vtkWidget.Initialize()
self.vtkWidget.Start()
self.ren = vtk.vtkRenderer()
self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
# Create source
source = vtk.vtkSphereSource()
source.SetCenter(0, 0, 0)
source.SetRadius(5.0)
# Create a mapper
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(source.GetOutputPort())
# Create an actor
actor = vtk.vtkActor()
actor.SetMapper(mapper)
self.ren.AddActor(actor)
self.ren.ResetCamera()
self.iren.Initialize()
self.iren.Start()
path = "/home/brain/uni/frustum-pointnets/dataset/KITTI/object/testing/image_2/000000.png"
imgdata = Image.open(path)
self.imgArr = np.array(imgdata)
#ToDo: undistort Image if neccessary
self.img.setImage(self.imgArr)
#self.vbLayout = self.w.addLayout(row=0, col=3, rowspan=10, colspan=20)
imageGraph = pg.PlotWidget(name='Signalgraph')
self.vb = imageGraph.plotItem.vb
self.lbl_list1.setAlignment(QtCore.Qt.AlignCenter)
self.lbl_list2.setAlignment(QtCore.Qt.AlignCenter)
self.vb.setAspectLocked()
self.vb.addItem(self.img)
self.vb.invertY(True)
self.vb.setMaximumSize(int(7/10.*win_width), int(9/20.*win_height))
self.layout.addWidget(imageGraph, 1 , 3, 10, 20)
self.layout.addWidget(self.vtkWidget , 11, 3, 10, 20)
self.layout.addWidget(self.lbl_list1 , 0, 1, 1, 1)
self.layout.addWidget(self.lbl_list2 , 0, 2, 1, 1)
self.layout.addWidget(self.list_imgs , 1, 1, 20,1)
self.layout.addWidget(self.list_marks, 1, 2, 20,1)
sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.lbl_list1.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.lbl_list2.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.list_imgs.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(18/20.*win_height))
self.list_marks.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(18/20.*win_height))
self.list_imgs.setMaximumWidth(int(1./10.*win_width))
self.list_marks.setMaximumWidth(int(1./10.*win_width))
self.vtkWidget.show()
if __name__ == "__main__":
app = QtGui.QApplication([])
guiobj = GUI()
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

How to aesthetically show a generic number of axes in matplotlib?

I want to do a simple GUI that allows the user to add or remove traces from a plot for any number of traces. It looks like this:
The problems I'm having:
I don't know how to make the axes not to superpose with each other for a generic number of plots.
When I plot more than one trace, and then delete all but one, there are two axes showing for some reason. There should always be one axis per trace being shown.
Is there a way to fix these issues? You can find my code below. The only function that should be changed is update_canvas(), I believe. To try it out, just modify the list name_vars in the main with the number of variables you want. The rest of the example code is self-contained.
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(ApplicationWindow, self).__init__(parent)
global name_vars
self.x = np.array([1,2,3,4,5])
self.y = np.random.random((5, len(name_vars)))
self.num_vars = np.size(self.y,1)
self.name_vars = name_vars
self.tags_on = [0] * self.num_vars
self.colors = ['#1F77B4','#FF7F0E','#2CA02C','#D62728','#9467BD',
'#8C564B','#E377C2','#F7F7F7','#BCBD22','#17BECF']
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
canvas = FigureCanvas(Figure(figsize=(10, 10)))
self.canvas_ax = canvas.figure.subplots()
self.canvas_ax.set_xlabel("Time")
self.canvas_ax_twin = []
self.list_tags = QtWidgets.QComboBox(self)
for name in self.name_vars:
self.list_tags.addItem(name)
button_add = QtWidgets.QPushButton('Add', self)
button_remove = QtWidgets.QPushButton('Remove', self)
button_add.clicked.connect(self.add_plot)
button_remove.clicked.connect(self.remove_plot)
layout = QtWidgets.QGridLayout(self._main)
layout.addWidget(canvas, 0, 0)
dropdown_layout = QtWidgets.QHBoxLayout()
dropdown_layout.addWidget(self.list_tags)
dropdown_layout.addWidget(button_add)
dropdown_layout.addWidget(button_remove)
layout.addLayout(dropdown_layout, 1, 0)
self.show()
def add_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 1
self.update_canvas()
def remove_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 0
self.update_canvas()
def update_canvas(self):
# Delete all traces
self.canvas_ax.clear()
[i.clear() for i in self.canvas_ax_twin]
self.canvas_ax_twin = []
num_plots = 0
for ii in range(self.num_vars):
if self.tags_on[ii] == 1:
# If it's not the first trace, create a twin axis
if num_plots != 0:
self.canvas_ax_twin.append(self.canvas_ax.twinx())
self.canvas_ax_twin[-1].plot(self.x, self.y[:,ii], self.colors[num_plots])
self.canvas_ax_twin[-1].set_ylabel(self.name_vars[ii])
self.canvas_ax_twin[-1].yaxis.label.set_color(self.colors[num_plots])
self.canvas_ax_twin[-1].tick_params(axis='y', colors=self.colors[num_plots])
num_plots += 1
# If it's the first trace, use the original axis
else:
self.canvas_ax.plot(self.x, self.y[:,ii], self.colors[num_plots])
self.canvas_ax.set_ylabel(self.name_vars[ii])
self.canvas_ax.yaxis.label.set_color(self.colors[num_plots])
self.canvas_ax.tick_params(axis='y', colors=self.colors[num_plots])
num_plots += 1
# Show the final plot
self.canvas_ax.figure.canvas.draw()
if __name__ == '__main__':
# Edit the number of elements in name_vars to try the code
name_vars = ['V1','V2','V3','V4']
app = QtWidgets.QApplication([])
ex = ApplicationWindow()
ex.show()
app.exec_()
I would suggest to separate the logic from the actual plotting. This makes it easier to follow through. This solves the second question about not removing all axes.
The question about not letting the axes superimpose may be solved by setting the position of additional twin axes to some distance from the axes, depending on how many axes you have.
ax.spines["right"].set_position(("axes", 1+(n-1)*0.1))
where n is the axes number starting from 0. The main axes (n=0) should be excluded, and the first axes will stay at position 1. Further axes are positionned in steps of 0.1.
Then it makes sense to also adjust the right margin of the main axes to give enough space for the extra spines.
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None, name_vars=[]):
super(ApplicationWindow, self).__init__(parent)
self.x = np.array([1,2,3,4,5])
self.y = np.random.random((5, len(name_vars)))
self.num_vars = np.size(self.y,1)
self.name_vars = name_vars
self.tags_on = [0] * self.num_vars
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
self.figure = Figure(figsize=(10, 10))
canvas = FigureCanvas(self.figure)
self.left = self.figure.subplotpars.left
self.right = self.figure.subplotpars.right
self.canvas_ax = canvas.figure.subplots()
self.canvas_ax.set_xlabel("Time")
self.axes = [self.canvas_ax]
self.list_tags = QtWidgets.QComboBox(self)
for name in self.name_vars:
self.list_tags.addItem(name)
button_add = QtWidgets.QPushButton('Add', self)
button_remove = QtWidgets.QPushButton('Remove', self)
button_add.clicked.connect(self.add_plot)
button_remove.clicked.connect(self.remove_plot)
layout = QtWidgets.QGridLayout(self._main)
layout.addWidget(canvas, 0, 0)
dropdown_layout = QtWidgets.QHBoxLayout()
dropdown_layout.addWidget(self.list_tags)
dropdown_layout.addWidget(button_add)
dropdown_layout.addWidget(button_remove)
layout.addLayout(dropdown_layout, 1, 0)
self.show()
def add_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 1
self.update_canvas()
def remove_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 0
self.update_canvas()
def create_nth_axes(self, n, dataset):
if n == 0:
ax = self.canvas_ax
else:
ax = self.canvas_ax.twinx()
ax.spines["right"].set_position(("axes", 1+(n-1)*0.1))
for direction in ["left", "bottom", "top"]:
ax.spines[direction].set_visible(False)
# adjust subplotparams to make space for new axes spine
new_right = (self.right-self.left)/(1+(n-1)*0.1)+self.left
self.figure.subplots_adjust(right=new_right)
color = next(self.canvas_ax._get_lines.prop_cycler)['color']
ax.set_ylabel(self.name_vars[dataset], color=color)
ax.plot(self.x, self.y[:,dataset], color=color)
return ax
def clear_canvas(self):
# Clear main axes
self.canvas_ax.clear()
# clear and remove other axes
for ax in self.axes[1:]:
ax.clear()
ax.remove()
self.axes = [self.canvas_ax]
self.figure.subplots_adjust(right=0.9)
def update_canvas(self):
self.clear_canvas()
k = 0
for i, tag in enumerate(self.tags_on):
if tag:
ax = self.create_nth_axes(k, i)
if k > 0:
self.axes.append(ax)
k += 1
self.canvas_ax.figure.canvas.draw()
if __name__ == '__main__':
# Edit the number of elements in name_vars to try the code
name_vars = ['V1','V2','V3','V4']
app = QtWidgets.QApplication([])
ex = ApplicationWindow(name_vars=name_vars)
ex.show()
app.exec_()

Categories

Resources