Reputation: 2058
I am trying to create a display where you have a matplotlib
canvas in the background, and an overlapping widget in the foreground displaying some arbitrary information about the plotted data. This in itself I have basically achieved, however, I have trouble with the alignment.
I would like the overlapping widget (in the example below the QGroupBox
) to be aligned with the lower left corner of the axes
, and also respond to whenever the window size is changed. The problem is that I don't know how I can change the relative position of the two overlapping widgets correctly.
I found this answer (below called method 1), which uses QAlignment
, but once that is set, the QGroupBox
seems irresponsive to any kind of positional changes. Maybe it is possible to add margins and change them dynamically?
The other method I found is this one (below called method 2), which uses absolute positioning, and thus doesn't change with resizing the window. Maybe this one makes more sense? But then there is some transformation and signal handling necessary to reposition the QGroupBox
every time the window resizes. But somehow I didn't manage to get the transformation right.
Lastly, I also found this, dealing with anchors, but I have no idea how they work, and if they are even a thing in regular PyQt5.
import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import \
FigureCanvasQTAgg as FigureCanvas
import matplotlib.patheffects as PathEffects
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QDialog, QApplication, QGridLayout, QGroupBox, \
QLabel, QLineEdit
class MainWindow(QDialog):
def __init__(self):
super().__init__()
# this is just for context
self.fig, self.ax = plt.subplots()
self.canvas = FigureCanvas(self.fig)
self.data = np.random.uniform(0, 1, (50, 2))
self.artists = []
for point in self.data:
artist = self.ax.plot(*point, 'o', c='orange')[0]
artist.set_pickradius = 5
self.artists.append(artist)
self.zoom_factor = 1.2
self.x_press = 0
self.y_press = 0
self.last_artist = None
self.cid_motion = self.canvas.mpl_connect(
'motion_notify_event', self.motion_event
)
self.cid_button = self.canvas.mpl_connect(
'button_press_event', self.pan_press
)
self.cid_zoom = self.canvas.mpl_connect('scroll_event', self.zoom)
self.mainLayout = QGridLayout(self)
self.mainLayout.addWidget(self.canvas, 0, 0)
self.setLayout(self.mainLayout)
self.statsBox = QGroupBox('Stats:')
self.statsLayout = QGridLayout(self.statsBox)
self.posLabel = QLabel('Pos:')
self.statsLayout.addWidget(self.posLabel, 0, 0)
self.posEdit = QLineEdit()
self.posEdit.setReadOnly(True)
self.posEdit.setAlignment(Qt.AlignHCenter)
self.statsLayout.addWidget(self.posEdit, 0, 1)
# here is what's interesting
# method 1
# self.mainLayout.addWidget(
# self.statsBox, 0, 0, Qt.AlignRight | Qt.AlignBottom
# )
# method 2
self.statsBox.setParent(self)
self.statsBox.setFixedSize(self.statsBox.sizeHint())
self.position_statsBox()
def resizeEvent(self, a0):
super().resizeEvent(a0)
self.position_statsBox()
def position_statsBox(self):
x, y = self.ax.get_xlim()[1], self.ax.get_ylim()[0]
pos = QPoint(*self.ax.transData.transform((x, y)))
self.statsBox.move(pos)
# below here is just for context again
def motion_event(self, event):
if event.inaxes == self.ax and event.button == 1:
self.pan_move(event)
else:
self.hover(event)
def pan_press(self, event):
if event.inaxes == self.ax and event.button == 1:
self.x_press = event.xdata
self.y_press = event.ydata
def pan_move(self, event):
xdata = event.xdata
ydata = event.ydata
cur_xlim = self.ax.get_xlim()
cur_ylim = self.ax.get_ylim()
dx = xdata - self.x_press
dy = ydata - self.y_press
new_xlim = [cur_xlim[0] - dx, cur_xlim[1] - dx]
new_ylim = [cur_ylim[0] - dy, cur_ylim[1] - dy]
self.ax.set_xlim(new_xlim)
self.ax.set_ylim(new_ylim)
self.canvas.draw_idle()
def zoom(self, event):
if event.inaxes == self.ax:
xdata, ydata = event.xdata, event.ydata
xlim = self.ax.get_xlim()
ylim = self.ax.get_ylim()
x_left = xdata - xlim[0]
x_right = xlim[1] - xdata
y_bottom = ydata - ylim[0]
y_top = ylim[1] - ydata
scale_factor = np.power(self.zoom_factor, -event.step)
new_xlim = xdata-x_left*scale_factor, xdata+x_right*scale_factor
new_ylim = ydata-y_bottom*scale_factor, ydata+y_top*scale_factor
self.ax.set_xlim(new_xlim)
self.ax.set_ylim(new_ylim)
self.canvas.draw_idle()
def hover(self, event):
ind = 0
cont = None
while ind in range(len(self.artists)) and not cont:
artist = self.artists[ind]
cont, _ = artist.contains(event)
if cont and artist is not self.last_artist:
if self.last_artist is not None:
self.last_artist.set_path_effects(
[PathEffects.Normal()]
)
self.last_artist = None
artist.set_path_effects(
[PathEffects.withStroke(
linewidth=7, foreground="c", alpha=0.4
)]
)
self.last_artist = artist
x, y = artist.get_data()
pos = f'({x[0]:.2f}, {y[0]:.2f})'
self.posEdit.setText(pos)
ind += 1
if not cont and self.last_artist:
self.last_artist.set_path_effects([PathEffects.Normal()])
self.last_artist = None
self.posEdit.clear()
self.canvas.draw_idle()
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
Upvotes: 1
Views: 361
Reputation: 243897
The solution is to set the QGroupBox as a child of the canvas and change the position using the position of the axis bbox:
import sys
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.patheffects as PathEffects
from PyQt5.QtCore import Qt, QPoint, QTimer
from PyQt5.QtWidgets import (
QDialog,
QApplication,
QGridLayout,
QGroupBox,
QLabel,
QLineEdit,
)
class MainWindow(QDialog):
def __init__(self):
super().__init__()
# this is just for context
self.fig = Figure()
self.canvas = FigureCanvas(self.fig)
self.ax = self.canvas.figure.subplots()
self.data = np.random.uniform(0, 1, (50, 2))
self.artists = []
for point in self.data:
artist = self.ax.plot(*point, "o", c="orange")[0]
artist.set_pickradius = 5
self.artists.append(artist)
self.zoom_factor = 1.2
self.x_press = 0
self.y_press = 0
self.last_artist = None
self.cid_motion = self.canvas.mpl_connect(
"motion_notify_event", self.motion_event
)
self.cid_button = self.canvas.mpl_connect("button_press_event", self.pan_press)
self.cid_zoom = self.canvas.mpl_connect("scroll_event", self.zoom)
self.mainLayout = QGridLayout(self)
self.mainLayout.addWidget(self.canvas, 0, 0)
self.statsBox = QGroupBox("Stats:", self.canvas)
self.statsLayout = QGridLayout(self.statsBox)
self.posLabel = QLabel("Pos:")
self.statsLayout.addWidget(self.posLabel, 0, 0)
self.posEdit = QLineEdit()
self.posEdit.setReadOnly(True)
self.posEdit.setAlignment(Qt.AlignHCenter)
self.statsLayout.addWidget(self.posEdit, 0, 1)
self.statsBox.setFixedSize(self.statsBox.sizeHint())
self.position_statsBox()
def resizeEvent(self, event):
super().resizeEvent(event)
self.position_statsBox()
def position_statsBox(self):
x0, y0, x1, y1 = self.ax.bbox.extents
p = QPoint(int(x0), int(y1))
p -= QPoint(0, self.statsBox.height())
p += QPoint(0, 6) # FIXME
self.statsBox.move(p)
def motion_event(self, event):
if event.inaxes == self.ax and event.button == 1:
self.pan_move(event)
else:
self.hover(event)
def pan_press(self, event):
if event.inaxes == self.ax and event.button == 1:
self.x_press = event.xdata
self.y_press = event.ydata
def pan_move(self, event):
xdata = event.xdata
ydata = event.ydata
cur_xlim = self.ax.get_xlim()
cur_ylim = self.ax.get_ylim()
dx = xdata - self.x_press
dy = ydata - self.y_press
new_xlim = [cur_xlim[0] - dx, cur_xlim[1] - dx]
new_ylim = [cur_ylim[0] - dy, cur_ylim[1] - dy]
self.ax.set_xlim(new_xlim)
self.ax.set_ylim(new_ylim)
self.canvas.draw_idle()
def zoom(self, event):
if event.inaxes == self.ax:
xdata, ydata = event.xdata, event.ydata
xlim = self.ax.get_xlim()
ylim = self.ax.get_ylim()
x_left = xdata - xlim[0]
x_right = xlim[1] - xdata
y_bottom = ydata - ylim[0]
y_top = ylim[1] - ydata
scale_factor = np.power(self.zoom_factor, -event.step)
new_xlim = xdata - x_left * scale_factor, xdata + x_right * scale_factor
new_ylim = ydata - y_bottom * scale_factor, ydata + y_top * scale_factor
self.ax.set_xlim(new_xlim)
self.ax.set_ylim(new_ylim)
self.canvas.draw_idle()
def hover(self, event):
ind = 0
cont = None
while ind in range(len(self.artists)) and not cont:
artist = self.artists[ind]
cont, _ = artist.contains(event)
if cont and artist is not self.last_artist:
if self.last_artist is not None:
self.last_artist.set_path_effects([PathEffects.Normal()])
self.last_artist = None
artist.set_path_effects(
[PathEffects.withStroke(linewidth=7, foreground="c", alpha=0.4)]
)
self.last_artist = artist
x, y = artist.get_data()
pos = f"({x[0]:.2f}, {y[0]:.2f})"
self.posEdit.setText(pos)
ind += 1
if not cont and self.last_artist:
self.last_artist.set_path_effects([PathEffects.Normal()])
self.last_artist = None
self.posEdit.clear()
self.canvas.draw_idle()
if __name__ == "__main__":
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
Upvotes: 2