Pyqt5. How to clear QVBoxLayout by a Signal? - pyqt5

I try to clear QVBoxLayout with self.vbox.takeAt(0), but previous vboxes clog up the QWidget and would not go away. New appear on top of the old ones.
Whole code https://pastebin.com/n43BH1R0
class Worker(QObject):
CLEAR_VBOX = pyqtSignal()
intReady = pyqtSignal(int)
#pyqtSlot()
def procCounter(self):
for i in range(1, 100):
self.CLEAR_VBOX.emit()
self.intReady.emit(i)
time.sleep(1)
class Form(QWidget):
..
..
def initUI(self):
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.setGeometry(333, 333, 222, 222)
self.show()
def clearvbox(self):
while self.vbox.count():
# ~ # break
self.vbox.takeAt(0)
def onIntReady(self, i):
hbox = QHBoxLayout()
for ii in range(i):
ll = QLabel(str(random.randint(1,9)))
hbox.addWidget(ll)
self.vbox.addLayout(hbox)

def clearvbox(self, L = False):
if not L:
L = self.vbox
if L is not None:
while L.count():
item = L.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
else:
self.clearvbox(item.layout())

Related

I cant show Qlabel in mouse move when draw a line

I want to move the QLabel with the mouse movement when draw a line
here is my Ruler class
set mouse tracking in QGraphicView
class RulerScene(QtWidgets.QGraphicsScene):
def __init__(self,MainWindow, *args, **kwargs):
super(RulerScene, self).__init__(*args, **kwargs)
self.MainWindow = MainWindow
# self.setSceneRect(QtCore.QRectF(0, 0, 500, 500))
self.indicatorLabel = QLabel(self.MainWindow)
self.indicatorLabel.setStyleSheet("background-color:red")
self.indicatorLabel.resize(160,40)
self.indicatorLabel.setText('Hi....')
self.graphics_Label = None
self.line = None
self.graphics_line = None
self.start_point = QtCore.QPointF()
self.end_point = QtCore.QPointF()
def mousePressEvent(self, event):
if len(self.items()) > 1:
self.removeItem(self.items()[0])
self.start_point = event.scenePos()
self.end_point = self.start_point
self.line = QtCore.QLineF(self.start_point, self.end_point)
self.graphics_line = QtWidgets.QGraphicsLineItem(self.line)
self.update_path()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self.end_point = event.scenePos()
self.update_path()
super(RulerScene, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.end_point = event.scenePos()
self.update_path()
super(RulerScene, self).mouseReleaseEvent(event)
def update_path(self):
if not self.start_point.isNull() and not self.end_point.isNull():
self.rulerSize()
self.line.setP2(self.end_point)
self.graphics_line.setLine(self.line)
self.graphics_line.setPen(QPen(
QColor(242, 219, 7),
3,
QtCore.Qt.SolidLine,
QtCore.Qt.FlatCap,
QtCore.Qt.MiterJoin,
))
self.addItem(self.graphics_Label)
self.addItem(self.graphics_line)
def rulerSize(self):
size = max(abs(self.start_point.x() - self.end_point.x()), abs(self.start_point.y() - self.end_point.y())) - 1
self.indicatorLabel.setText(str(size))
self.indicatorLabel.move(self.start_point.x() - self.end_point.x(),self.start_point.y() - self.end_point.y())
self.graphics_Label = QtWidgets.QGraphicsSimpleTextItem(self.indicatorLabel)
Draw a line and add this line to QGraphicView
but i want to show coordinate of a mouse and a label must be show size of pixel and track mouse.
I would like to know in general the way of overlaying widgets on top of other in an absolute way with XY pixel coordinates.
sample of my ruler

Matplotlib cross hair cursor in PyQt5

I want to add a cross hair that snaps to data points and be updated on mouse move. I found this example that works well:
import numpy as np
import matplotlib.pyplot as plt
class SnappingCursor:
"""
A cross hair cursor that snaps to the data point of a line, which is
closest to the *x* position of the cursor.
For simplicity, this assumes that *x* values of the data are sorted.
"""
def __init__(self, ax, line):
self.ax = ax
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
self.x, self.y = line.get_data()
self._last_index = None
# text location in axes coords
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
def set_cross_hair_visible(self, visible):
need_redraw = self.vertical_line.get_visible() != visible
self.vertical_line.set_visible(visible)
self.horizontal_line.set_visible(visible)
self.text.set_visible(visible)
return need_redraw
def on_mouse_move(self, event):
if not event.inaxes:
self._last_index = None
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.draw()
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
index = min(np.searchsorted(self.y, y), len(self.y) - 1)
if index == self._last_index:
return # still on the same data point. Nothing to do.
self._last_index = index
x = self.x[index]
y = self.y[index]
# update the line positions
self.horizontal_line.set_ydata(y)
self.vertical_line.set_xdata(x)
self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
self.ax.figure.canvas.draw()
y = np.arange(0, 1, 0.01)
x = np.sin(2 * 2 * np.pi * y)
fig, ax = plt.subplots()
ax.set_title('Snapping cursor')
line, = ax.plot(x, y, 'o')
snap_cursor = SnappingCursor(ax, line)
fig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)
plt.show()
But I get into trouble when I want to adapt the code with the PyQt5 and show the plot in a GUI. My code is:
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
import sys
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import numpy as np
class SnappingCursor:
"""
A cross hair cursor that snaps to the data point of a line, which is
closest to the *x* position of the cursor.
For simplicity, this assumes that *x* values of the data are sorted.
"""
def __init__(self, ax, line):
self.ax = ax
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
self.x, self.y = line.get_data()
self._last_index = None
# text location in axes coords
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
def set_cross_hair_visible(self, visible):
need_redraw = self.vertical_line.get_visible() != visible
self.vertical_line.set_visible(visible)
self.horizontal_line.set_visible(visible)
self.text.set_visible(visible)
return need_redraw
def on_mouse_move(self, event):
if not event.inaxes:
self._last_index = None
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.draw()
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
index = min(np.searchsorted(self.y, y), len(self.y) - 1)
if index == self._last_index:
return # still on the same data point. Nothing to do.
self._last_index = index
x = self.x[index]
y = self.y[index]
# update the line positions
self.horizontal_line.set_ydata(y)
self.vertical_line.set_xdata(x)
self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
self.ax.figure.canvas.draw()
class Window(QMainWindow):
def __init__(self):
super().__init__()
widget=QWidget()
vbox=QVBoxLayout()
plot1 = FigureCanvas(Figure(tight_layout=True, linewidth=3))
ax = plot1.figure.subplots()
x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)
line, = ax.plot(x, y, 'o')
snap_cursor = SnappingCursor(ax, line)
plot1.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)
vbox.addWidget(plot1)
widget.setLayout(vbox)
self.setCentralWidget(widget)
self.setWindowTitle('Example')
self.show()
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
By running the above code, the data is plotted properly, but the cross hair is only shown in its initial position and does not move by mouse movement. Data values are also not displayed.
I have found a similar question here too, but the question is not answered clearly.
There are 2 problems:
snap_cursor is a local variable that will be removed when __init__ finishes executing. You must make him a member of the class.
The initial code of the tutorial is designed so that the point that information is displayed is the horizontal line that passes through the cursor and intersects the curve. In your initial code it differs from the example and also does not work for your new curve so I restored the logic of the tutorial.
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class SnappingCursor:
"""
A cross hair cursor that snaps to the data point of a line, which is
closest to the *x* position of the cursor.
For simplicity, this assumes that *x* values of the data are sorted.
"""
def __init__(self, ax, line):
self.ax = ax
self.horizontal_line = ax.axhline(color="k", lw=0.8, ls="--")
self.vertical_line = ax.axvline(color="k", lw=0.8, ls="--")
self.x, self.y = line.get_data()
self._last_index = None
# text location in axes coords
self.text = ax.text(0.72, 0.9, "", transform=ax.transAxes)
def set_cross_hair_visible(self, visible):
need_redraw = self.vertical_line.get_visible() != visible
self.vertical_line.set_visible(visible)
self.horizontal_line.set_visible(visible)
self.text.set_visible(visible)
return need_redraw
def on_mouse_move(self, event):
if not event.inaxes:
self._last_index = None
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.draw()
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
index = min(np.searchsorted(self.x, x), len(self.x) - 1)
if index == self._last_index:
return # still on the same data point. Nothing to do.
self._last_index = index
x = self.x[index]
y = self.y[index]
# update the line positions
self.horizontal_line.set_ydata(y)
self.vertical_line.set_xdata(x)
self.text.set_text("x=%1.2f, y=%1.2f" % (x, y))
self.ax.figure.canvas.draw()
class Window(QMainWindow):
def __init__(self):
super().__init__()
widget = QWidget()
vbox = QVBoxLayout(widget)
x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)
canvas = FigureCanvas(Figure(tight_layout=True, linewidth=3))
ax = canvas.figure.subplots()
ax.set_title("Snapping cursor")
(line,) = ax.plot(x, y, "o")
self.snap_cursor = SnappingCursor(ax, line)
canvas.mpl_connect("motion_notify_event", self.snap_cursor.on_mouse_move)
vbox.addWidget(canvas)
self.setCentralWidget(widget)
self.setWindowTitle("Example")
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec()

How to delete old plot on Matplotlib figure embedded in PyQt5?

I am using matplotlib figure embedded in pyqt5 to draw a frame taking the height and the width from line edit entries.It works well but when I change the values inside the line edits and click on the push button it will draw another frame over the previous one. I have tried plt.gcf().clear() and ax.clear and they did not work.
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Draw Frame")
self.setGeometry(100,100,680, 450)
# Creation of figure and canvas
self.figure = plt.figure()
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111)
self.ax.axis("off")
#self.toolbar = NavigationToolbar(self.canvas, self)
self.plot_widget = QWidget(self)
self.plot_widget.setGeometry(250, 10, 400, 400)
plot_box = QVBoxLayout()
plot_box.addWidget(self.canvas)
self.plot_widget.setLayout(plot_box)
self.label1=QLabel("Frame height",self)
self.label1.move(10,30)
self.label2 = QLabel("Frame height", self)
self.label2.move(10, 70)
self.lineEdit1=QLineEdit(self)
self.lineEdit1.move(100,30)
self.lineEdit1.setText("10")
self.lineEdit2 = QLineEdit(self)
self.lineEdit2.move(100, 70)
self.lineEdit2.setText("20")
self.button = QPushButton('Draw Frame', self)
self.button.clicked.connect(self.plot)
self.button.move(70, 350)
self.show()
def plot(self):
if len(self.lineEdit1.text())!=0:
self.h=int(self.lineEdit1.text())
else:
self.h=0
if len(self.lineEdit2.text()) != 0:
self.w=int(self.lineEdit2.text())
else:
self.w=0
x = [0, 0, self.w, self.w]
y = [0, self.h, self.h, 0]
self.ax.plot(x, y)
self.canvas.draw()
Picture of the app
Instead of clearing the axes you may update the line that is drawn with the new coordinates.
from PyQt5.QtWidgets import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Draw Frame")
self.setGeometry(100,100,680, 450)
# Creation of figure and canvas
self.figure = plt.figure()
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111)
self.ax.axis("off")
self.line, = self.ax.plot([])
self.ax.axis([0,100,0,100])
#self.toolbar = NavigationToolbar(self.canvas, self)
self.plot_widget = QWidget(self)
self.plot_widget.setGeometry(250, 10, 400, 400)
plot_box = QVBoxLayout()
plot_box.addWidget(self.canvas)
self.plot_widget.setLayout(plot_box)
self.label1=QLabel("Frame height",self)
self.label1.move(10,30)
self.label2 = QLabel("Frame height", self)
self.label2.move(10, 70)
self.lineEdit1=QLineEdit(self)
self.lineEdit1.move(100,30)
self.lineEdit1.setText("10")
self.lineEdit2 = QLineEdit(self)
self.lineEdit2.move(100, 70)
self.lineEdit2.setText("20")
self.button = QPushButton('Draw Frame', self)
self.button.clicked.connect(self.plot)
self.button.move(70, 350)
self.show()
def plot(self):
if len(self.lineEdit1.text())!=0:
self.h=int(self.lineEdit1.text())
else:
self.h=0
if len(self.lineEdit2.text()) != 0:
self.w=int(self.lineEdit2.text())
else:
self.w=0
x = [0, 0, self.w, self.w]
y = [0, self.h, self.h, 0]
self.line.set_data(x,y)
self.canvas.draw()
if __name__=='__main__':
app = QApplication(sys.argv)
w = Window()
app.exec_()
As mentioned in the comment above, you need to call self.ax.clear() every time you click the button. Otherwise you will be redrawing on the same plot. Not sure why your graph doesnt reset on clear(), but here is the code I ran which worked fine. Let me know if it works for you:
from PyQt5.QtWidgets import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Draw Frame")
self.setGeometry(100,100,680, 450)
# Creation of figure and canvas
self.figure = plt.figure()
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111)
self.ax.axis("off")
#self.toolbar = NavigationToolbar(self.canvas, self)
self.plot_widget = QWidget(self)
self.plot_widget.setGeometry(250, 10, 400, 400)
plot_box = QVBoxLayout()
plot_box.addWidget(self.canvas)
self.plot_widget.setLayout(plot_box)
self.label1=QLabel("Frame height",self)
self.label1.move(10,30)
self.label2 = QLabel("Frame height", self)
self.label2.move(10, 70)
self.lineEdit1=QLineEdit(self)
self.lineEdit1.move(100,30)
self.lineEdit1.setText("10")
self.lineEdit2 = QLineEdit(self)
self.lineEdit2.move(100, 70)
self.lineEdit2.setText("20")
self.button = QPushButton('Draw Frame', self)
self.button.clicked.connect(self.plot)
self.button.move(70, 350)
self.show()
def plot(self):
self.ax.clear()
if len(self.lineEdit1.text())!=0:
self.h=int(self.lineEdit1.text())
else:
self.h=0
if len(self.lineEdit2.text()) != 0:
self.w=int(self.lineEdit2.text())
else:
self.w=0
x = [0, 0, self.w, self.w]
y = [0, self.h, self.h, 0]
self.ax.plot(x, y)
self.canvas.draw()
if __name__=='__main__':
app = QApplication(sys.argv)
w = Window()
app.exec_()

how to show the cursor coordinates in wxPython when using wx.SplitterWindow

I'm trying to do something similar to that shown in this link to draw a cursor and report the data coordinates in the status bar. However, my code is a little bit different since I need to use wx.SplitterWindow to separate buttons and plots. Basically, in the main frame module I create the status bar, but the plot is generated in a separated module. See below my current code:
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar, \
wxc as wxc
import pylab
import wx
from numpy import arange, sin, pi
class data:
def __init__(self):
self.t = []
self.s = []
class Plot_Panel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
# create some sizers
mainSizer = wx.BoxSizer(wx.VERTICAL)
checkSizer = wx.BoxSizer(wx.HORIZONTAL)
# create figrue
self.fig = Figure()
self.canvas = FigCanvas(self, -1, self.fig)
self.axes = self.fig.add_subplot(111)
# create the widgets
self.toggleMarker = wx.CheckBox(self, label="Show Marker")
# layout the widgets
mainSizer.Add(self.canvas, 1, wx.EXPAND)
checkSizer.Add(self.toggleMarker, 0, wx.ALL, 5)
mainSizer.Add(checkSizer)
self.SetSizer(mainSizer)
def draw_plot(self, data):
# Clear the previous figure
self.fig.clear()
# Redraw figure
self.axes = self.fig.add_subplot(111)
# Define data to plot
self.plot_data= self.axes.plot(data.t, data.s, linewidth=3, color='y',)[0]
# Draw Cursor or not
if self.toggleMarker.IsChecked():
# Note that event is a MplEvent
self.canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar)
self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.ChangeCursor)
self.canvas.draw()
def ChangeCursor(self, event):
self.canvas.SetCursor(wxc.StockCursor(wx.CURSOR_BULLSEYE))
def UpdateStatusBar(self, event):
if event.inaxes:
x, y = event.xdata, event.ydata
# self.statusBar.SetStatusText(("x= "+str(Pos.x)+" y="+str(Pos.y)))
class Button_Panel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
# create the widgets
self.toggleStart = wx.Button(self, id=wx.ID_ANY, label="Plot data")
class ProportionalSplitter(wx.SplitterWindow):
def __init__(self,parent, id = -1, proportion=0.66, size = wx.DefaultSize, **kwargs):
wx.SplitterWindow.__init__(self,parent,id,wx.Point(0, 0),size, **kwargs)
self.SetMinimumPaneSize(50) #the minimum size of a pane.
self.proportion = proportion
if not 0 < self.proportion < 1:
raise ValueError, "proportion value for ProportionalSplitter must be between 0 and 1."
self.ResetSash()
self.Bind(wx.EVT_SIZE, self.OnReSize)
self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.OnSashChanged, id=id)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.firstpaint = True
def SplitHorizontally(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitHorizontally(self, win1, win2,
int(round(self.GetParent().GetSize().GetHeight() * self.proportion)))
def SplitVertically(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitVertically(self, win1, win2,
int(round(self.GetParent().GetSize().GetWidth() * self.proportion)))
def GetExpectedSashPosition(self):
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
return int(round(tot * self.proportion))
def ResetSash(self):
self.SetSashPosition(self.GetExpectedSashPosition())
def OnReSize(self, event):
"Window has been resized, so we need to adjust the sash based on self.proportion."
self.ResetSash()
event.Skip()
def OnSashChanged(self, event):
"We'll change self.proportion now based on where user dragged the sash."
pos = float(self.GetSashPosition())
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
self.proportion = pos / tot
event.Skip()
def OnPaint(self, event):
if self.firstpaint:
if self.GetSashPosition() != self.GetExpectedSashPosition():
self.ResetSash()
self.firstpaint = False
event.Skip()
class Main_Window(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title = title)
# Create a StatusBar at the bottom of the window
self.statusBar = wx.StatusBar(self, -1)
self.SetStatusBar(self.statusBar)
# Set plot panel
self.splitter = ProportionalSplitter(self,-1, 0.85)
self.ppanel = Plot_Panel(self.splitter)
self.ppanel.SetBackgroundColour('#ffffff')
# Set button panel
self.bpanel = Button_Panel(self.splitter)
# Set frame
self.splitter.SplitVertically(self.ppanel, self.bpanel)
self.Show(True)
self.Maximize(True)
# bind the widgets
self.ppanel.toggleMarker.Bind(wx.EVT_CHECKBOX, self.onToggleMarker)
self.bpanel.toggleStart.Bind(wx.EVT_BUTTON, self.onToggleStart)
# Set classes
self.data = data()
def onToggleMarker(self, event):
self.ppanel.draw_plot(self.data)
def onToggleStart(self, event):
self.data.t = arange(0.0, 1.0, 0.01)
self.data.s = sin(2*2*pi*self.data.t)
# plot data
self.ppanel.draw_plot(self.data)
def main():
app = wx.App(False)
frame = Main_Window(None, "GUI")
frame.Show()
app.MainLoop()
if __name__ == "__main__" :
main()
The plot is shown when the button "Plot data" is pressed. What I would like to do is to show the x and y position in the status bar when the checkbox "Show Marker" is checked (in a similar way as it is done in the code posted in the link), and stop when it is unchecked. But I'm not sure if it is possible to do it in my code due to having the definitions of the status bar and the plot in different modules. Any hint will be welcome.
What a joy to get a full, working example program with the question.
You just need to pass the base module as a parameter to self.ppanel
i.e.
self.ppanel = Plot_Panel(self.splitter, self)
then refer to that when updating the status bar, see below for references to base
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar, \
wxc as wxc
import pylab
import wx
from numpy import arange, sin, pi
class data:
def __init__(self):
self.t = []
self.s = []
class Plot_Panel(wx.Panel):
def __init__(self, parent, base):
wx.Panel.__init__(self, parent)
self.base = base
# create some sizers
mainSizer = wx.BoxSizer(wx.VERTICAL)
checkSizer = wx.BoxSizer(wx.HORIZONTAL)
# create figrue
self.fig = Figure()
self.canvas = FigCanvas(self, -1, self.fig)
self.axes = self.fig.add_subplot(111)
# create the widgets
self.toggleMarker = wx.CheckBox(self, label="Show Marker")
# layout the widgets
mainSizer.Add(self.canvas, 1, wx.EXPAND)
checkSizer.Add(self.toggleMarker, 0, wx.ALL, 5)
mainSizer.Add(checkSizer)
self.SetSizer(mainSizer)
def draw_plot(self, data):
# Clear the previous figure
self.fig.clear()
# Redraw figure
self.axes = self.fig.add_subplot(111)
# Define data to plot
self.plot_data= self.axes.plot(data.t, data.s, linewidth=3, color='y',)[0]
# Draw Cursor or not
if self.toggleMarker.IsChecked():
# Note that event is a MplEvent
self.canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar)
self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.ChangeCursor)
self.canvas.draw()
def ChangeCursor(self, event):
self.canvas.SetCursor(wxc.StockCursor(wx.CURSOR_BULLSEYE))
def UpdateStatusBar(self, event):
if event.inaxes:
x, y = event.xdata, event.ydata
self.base.statusBar.SetStatusText(("x= "+str(x)+" y="+str(y)))
class Button_Panel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
# create the widgets
self.toggleStart = wx.Button(self, id=wx.ID_ANY, label="Plot data")
class ProportionalSplitter(wx.SplitterWindow):
def __init__(self,parent, id = -1, proportion=0.66, size = wx.DefaultSize, **kwargs):
wx.SplitterWindow.__init__(self,parent,id,wx.Point(0, 0),size, **kwargs)
self.SetMinimumPaneSize(50) #the minimum size of a pane.
self.proportion = proportion
if not 0 < self.proportion < 1:
raise ValueError, "proportion value for ProportionalSplitter must be between 0 and 1."
self.ResetSash()
self.Bind(wx.EVT_SIZE, self.OnReSize)
self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.OnSashChanged, id=id)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.firstpaint = True
def SplitHorizontally(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitHorizontally(self, win1, win2,
int(round(self.GetParent().GetSize().GetHeight() * self.proportion)))
def SplitVertically(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitVertically(self, win1, win2,
int(round(self.GetParent().GetSize().GetWidth() * self.proportion)))
def GetExpectedSashPosition(self):
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
return int(round(tot * self.proportion))
def ResetSash(self):
self.SetSashPosition(self.GetExpectedSashPosition())
def OnReSize(self, event):
"Window has been resized, so we need to adjust the sash based on self.proportion."
self.ResetSash()
event.Skip()
def OnSashChanged(self, event):
"We'll change self.proportion now based on where user dragged the sash."
pos = float(self.GetSashPosition())
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
self.proportion = pos / tot
event.Skip()
def OnPaint(self, event):
if self.firstpaint:
if self.GetSashPosition() != self.GetExpectedSashPosition():
self.ResetSash()
self.firstpaint = False
event.Skip()
class Main_Window(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title = title)
# Create a StatusBar at the bottom of the window
self.statusBar = wx.StatusBar(self, -1)
self.SetStatusBar(self.statusBar)
# Set plot panel
self.splitter = ProportionalSplitter(self,-1, 0.85)
self.ppanel = Plot_Panel(self.splitter, self)
self.ppanel.SetBackgroundColour('#ffffff')
# Set button panel
self.bpanel = Button_Panel(self.splitter)
# Set frame
self.splitter.SplitVertically(self.ppanel, self.bpanel)
self.Show(True)
self.Maximize(True)
# bind the widgets
self.ppanel.toggleMarker.Bind(wx.EVT_CHECKBOX, self.onToggleMarker)
self.bpanel.toggleStart.Bind(wx.EVT_BUTTON, self.onToggleStart)
# Set classes
self.data = data()
def onToggleMarker(self, event):
self.ppanel.draw_plot(self.data)
def onToggleStart(self, event):
self.data.t = arange(0.0, 1.0, 0.01)
self.data.s = sin(2*2*pi*self.data.t)
# plot data
self.ppanel.draw_plot(self.data)
def main():
app = wx.App(False)
frame = Main_Window(None, "GUI")
frame.Show()
app.MainLoop()
if __name__ == "__main__" :
main()

Include matplotlib in pyqt5 with hover labels

I have a plot from matplotlib for which I would like to display labels on the marker points when hover over with the mouse.
I found this very helpful working example on SO and I was trying to integrate the exact same plot into a pyqt5 application.
Unfortunately when having the plot in the application the hovering doesn't work anymore.
Here is a full working example based on the mentioned SO post:
import matplotlib.pyplot as plt
import scipy.spatial as spatial
import numpy as np
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import sys
pi = np.pi
cos = np.cos
def fmt(x, y):
return 'x: {x:0.2f}\ny: {y:0.2f}'.format(x=x, y=y)
class FollowDotCursor(object):
"""Display the x,y location of the nearest data point.
https://stackoverflow.com/a/4674445/190597 (Joe Kington)
https://stackoverflow.com/a/13306887/190597 (unutbu)
https://stackoverflow.com/a/15454427/190597 (unutbu)
"""
def __init__(self, ax, x, y, tolerance=5, formatter=fmt, offsets=(-20, 20)):
try:
x = np.asarray(x, dtype='float')
except (TypeError, ValueError):
x = np.asarray(mdates.date2num(x), dtype='float')
y = np.asarray(y, dtype='float')
mask = ~(np.isnan(x) | np.isnan(y))
x = x[mask]
y = y[mask]
self._points = np.column_stack((x, y))
self.offsets = offsets
y = y[np.abs(y-y.mean()) <= 3*y.std()]
self.scale = x.ptp()
self.scale = y.ptp() / self.scale if self.scale else 1
self.tree = spatial.cKDTree(self.scaled(self._points))
self.formatter = formatter
self.tolerance = tolerance
self.ax = ax
self.fig = ax.figure
self.ax.xaxis.set_label_position('top')
self.dot = ax.scatter(
[x.min()], [y.min()], s=130, color='green', alpha=0.7)
self.annotation = self.setup_annotation()
plt.connect('motion_notify_event', self)
def scaled(self, points):
points = np.asarray(points)
return points * (self.scale, 1)
def __call__(self, event):
ax = self.ax
# event.inaxes is always the current axis. If you use twinx, ax could be
# a different axis.
if event.inaxes == ax:
x, y = event.xdata, event.ydata
elif event.inaxes is None:
return
else:
inv = ax.transData.inverted()
x, y = inv.transform([(event.x, event.y)]).ravel()
annotation = self.annotation
x, y = self.snap(x, y)
annotation.xy = x, y
annotation.set_text(self.formatter(x, y))
self.dot.set_offsets((x, y))
bbox = ax.viewLim
event.canvas.draw()
def setup_annotation(self):
"""Draw and hide the annotation box."""
annotation = self.ax.annotate(
'', xy=(0, 0), ha = 'right',
xytext = self.offsets, textcoords = 'offset points', va = 'bottom',
bbox = dict(
boxstyle='round,pad=0.5', fc='yellow', alpha=0.75),
arrowprops = dict(
arrowstyle='->', connectionstyle='arc3,rad=0'))
return annotation
def snap(self, x, y):
"""Return the value in self.tree closest to x, y."""
dist, idx = self.tree.query(self.scaled((x, y)), k=1, p=1)
try:
return self._points[idx]
except IndexError:
# IndexError: index out of bounds
return self._points[0]
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.width = 1000
self.height = 800
self.setGeometry(0, 0, self.width, self.height)
canvas = self.get_canvas()
w = QWidget()
w.layout = QHBoxLayout()
w.layout.addWidget(canvas)
w.setLayout(w.layout)
self.setCentralWidget(w)
self.show()
def get_canvas(self):
fig, ax = plt.subplots()
x = np.linspace(0.1, 2*pi, 10)
y = cos(x)
markerline, stemlines, baseline = ax.stem(x, y, '-.')
plt.setp(markerline, 'markerfacecolor', 'b')
plt.setp(baseline, 'color','r', 'linewidth', 2)
cursor = FollowDotCursor(ax, x, y, tolerance=20)
canvas = FigureCanvas(fig)
return canvas
app = QApplication(sys.argv)
win = MainWindow()
sys.exit(app.exec_())
What would I have to do to make the labels also show when hovering over in the pyqt application?
The first problem may be that you don't keep a reference to the FollowDotCursor.
So to make sure the FollowDotCursor stays alive, you can make it a class variable
self.cursor = FollowDotCursor(ax, x, y, tolerance=20)
instead of cursor = ....
Next make sure you instatiate the Cursor class after giving the figure a canvas.
canvas = FigureCanvas(fig)
self.cursor = FollowDotCursor(ax, x, y, tolerance=20)
Finally, keep a reference to the callback inside the FollowDotCursor and don't use plt.connect but the canvas itself:
self.cid = self.fig.canvas.mpl_connect('motion_notify_event', self)