pyqtgraph: add crosshair on mouse_x graph_y - python

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_()

Related

PyQt widget hide show crash Layout Problem

I have a nested layout 4-levels of nesting
as in the attached photo
top layout is
MainHorLayout
|
|--QVBoxLayout_______________ Hosts CheckBoxes
|--AllfigVlayout_____________ Hosts figures
|-TopHlayoutfig_0_1_______Hosts profile_0,Profile_1 charts, downside labels
| |
| |-- TopVlayoutFig_0_Lbl (loaded with chart[0], chart[0].belowlabel)
| |
| |-- TopVlayoutFig_1_Lbl (loaded with chart[1], chart[1].belowlabel)
|
|-BotHlayoutfig_2_3_______same as TopHlayoutfig_0_1 but for bottom fig 2,3
|
|-- TopVlayoutFig_2_Lbl (loaded with chart[2], chart[2].belowlabel)
|
|-- TopVlayoutFig_3_Lbl (loaded with chart[2], chart[2].belowlabel)
I'm using check box to toggle the charts[i] visibale/ unvisible
start up behaviour is shown in phot 1
No problem with Charts[0] and charts2 as in photo 2
The problem when i'm trying to hid charts1 it hides charts[0] and 1
also trying to hide charts3 it hides both 2 and 3 please check photo 3
when i trying to show them again the dimensions of charts is not equall and non symetrical please check photo 4
as it shows when program starts
from functools import partial
from scipy.interpolate import make_interp_spline, BSpline
import sys
import matplotlib.pyplot as plt
import numpy as np
#from PyQt5.QtWidgets import QWidget
import matplotlib
matplotlib.use('QtAgg')
from PyQt6 import QtCore
from PyQt6.QtWidgets import QWidget,QApplication , QLabel, QSizePolicy, QFrame, QCheckBox, QVBoxLayout, QHBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from crosshairplt import CrossHairCursor
import mouse
import csv
NUMBERofProfiles = 4
class BelowFigLabel(QLabel):
def __init__(self):
super().__init__()
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
#sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
self.setMaximumHeight(60)
#self.setMaximumHeight(self.height()/2)
#sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
self.setSizePolicy(sizePolicy)
#self.setTextFormat(QtCore.Qt.TextFormat.RichText)
self.setScaledContents(True)
#self.label_1.setStyleSheet("font-family: Verdana,sans-serif;font: 87 10pt \"Arial Black\";\n""background-color: rgb(30,231,235);")#;font-size: 12px;height: 28px;width: 80px
self.setStyleSheet("font-family: Verdana,sans-serif; font: 87 12pt ;font-weight:Bold;background-color: #007EA7;color:#E9D2F4")
self.setFrameShape(QFrame.Shape.Box)
self.setFrameShadow(QFrame.Shadow.Sunken)
self.setLineWidth(2)
###############################
class Canvas(FigureCanvas):
def __init__(self, parent,Horizontal,Vertical,figname,subscribt):
self.parentx = parent
self.t = Horizontal
self.s = Vertical
self.figname = figname +f"_{subscribt}"
self.fig, self.ax = plt.subplots()
super().__init__(self.fig)
self.setParent(parent)# parent must be a QWidget
self.geo = self.parentx.geometry() # used to calculate the fig location
self.x = (self.t[-1] - self.t[0])/2 #initial zome distance axis rang/2
self.belowfiglbl = BelowFigLabel()
self.colSize = self.s.shape[0] # get column size
self.slic = [self.s[i][0] for i in range(self.colSize)]
#self.cursor = CrossHairCursor(self.ax,self.t,self.slic, self.belowfiglbl)
self.ax.plot(self.t, self.s)
self.ax.set(xlabel='time (s)', ylabel='Current (A))', title=self.figname)
self.ax.grid()
self.ax.grid(True,'both','x',color='r', linestyle='-' , linewidth=1)
self.ax.grid(True,'both','y',color='b', linestyle='--', linewidth=1)
self.ax.grid()
ticks = []
self.fig.canvas.mpl_connect('scroll_event', self.on_mouse_scroll)
#self.fig.canvas.mpl_connect('motion_notify_event', self.cursor.on_mouse_move)
#self.fig.canvas.mpl_connect('button_press_event', self.cursor.on_mouse_click)
#self.fig.canvas.mpl_connect('button_release_event', self.cursor.on_mouse_click)
"""
Matplotlib Script
"""
def on_mouse_scroll(self,event):
#self.geo = self.parentx.geometry()
#print(self.geo)
self.event = event
if self.event.inaxes:
if self.event.button == 'up':
max_x = self.event.xdata + self.x
min_x = self.event.xdata - self.x
#print("P= %0.3f min = %0.3f max = %0.3f x= %0.3f" %(self.event.xdata, min_x,max_x, self.x))
if max_x > (min_x+.002): # ADD .002 MARGIN COZ when both limits get equall the widget behalves in WRONG WAY
if min_x > self.t[0] and max_x < self.t[-1]:
self.ax.set_xlim(min_x,max_x)
self.x /=2
#print("in bound")
elif min_x <= self.t[0]:
#print(" -------bound")
self.ax.set_xlim(self.t[0],max_x)
self.x /=2
else:
#print(" ++++++++++bound")
self.ax.set_xlim(min_x,self.t[-1])
self.x /=2
elif self.event.button == 'down':
x_0 = self.ax.get_xlim()
#y_0 = self.ax.get_ylim()
min_x = x_0[0]-self.x
max_x = x_0[1]+self.x
if min_x >= self.t[0] and max_x <= self.t[-1]:
self.ax.set_xlim(min_x,max_x)
self.x *=2
elif min_x < self.t[0] and max_x <= self.t[-1]:
self.ax.set_xlim(self.t[0],max_x)
self.x *=2
elif min_x>= self.t[0] and max_x > self.t[-1]:
self.ax.set_xlim(min_x,self.t[-1])
self.x *=2
else:
self.ax.set_xlim(self.t[0],self.t[-1])
#self.fig.canvas.draw()
"""
Get new Transformed co-ordinates after Canvas redraw to have updated xdata and ydata transformed
"""
DataTransformedXtoCanvas,DataTransformedYtoCanvas = self.ax.transData.transform((self.event.xdata,self.event.ydata))
"""
- The mouse object co-ordinates are the physical display
- The event object Uses the Data Co-ordinates for event.xdata,event.ydata
- When we use the transData it tranform the data Co-ordinates into fig Co-ordinates
- The Fig's are hosted in GridLayout so each figure has it on position relative to parent widget window
- The parent window has its own relative position to physical dispaly
---- so to keep the mouse pointer pointing to the DATApOINT under zoom in/out
1- we first Transform Data Coordinates into fig coordinates using .transData.transform
2- add to the coordinates in (1) the canvas.geometry()'s topleft corner position relative to parent
self.geometry().left(), self.geometry().top()
3- add the parent TopLeft corner position relative to PHYSICAL DISPLAY
"""
CavasTopCornertoParent = self.geometry().top()
CavasLefCornertoParent = self.geometry().left()
ParentTopCornertoScreen = self.parentx.geometry().top()
ParentLeftCornertoScreen = self.parentx.geometry().left()
PointerPositionX = (DataTransformedXtoCanvas + CavasLefCornertoParent + ParentLeftCornertoScreen)
PointerPositionY = (DataTransformedYtoCanvas + CavasTopCornertoParent + ParentTopCornertoScreen)
mouse.move(PointerPositionX , PointerPositionY)
def on_mouse_click(self,event):
print(event.xdata,event.ydata)
print("geo = ", self.parentx.geometry())#fig.canvas.geometry())
inverted = self.ax.transData.inverted().transform((event.x,event.y))
print(f"inverted ={inverted}, screen xy ={(event.x,event.y)}")
class AppDemo(QWidget):
def __init__(self):
super().__init__()
self.resize(1600, 800)
#self.setStyleSheet("font: 75 10pt \"Monospac821 BT\";""background-color: rgb(120,190,220);")
self.setStyleSheet("font: 75 10pt Monospac821 BT;background-color: #003459")#rgb(120,190,220)")
self.setWindowTitle("Current Oscilography")
self.s=[]
self.Ismoth=[]
for i in range(NUMBERofProfiles):
with open(f"E:/OneDrive/011_APROJECTS/digital relay/code/GitHub/Python Application/SourceCode/MODBUSfolder/loadprofiles/tstdata/LP_{i}.csv", mode='r', newline='') as profile:
a = csv.reader(profile)
x = 0
CurrentTimeSamples = [] # list holding 5 col 4 for current and 1st is for time series
for row in a:
try:
xx =[float(i) for i in row ]
CurrentTimeSamples.append(xx)
except:
CurrentTimeSamples.append(row)
#CurrentTimeSamples is 5x801 list
CurrentTimeSamples = CurrentTimeSamples[1:] # Remove Header Line
CurrentOnly = [CurrentTimeSamples[i][1:] for i in range(len(CurrentTimeSamples))] # remove 1st Col which is time series
#Isolate the time series
timex = np.array([CurrentTimeSamples[i][0] for i in range(len(CurrentTimeSamples))])
#self.s.append(CurrentOnly)
#self.s[i]=np.array(self.s[i])
# convert to Numpy
CurrentOnly = np.array(CurrentOnly)
# evenly spaced 3200 points between Xmin and X
self.time = np.linspace(timex.min(), timex.max(), 3200)
# order 3 " Quadraic" Interpolation Equation
spl = make_interp_spline(timex, CurrentOnly, k=3)
# Application of the quadratic interpolation f
self.Ismoth.append(spl(self.time))
self.charts = []
for i in range(NUMBERofProfiles):
self.charts.append(Canvas(self,self.time,self.Ismoth[i],"Profile", i))
self.MainHorLayout = QHBoxLayout(self)
#self.MainHorLayout.setSpacing(0)
self.TopHlayoutfig_0_1 = QHBoxLayout()
#self.TopHlayoutfig_0_1.setSpacing(0)
self.TopHlayoutfig_0_1.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)#QtCore.Qt.AlignmentFlag.AlignLeft|
self.TopVlayoutFig_0_Lbl =QVBoxLayout()
self.TopVlayoutFig_0_Lbl.addWidget(self.charts[0])
self.TopVlayoutFig_0_Lbl.addWidget(self.charts[0].belowfiglbl)
self.TopVlayoutFig_1_Lbl =QVBoxLayout()
self.TopVlayoutFig_1_Lbl.addWidget(self.charts[1])
self.TopVlayoutFig_1_Lbl.addWidget(self.charts[1].belowfiglbl)
self.TopHlayoutfig_0_1.addLayout(self.TopVlayoutFig_0_Lbl)
self.TopHlayoutfig_0_1.addLayout(self.TopVlayoutFig_1_Lbl)
self.BotHlayoutfig_2_3 = QHBoxLayout()
self.BotHlayoutfig_2_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
self.BotVlayoutFig_2_Lbl =QVBoxLayout()
self.BotVlayoutFig_2_Lbl.addWidget(self.charts[2])
self.BotVlayoutFig_2_Lbl.addWidget(self.charts[2].belowfiglbl)
self.BotVlayoutFig_3_Lbl =QVBoxLayout()
self.BotVlayoutFig_3_Lbl.addWidget(self.charts[3])
self.BotVlayoutFig_3_Lbl.addWidget(self.charts[3].belowfiglbl)
self.BotHlayoutfig_2_3.addLayout(self.BotVlayoutFig_2_Lbl)
self.BotHlayoutfig_2_3.addLayout(self.BotVlayoutFig_3_Lbl)
self.AllfigVlayout = QVBoxLayout()
self.AllfigVlayout.addLayout(self.TopHlayoutfig_0_1)
self.AllfigVlayout.addLayout(self.BotHlayoutfig_2_3)
#self.AllfigVlayout.setSpacing(0)
self.MainHorLayout.addLayout(self.AllfigVlayout)
SideBarVLayout_main = QVBoxLayout()
LeftSidbarLabel = QLabel()
ChBoxesFrame = QFrame()
ChBoxesFrame.setStyleSheet("font-family: Verdana,sans-serif; font: 40 12pt ;font-weight:Bold;background-color: #30638E;color:#ffd166")
ChBoxesFrame.setContentsMargins(5,1,5,5)
ChBoxesFrame.setFrameShape(QFrame.Shape.Box)
ChBoxesFrame.setFrameShadow(QFrame.Shadow.Sunken)
ChBoxesFrame.setLineWidth(3)
ChBoxesFrame.setMaximumHeight(220)
ChBoxesFrame_Vlaouy = QVBoxLayout(ChBoxesFrame)
SelectActvProfilLabel = QLabel("SeLect Profiles", ChBoxesFrame)
SelectActvProfilLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop|QtCore.Qt.AlignmentFlag.AlignLeft)
SelectActvProfilLabel.setMaximumHeight(20)
ChBoxesFrame_Vlaouy.addWidget(SelectActvProfilLabel)
cc = QCheckBox()
self.ChkBox =[]
for i in range(NUMBERofProfiles):
self.ChkBox.append(QCheckBox(f"Profile_{i}",ChBoxesFrame))
self.ChkBox[i].setChecked(True)
self.ChkBox[i].clicked.connect(partial(self.chkBox_cliked,i))
ChBoxesFrame_Vlaouy.addWidget(self.ChkBox[i])
SideBarVLayout_main.addWidget(ChBoxesFrame)
LeftSidbarLabel.setMaximumWidth(180)
LeftSidbarLabel.setStyleSheet("font-family: Verdana,sans-serif; font: 40 12pt ;font-weight:Bold;background-color: #30638E;color:#EDAE49")
LeftSidbarLabel.setContentsMargins(10,5,5,5)
LeftSidbarLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop|QtCore.Qt.AlignmentFlag.AlignLeft)
LeftSidbarLabel.setFrameShape(QFrame.Shape.Box)
LeftSidbarLabel.setFrameShadow(QFrame.Shadow.Sunken)
LeftSidbarLabel.setLineWidth(3)
SideBarVLayout_main.addWidget(LeftSidbarLabel)
self.MainHorLayout.addLayout(SideBarVLayout_main, )
def chkBox_cliked(self,ckboxNum):
#for i in range(NUMBERofProfiles):
if self.ChkBox[ckboxNum].isChecked() == False:
self.charts[ckboxNum].belowfiglbl.setVisible(False)
#deleteLater()
self.charts[ckboxNum].setVisible(False)
else:
self.charts[ckboxNum].setVisible(True)# = Canvas(self,self.time,self.Ismoth[ckboxNum],"Profile",ckboxNum)
self.charts[ckboxNum].belowfiglbl.setVisible(True)
#self.MainHorLayout.update()
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = AppDemo()
demo.show()#Maximized()
sys.exit(app.exec())

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.

QChart Line exceed the boundary when the window first start to show?

In my app, i need to draw a line when user move mouse. But when the chart window first showing and move mouse, the line exceed boundary. The i resize window, it work ok. i don't know why first start window it not work fine, it's seem that there is no different the first and after.
The code is:
import random
from PyQt5.QtChart import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class DemoChar(QChartView):
def __init__(self):
super().__init__()
self.setRenderHint(QPainter.Antialiasing)
self.chart = QChart()
self.chart.setTitle('Demo')
self.chart.setAnimationOptions(QChart.SeriesAnimations)
self.setChart(self.chart)
self.lineItem = QGraphicsLineItem(self.chart)
series = QLineSeries(name="random serie")
series.setPointsVisible(True)
for i in range(20):
series << QPointF(0.1 * i, random.uniform(-10, 10))
self.chart.addSeries(series)
self.chart.createDefaultAxes()
axis_x, axis_y = self.chart.axes()
#get axis min, max value to calculate position
self.min_x, self.min_y = axis_x.min(), axis_y.min()
self.max_x, self.max_y = axis_x.max(), axis_y.max()
def resizeEvent(self, event):
super().resizeEvent(event)
self.point_bottom = self.chart.mapToPosition(QPointF(self.min_x, self.min_y))
self.point_top = self.chart.mapToPosition(QPointF(self.min_x, self.max_y))
line = self.lineItem.line()
line.setLine(line.x1(), self.point_bottom.y(), line.x2(), self.point_top.y() )
self.lineItem.setLine(line)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
pt = self.chart.mapToValue(event.pos())
if not (self.min_x <= pt.x() <= self.max_x):
self.lineItem.hide()
return
for marker in self.chart.legend().markers():
if marker.series().isVisible():
points = marker.series().pointsVector() #type:list
for point in points:
left_idx = points.index(point)
right_point = points[left_idx + 1]
if point.x() <= pt.x() <= right_point.x():
left_delta = pt.x() - point.x()
right_delta = right_point.x() - pt.x()
if left_delta < right_delta:
pos = self.chart.mapToPosition(point)
self.lineItem.setLine(pos.x(), self.point_bottom.y(), pos.x(), self.point_top.y() )
self.lineItem.show()
break
app = QApplication([])
demo = DemoChar()
demo.show()
app.exec()
First start and move mouse like this.
Then resize window, it work ok.
There seems to be a gap between the painted and the resizeEvent, probably a bug. A workaround is to modify the size by code:
# ...
self.min_x, self.min_y = axis_x.min(), axis_y.min()
self.max_x, self.max_y = axis_x.max(), axis_y.max()
self.resize(self.size() + QSize(1, 1))

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

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()

Matplotlib: scrolling plot

I'm new to Python and I want to implement a scrolling plot for a very long time series data. I've found an example from Matplotlib as follows.
http://scipy-cookbook.readthedocs.io/items/Matplotlib_ScrollingPlot.html
When I run the example from the link, I found every time I scroll the plot and release the scrollbar, the scrollbar returns to the beginning. Want to scroll to the next position? I need to start to scroll from the beginning again.
I want to understand why it happens and how to fix it.
Here's an improved version of the example. (Disclaimer: I started digging into it half an hour ago, never before used wx/matplotlib scrollbars so there might be a much better solution.)
The path I took: first I checked the wx scroll events, then found out that the canvas is FigureCanvasWxAgg derived from wxPanel, inheriting wxWindow methods. There you may find the scroll position handling methods GetScrollPos and SetScrollPos.
from numpy import arange, sin, pi, float, size
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
from matplotlib.figure import Figure
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self,parent, id, 'scrollable plot',
style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER,
size=(800, 400))
self.panel = wx.Panel(self, -1)
self.fig = Figure((5, 4), 75)
self.canvas = FigureCanvasWxAgg(self.panel, -1, self.fig)
self.scroll_range = 400
self.canvas.SetScrollbar(wx.HORIZONTAL, 0, 5,
self.scroll_range)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas, -1, wx.EXPAND)
self.panel.SetSizer(sizer)
self.panel.Fit()
self.init_data()
self.init_plot()
self.canvas.Bind(wx.EVT_SCROLLWIN, self.OnScrollEvt)
def init_data(self):
# Generate some data to plot:
self.dt = 0.01
self.t = arange(0,5,self.dt)
self.x = sin(2*pi*self.t)
# Extents of data sequence:
self.i_min = 0
self.i_max = len(self.t)
# Size of plot window:
self.i_window = 100
# Indices of data interval to be plotted:
self.i_start = 0
self.i_end = self.i_start + self.i_window
def init_plot(self):
self.axes = self.fig.add_subplot(111)
self.plot_data = \
self.axes.plot(self.t[self.i_start:self.i_end],
self.x[self.i_start:self.i_end])[0]
def draw_plot(self):
# Update data in plot:
self.plot_data.set_xdata(self.t[self.i_start:self.i_end])
self.plot_data.set_ydata(self.x[self.i_start:self.i_end])
# Adjust plot limits:
self.axes.set_xlim((min(self.t[self.i_start:self.i_end]),
max(self.t[self.i_start:self.i_end])))
self.axes.set_ylim((min(self.x[self.i_start:self.i_end]),
max(self.x[self.i_start:self.i_end])))
# Redraw:
self.canvas.draw()
def update_scrollpos(self, new_pos):
self.i_start = self.i_min + new_pos
self.i_end = self.i_min + self.i_window + new_pos
self.canvas.SetScrollPos(wx.HORIZONTAL, new_pos)
self.draw_plot()
def OnScrollEvt(self, event):
evtype = event.GetEventType()
if evtype == wx.EVT_SCROLLWIN_THUMBTRACK.typeId:
pos = event.GetPosition()
self.update_scrollpos(pos)
elif evtype == wx.EVT_SCROLLWIN_LINEDOWN.typeId:
pos = self.canvas.GetScrollPos(wx.HORIZONTAL)
self.update_scrollpos(pos + 1)
elif evtype == wx.EVT_SCROLLWIN_LINEUP.typeId:
pos = self.canvas.GetScrollPos(wx.HORIZONTAL)
self.update_scrollpos(pos - 1)
elif evtype == wx.EVT_SCROLLWIN_PAGEUP.typeId:
pos = self.canvas.GetScrollPos(wx.HORIZONTAL)
self.update_scrollpos(pos - 10)
elif evtype == wx.EVT_SCROLLWIN_PAGEDOWN.typeId:
pos = self.canvas.GetScrollPos(wx.HORIZONTAL)
self.update_scrollpos(pos + 10)
else:
print "unhandled scroll event, type id:", evtype
class MyApp(wx.App):
def OnInit(self):
self.frame = MyFrame(parent=None,id=-1)
self.frame.Show()
self.SetTopWindow(self.frame)
return True
if __name__ == '__main__':
app = MyApp()
app.MainLoop()
You may adjust e.g. the increments for PAGEUP/PAGEDOWN if you feel it too slow.
Also if you wish, the events can be handled separately setting up the specific event handlers instead of their collection EVT_SCROLLWIN, then instead of if/elifs there will be OnScrollPageUpEvt etc.

Categories

Resources