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())
Related
I'm trying to get the intersection of a blue and green QPolygons, and the % that this makes up of the blue QPolygon (see image below).
I've created the polygons as follows:
import PyQt5
from PyQt5 import QtWidgets,QtGui,QtCore
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys, math
class MyWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.pen1 = QtGui.QPen(QtGui.QColor(20,0,250)) # set lineColor
self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) # set lineColor
self.pen2 = QtGui.QPen(QtGui.QColor(0,250,0)) # set lineColor
self.pen.setWidth(3) # set lineWidth
self.brush = QtGui.QBrush(QtGui.QColor(255,255,255,10)) # set fillColor
coords = [PyQt5.QtCore.QPointF(640.0, 334.0), PyQt5.QtCore.QPointF(626.0, 462.0), PyQt5.QtCore.QPointF(782.0, 509.0), PyQt5.QtCore.QPointF(807.0, 373.0), PyQt5.QtCore.QPointF(807.0, 333.0)]
greenpolygoncoords = [PyQt5.QtCore.QPointF(698.0, 373.0), PyQt5.QtCore.QPointF(690.0, 433.0), PyQt5.QtCore.QPointF(757.0, 376.0), PyQt5.QtCore.QPointF(713.0, 351.0), PyQt5.QtCore.QPointF(713.0, 351.0), PyQt5.QtCore.QPointF(698.0, 373.0)]
self.bluepolygon = QPolygonF(coords)
self.greenpolygon =QPolygonF(greenpolygoncoords)
self.intersection = self.bluepolygon.intersected(self.greenpolygon)
print("overlap = "+str(self.intersection.size()/self.bluepolygon.size()))
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setPen(self.pen1)
painter.setBrush(self.brush)
painter.drawPolygon(self.bluepolygon) #bluepolygon = Blue
painter.setPen(self.pen)
painter.drawPolygon(self.intersection) #Intersection = black
painter.setPen(self.pen2)
painter.drawPolygon(self.greenpolygon) # greenpolygon = Green
app = QtWidgets.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
However, when I try to calculate the overlap between the two polygons, as a percentage of the blue polygon, I get a wrong answer (it says that the overlap is equal to 1, however, the green polygon definately does not make up 100% of the blue polygon).
overlap = self.intersection.size()/self.bluepolygon.size()
When looking at the sizes calculated by QPolygon.size(), it becomes apparent that the QPolygon.size() returns an incorrect size of the polygon.
print("size of blue polygon = "+str(self.bluepolygon.size()))
print("size of greenpolygon = "+str(self.greenpolygon.size()))
print("size of intersection = "+str(self.intersection.size()))
returns:
size of blue polygon = 5
size of greenpolygon = 6
size of intersection = 5
Apparently for some reason for PyQt5, the blue polygon is smaller than the green polygon. How come?
What am I doing wrong?
The size() method does not return the area of the polygon but the number of sides or vertices. If you want to calculate the area then you must implement it using the standard method:
def calculate_area(qpolygon):
area = 0
for i in range(qpolygon.size()):
p1 = qpolygon[i]
p2 = qpolygon[(i + 1) % qpolygon.size()]
d = p1.x() * p2.y() - p2.x() * p1.y()
area += d
return abs(area) / 2
And then you evaluate what you want:
print(
"overlap = "
+ str(calculate_area(self.intersection) / calculate_area(self.bluepolygon))
)
Output:
overlap = 0.0962288941619438
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))
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_()
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_()
I implementing custom chart. But I stucked with mouse hitting detection with QPainterPath.
I tried with graphicsitem's shape(), boundingRect(). but that only checks rough shape of boundary.
I want to check mouse hit system with exact position on QPainterPath path instance. But seems to no api like that functionality.
My app's QGraphicsScene is set with same coordinate with QGraphicsView in view's resizeEvent().
scene: MyScene = self.scene()
scene.setSceneRect(self.rect().x(), self.rect().y(),
self.rect().width(), self.rect().height())
At the same time, my plot QGraphicsItem scales by QTransform.
plot: QGraphicsItem = scene.plot
trans = QTransform()
data = plot.df['data']
data = data - data.min()
data_max = data.max()
data_min = data.min()
trans.scale(self.width() / len(data),
self.height() / (data_max - data_min))
plot.trans = trans
plot.setTransform(trans)
And in the MyScene, add rect item mouse_rec. So, I check mouse_rec and plot item's path with mouse_rec.collidesWithPath(path)
It just works only with original path.
Here is all code. Just copy and paste, you could run it.
Red plot is original path and yellow plot is scaled path. Mouse hit check is only works with red plot...
import numpy
import pandas
from PyQt5 import QtGui
from PyQt5.QtCore import Qt, QRectF, QRect
from PyQt5.QtGui import QRadialGradient, QGradient, QPen, QPainterPath, QTransform, QPainter, QColor
from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QGraphicsSceneMouseEvent, QGraphicsItem, \
QStyleOptionGraphicsItem, QWidget, QGraphicsRectItem
class MyItem(QGraphicsItem):
def __init__(self, df, parent=None):
QGraphicsItem.__init__(self, parent)
self.num = 1
self.df = df
self.path = QPainterPath()
self.trans = QTransform()
self.cached = False
self.printed = False
self.setZValue(0)
def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionGraphicsItem', widget: QWidget = ...):
data = self.df['data']
data = data - data.min()
data_max = data.max()
data_min = data.min()
if not self.cached:
for i in range(data.size - 1):
self.path.moveTo(i, data[i])
self.path.lineTo(i+1, data[i+1])
self.cached = True
pen = QPen(Qt.white)
pen.setCosmetic(True)
painter.setPen(pen)
painter.drawRect(0, 0, data.size, data_max - data_min)
pen.setColor(Qt.yellow)
painter.setPen(pen)
painter.drawPath(self.path)
if not self.printed:
rec_item = self.scene().addPath(self.path, QPen(Qt.red))
rec_item.setZValue(-10)
self.printed = True
def boundingRect(self):
data = self.df['data']
data_max = data.max()
data_min = data.min()
return QRectF(0, 0, data.size, data_max - data_min)
class MyScene(QGraphicsScene):
def __init__(self, data, parent=None):
QGraphicsScene.__init__(self, parent)
self.data = data
self.mouse_rect = QGraphicsRectItem()
self.plot: MyItem(data) = None
self.bounding_rect = QGraphicsRectItem()
self.setBackgroundBrush(QColor('#14161f'))
self.addItem(self.bounding_rect)
self.printed = False
def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent'):
print()
print("rec rect : ", self.mouse_rect.rect())
print("Scene rect : ", self.sceneRect())
print("ItemBounding rect : ", self.itemsBoundingRect())
print("transform : ", self.plot.transform().m11(), ", ", self.plot.transform().m22())
item = self.itemAt(event.scenePos(), self.plot.transform())
if item and isinstance(item, MyItem):
print()
print('collides path : ', self.mouse_rect.collidesWithPath(item.path))
print('collides item : ', self.mouse_rect.collidesWithItem(item))
super().mouseMoveEvent(event)
def print_bound(self, rect):
self.bounding_rect.setPen(QPen(Qt.green))
self.bounding_rect.setRect(rect.x() + 5, rect.y() + 5,
rect.width() - 10, rect.height() - 10)
class MyView(QGraphicsView):
def __init__(self, data, parent=None):
QGraphicsView.__init__(self, parent)
self.data = data
self.setMouseTracking(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def wheelEvent(self, event: QtGui.QWheelEvent):
print("pixel / Data : {}".format(self.width() / len(self.data)))
def resizeEvent(self, event: QtGui.QResizeEvent):
scene: MyScene = self.scene()
scene.setSceneRect(self.rect().x(), self.rect().y(),
self.rect().width(), self.rect().height())
scene.print_bound(self.rect())
plot: QGraphicsItem = scene.plot
trans = QTransform()
data = plot.df['data']
data = data - data.min()
data_max = data.max()
data_min = data.min()
trans.scale(self.width() / len(data),
self.height() / (data_max - data_min))
plot.trans = trans
plot.setTransform(trans)
def mouseMoveEvent(self, event: QtGui.QMouseEvent):
mouse_rect: QGraphicsRectItem = self.scene().mouse_rect
mouse_rect.setRect(event.pos().x() - 2, event.pos().y() - 2, 4, 4)
super().mouseMoveEvent(event)
if __name__ == '__main__':
df = pandas.DataFrame({'data': numpy.random.randint(0, 20, 50)})
app = QApplication([])
scene = MyScene(df)
view = MyView(df)
view.setScene(scene)
rec = QGraphicsRectItem(-2, -2, 4, 4)
rec.setPen(Qt.white)
scene.mouse_rect = rec
scene.addItem(rec)
plot = MyItem(df)
scene.addItem(plot)
scene.plot = plot
view.show()
app.exec_()
Any idea checking mouse point with path ?? I first tried custom math function calculating [point <-> line] distance, but that need much time and making lagging app..
I will make not only line plot but also bar, area, points, candlestick plot.. Is there any idea to solve this problem?
You have to convert the position of the path with respect to the item that is scaled to the position relative to the scene using mapToScene():
if item and isinstance(item, MyItem):
print('collides path : ', self.mouse_rect.collidesWithPath(item.mapToScene(item.path)))
print('collides item : ', self.mouse_rect.collidesWithItem(item))