python matplotlib and PyQT for multi-tab plotting - navigation - python

I've created a qt app that can be used to display matplotlib figures in multiple tabs. Now I'm trying to get the standard matplotlib navigation toolbar to work for all the figures in the various tabs. So far I've only managed to get it working in one of the figures, but not all.
Here's the code:
from PyQt4 import QtCore
from PyQt4 import QtGui as qt
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
from matplotlib.figure import Figure
import itertools
class MultiTabNavTool(NavigationToolbar):
#====================================================================================================
def __init__(self, canvases, tabs, parent=None):
self.canvases = canvases
self.tabs = tabs
NavigationToolbar.__init__(self, canvases[0], parent)
#====================================================================================================
def get_canvas(self):
return self.canvases[self.tabs.currentIndex()]
def set_canvas(self, canvas):
self._canvas = canvas
canvas = property(get_canvas, set_canvas)
class MplMultiTab(qt.QMainWindow):
#====================================================================================================
def __init__(self, parent=None, figures=None, labels=None):
qt.QMainWindow.__init__(self, parent)
self.main_frame = qt.QWidget()
self.tabWidget = qt.QTabWidget( self.main_frame )
self.create_tabs( figures, labels )
# Create the navigation toolbar, tied to the canvas
self.mpl_toolbar = MultiTabNavTool(self.canvases, self.tabWidget, self.main_frame)
self.vbox = vbox = qt.QVBoxLayout()
vbox.addWidget(self.mpl_toolbar)
vbox.addWidget(self.tabWidget)
self.main_frame.setLayout(vbox)
self.setCentralWidget(self.main_frame)
#====================================================================================================
def create_tabs(self, figures, labels ):
if labels is None: labels = []
figures = [Figure()] if figures is None else figures #initialise with empty figure in first tab if no figures provided
self.canvases = [self.add_tab(fig, lbl)
for (fig, lbl) in itertools.zip_longest(figures, labels) ]
#====================================================================================================
def add_tab(self, fig=None, name=None):
'''dynamically add tabs with embedded matplotlib canvas with this function.'''
# Create the mpl Figure and FigCanvas objects.
if fig is None:
fig = Figure()
ax = fig.add_subplot(111)
canvas = fig.canvas if fig.canvas else FigureCanvas(fig)
canvas.setParent(self.tabWidget)
canvas.setFocusPolicy( QtCore.Qt.ClickFocus )
#self.tabs.append( tab )
name = 'Tab %i'%(self.tabWidget.count()+1) if name is None else name
self.tabWidget.addTab(canvas, name)
return canvas
A basic usage example would be:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(1, 2*np.pi, 100)
figures = []
for i in range(1,3):
fig, ax = plt.subplots()
y = np.sin(np.pi*i*x)+0.1*np.random.randn(100)
ax.plot(x,y)
figures.append( fig )
app = qt.QApplication(sys.argv)
ui = MplMultiTab( figures=figures )
ui.show()
app.exec_()
Are there any matplotlib ninjas out there who might know how I can get the navigation toolbar to play with the multiple figure canvasses?

I think you can create toolbar for every canvas and show/hide them when tabs.currentTab changed:
class MultiTabNavTool(qt.QWidget):
def __init__(self, canvases, tabs, parent=None):
qt.QWidget.__init__(self, parent)
self.canvases = canvases
self.tabs = tabs
self.toolbars = [NavigationToolbar(canvas, parent) for canvas in self.canvases]
vbox = qt.QVBoxLayout()
for toolbar in self.toolbars:
vbox.addWidget(toolbar)
self.setLayout(vbox)
self.switch_toolbar()
self.tabs.currentChanged.connect(self.switch_toolbar)
def switch_toolbar(self):
for toolbar in self.toolbars:
toolbar.setVisible(False)
self.toolbars[self.tabs.currentIndex()].setVisible(True)

Related

How to collect the axis object of the last mouse click in matplotlib canvas drawn with pyqt5?

I have an interactive window and I need to know which subplot was selected during the interaction. When I was using matplotlib alone, I could use plt.connect('button_press_event', myMethod). But with pyqt5, I am importing FigureCanvasQTAgg and there is a reference to the figure itself but not an equivalent of pyplot. So, I am unable to create that reference.
Minimal reproducible example:
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
# list to store the axis last used with a mouseclick
currAx = []
# detect the currently modified axis
def onClick(event):
if event.inaxes:
currAx[:] = [event.inaxes]
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.canvas = FigureCanvas(plt.Figure())
self.axis = self.canvas.figure.subplots(3)
for i, ax in enumerate(self.axis):
t = np.linspace(-i, i + 1, 100)
ax.plot(t, np.sin(2 * np.pi * t))
self.listOfSpans = [SpanSelector(
ax,
self.onselect,
"horizontal"
)
for ax in self.axis]
plt.connect('button_press_event', onClick)
# need an equivalent of ^^ to find the axis interacted with
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
toolbar = NavigationToolbar(self.canvas, self)
layout.addWidget(toolbar)
layout.addWidget(self.canvas)
self.setLayout(layout)
self.show()
def onselect(self, xmin, xmax):
if xmin == xmax:
return
# identify the axis interacted and do something with that information
for ax, span in zip(self.axis, self.listOfSpans):
if ax == currAx[0]:
print(ax)
print(xmin, xmax)
self.canvas.draw()
def run():
app = QApplication([])
mw = MyWidget()
app.exec_()
if __name__ == '__main__':
run()
Apparently, there is a method for connecting canvas as well - canvas.mpl_connect('button_press_event', onclick).
Link to the explanation: https://matplotlib.org/stable/users/explain/event_handling.html
Found the link to the explanation in this link :Control the mouse click event with a subplot rather than a figure in matplotlib.

Update lat/lon axes when zooming on cartopy plot

I am plotting some data in cartopy. I would like to be able to zoom in on a region of the map and have the latitude/longitude axes update to reflect the zoomed in region. Instead, they just dissapear altogether when I zoom in. How do I fix this?
Here is my code for generating the axes
plt.figure()
ax = plt.axes(projection=cartopy.crs.PlateCarree())
ax.add_feature(cartopy.feature.LAND, edgecolor='black')
gl = ax.gridlines(crs=cartopy.crs.PlateCarree(), draw_labels=True,
linewidth=2, color='gray', alpha=0.5, linestyle='--')
# plot some stuff here
It is possible to update the cartopy gridliners in interactive mode, but you need to subclass the Navigation toolbar.
In this example below I have used a PySide/QT5 example code that allows me to substitute a subclassed toolbar, then merged in the gridliner example code. The overloaded toolbar callbacks recreate the gridlines everytime zoom/pan/home is used.
I used python3.8, matplotlib-3.4.2, cartopy-0.20
import sys
from PySide2 import QtWidgets
from PySide2.QtWidgets import QVBoxLayout
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import cartopy.crs as ccrs
class CustomNavigationToolbar(NavigationToolbar):
toolitems = [t for t in NavigationToolbar.toolitems if t[0] in ('Home', 'Pan', 'Zoom', 'Save')]
def __init__(self, canvas, parent, coordinates=True, func_recreate_gridlines=None):
print('CustomNavigationToolbar::__init__')
super(CustomNavigationToolbar, self).__init__(canvas, parent, coordinates)
self.func_recreate_gridlines = func_recreate_gridlines
def home(self, *args):
print('CustomNavigationToolbar::home')
super(CustomNavigationToolbar, self).home(*args)
if self.func_recreate_gridlines is not None:
self.func_recreate_gridlines()
def release_pan(self, event):
print('CustomNavigationToolbar::release_pan')
super(CustomNavigationToolbar, self).release_pan(event)
if self.func_recreate_gridlines is not None:
self.func_recreate_gridlines()
def release_zoom(self, event):
print('CustomNavigationToolbar::release_zoom')
super(CustomNavigationToolbar, self).release_zoom(event)
if self.func_recreate_gridlines is not None:
self.func_recreate_gridlines()
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
print('ApplicationWindow::__init__')
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
self.layout = QVBoxLayout(self._main)
self.fig = Figure()
self.canvas = FigureCanvas(self.fig)
self.toolbar = CustomNavigationToolbar(self.canvas, self,
coordinates=True,
func_recreate_gridlines=self.recreate_gridlines)
self.layout.addWidget(self.canvas)
self.addToolBar(self.toolbar)
# figure setup taken from gridlines example at
# https://scitools.org.uk/cartopy/docs/latest/matplotlib/gridliner.html
projection = ccrs.RotatedPole(pole_longitude=120.0, pole_latitude=70.0)
self.ax = self.canvas.figure.add_subplot(1, 1, 1, projection=projection)
self.ax.set_extent([-6, 3, 48, 58], crs=ccrs.PlateCarree())
self.ax.coastlines(resolution='10m')
self._gl = None
self.recreate_gridlines()
def recreate_gridlines(self):
print('ApplicationWindow::recreate_gridlines')
print(' remove old gridliner artists')
if self._gl is not None:
for artist_coll in [self._gl.xline_artists, self._gl.yline_artists, self._gl.xlabel_artists, self._gl.ylabel_artists]:
for a in artist_coll:
a.remove()
self.ax._gridliners = []
print(' self.ax.gridlines()')
self._gl = self.ax.gridlines(crs=ccrs.PlateCarree(),
draw_labels=True, dms=True, x_inline=False, y_inline=False)
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()

matplotlib, How to save subplot with scrollbar

Hi I searched nice module to save subplot with scrollbar. but this module can only show plot not save file showing with 'Segmentation fault (core dumped)' message...
I don't know why.. Can you help me..? Actually when I saved only one plot, It was working well. but When I saved multiple plot with for loop function, that message show and block script running..
```python````
import matplotlib.pyplot as plt
from PyQt4 import QtGui
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class ScrollableWindow(QtGui.QMainWindow):
def __init__(self, fig, savefile):
self.qapp = QtGui.QApplication([])
QtGui.QMainWindow.__init__(self)
self.widget = QtGui.QWidget()
self.setCentralWidget(self.widget)
self.widget.setLayout(QtGui.QVBoxLayout())
self.widget.layout().setContentsMargins(0,0,0,0)
self.widget.layout().setSpacing(0)
self.fig = fig
self.canvas = FigureCanvas(self.fig)
self.canvas.draw()
self.scroll = QtGui.QScrollArea(self.widget)
self.scroll.setWidget(self.canvas)
self.nav = NavigationToolbar(self.canvas, self.widget)
self.widget.layout().addWidget(self.nav)
self.widget.layout().addWidget(self.scroll)
#self.show()
plt.savefig(savefile)
#exit()
#exit(self.qapp.exec_())
# create a figure and some subplots
fig, axes = plt.subplots(ncols=4, nrows=5, figsize=(16,16))
for ax in axes.flatten():
ax.plot([2,3,5,1])
# pass the figure to the custom window
a = ScrollableWindow(fig,'test.png')

Plotting with a for loop in matplotlib, with lists

form_class = uic.loadUiType("GUI.ui")[0] # Load the UI
class MainWindowClass(QtGui.QMainWindow, form_class):
def __init__(self, target, Afb, np, conversion, parent=None):
QtGui.QMainWindow.__init__(self, parent)
self.setupUi(self)
if self.RadioButton.isChecked():
Ids, Ugs = unipolar_steuerkennlinie(self.target, self.Afb, self.np, self.conversion)
def plot_matplotlibwidget(self, *args):
for x in range(0, 40):
self.matplotlibwidget.axes.plot([args[0][x]], [[args[1][x]]])
self.matplotlibwidget.axes.figure.canvas.draw_idle()
Hi, I have this function and I don't really know how to plot correctly. Now it shows nothing but the does change the scale.
Since it is hard to completely replicate your code and error, I've created you a simple minimal GUI that has a MPL widget and draws a few random lines.
from PyQt4 import QtGui, QtCore
import sys
import functools
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
import numpy as np
from matplotlib.figure import Figure
class test(QtGui.QWidget):
def __init__(self,parent=None):
self.widget=QtGui.QWidget.__init__(self, parent)
# Button to plot
self.btnPlot = QtGui.QPushButton('Plot')
self.btnPlot.connect(self.btnPlot, QtCore.SIGNAL('clicked()'), self.btnPlotPressed)
# Line edit for number of lines
self.qleN = QtGui.QLineEdit(str(0))
# Create canvas
self.setupMPLWidget()
# Layout
self.hbox = QtGui.QHBoxLayout()
self.hbox.addWidget(self.btnPlot)
self.hbox.addWidget(self.qleN)
self.hbox.addWidget(self.canvas)
self.setLayout(self.hbox)
self.show()
def btnPlotPressed(self):
"""Plots a few lines."""
# Get number of buttons to add
n=int(self.qleN.text())
# Generate some data
xs,ys=self.randData(n)
# Plot
self.plot_matplotlibwidget(xs,ys)
def randData(self,n):
"""Creates n random data sets."""
ys=[]
xs=[]
for i in range(n):
xs.append(np.arange(n-i))
ys.append(np.random.random(size=xs[-1].shape))
return xs,ys
def plot_matplotlibwidget(self,*args):
"""Plots list of datasets."""
for x in range(0, len(args[0])):
self.ax.plot(args[0][x], args[1][x])
self.ax.figure.canvas.draw_idle()
def setupMPLWidget(self):
"""Sets up a MPL figure to draw on."""
# Create parenting widget
self.plotFrame = QtGui.QWidget()
self.plotFrame.setMaximumWidth(1)
# Create Figure
self.fig = Figure( dpi=100)
#self.fig.set_size_inches(100,100,forward=True)
self.canvas = FigureCanvas(self.fig)
self.canvas.setParent(self.plotFrame)
self.ax = self.fig.add_subplot(111)
def main():
#Creating application
app = QtGui.QApplication(sys.argv)
main_win = test()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
It has a little QLE where you can specify how many lines you want to draw. I hope this helps.

How to display a value in a PyQt text field using matplotlib's object picking function?

I am using PyQt 4 for a basic GUI and matplotlib for a plot from which I want to read the coordinates of the plotted data points. Based on these examples (simple picking example), I have the simple problem that I cannot display the coordinates of a data point in a text field such as QtGui.QLabel(). I do not understand why I cannot call the instance Window.msg in the method onpick(). Probably it is because the instance it not given to the method. I only have a basic understanding of object oriented programming (but I am working on it), so the problem is my lack of knowledge.
My question: How to display the coordinates of chosen data (by clicking on it) from a matplotlib plot in my GUI based on PyQT (in that case in my label lbl)?
Also, it would be nice to highlight the chosen data point in the plot.
Here is my code (working):
import numpy as np
import matplotlib.pyplot as plt
from PyQt4 import QtGui
import sys
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
import matplotlib.pyplot as plt
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.initUI()
def initUI(self):
self.msg = '0'
# a figure instance to plot on
self.figure = plt.figure()
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
# a label
self.lbl = QtGui.QLabel(self.msg)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.lbl)
self.setLayout(layout)
self.plot()
def plot(self):
# random data
data = [np.random.random() for i in range(10)]
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
ax.hold(False)
# plot data
line, = ax.plot(data, 'o', picker=5) # 5 points tolerance
self.canvas.draw()
self.canvas.mpl_connect('pick_event', Window.onpick)
def onpick(self):
thisline = self.artist
xdata = thisline.get_xdata()
ydata = thisline.get_ydata()
ind = self.ind
# show data
self.msg = (xdata[ind], ydata[ind])
print(self.msg)
# This does not work:
#Window.lbl.setText(self.msg)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
The self is being overlapped by the picker (not sure why). In any case this should work:
import numpy as np
import matplotlib.pyplot as plt
from PyQt4 import QtGui
import sys
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
import matplotlib.pyplot as plt
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.initUI()
def initUI(self):
self.msg = '0'
# a figure instance to plot on
self.figure = plt.figure()
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
# a label
self.lbl = QtGui.QLabel(self.msg)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.lbl)
self.setLayout(layout)
self.plot()
def changelabel(arg):
main.lbl.setText(str(arg[0])+' '+str(arg[1]))
def plot(self):
# random data
data = [np.random.random() for i in range(10)]
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
ax.hold(False)
# plot data
line, = ax.plot(data, 'o', picker=5) # 5 points tolerance
self.canvas.draw()
self.canvas.mpl_connect('pick_event', Window.onpick)
def onpick(self):
thisline = self.artist
xdata = thisline.get_xdata()
ydata = thisline.get_ydata()
ind = self.ind
# show data
self.msg = (xdata[ind], ydata[ind])
print(self.msg)
# Window.changelabel(self.msg)
main.lbl.setText(str(self.msg[0])+' '+str(self.msg[1]))
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
, the change is in the setText function, since I call it directly from the variable (no self or Window).
main.lbl.setText(str(self.msg[0])+' '+str(self.msg[1]))

Categories