How to modify the navigation toolbar easily in a matplotlib figure window?
The way I found to remove unwanted toolbar items is making a subclass, which is instantiated and used in a GTK application. As I manually create Figure, FigureCanvas and NavigationToolbar objects anyway, this was the easiest way.
class NavigationToolbar(NavigationToolbar2GTKAgg):
# only display the buttons we need
toolitems = [t for t in NavigationToolbar2GTKAgg.toolitems if
t[0] in ('Home', 'Pan', 'Zoom', 'Save')]
If you want to create custom buttons, you should take a look on the definition of NavigationToolbar2
in backend_bases. You can easily add your own entries to the toolitems
list and define appropriate callback functions in your toolbar subclass.
embed matplotlib with custom Navigation toolbar actions in the Mainwindow toolbar
You have to pass None as parent of the NavigationToolbar2QT:
import sys
import matplotlib
matplotlib.use("Qt5Agg")
from PyQt5.QtWidgets import QAction, QApplication, QMainWindow, QVBoxLayout, QWidget
from PyQt5.QtGui import QIcon
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
print("Text: Name of method")
for text, _, _, callback in NavigationToolbar2QT.toolitems:
if isinstance(text, str):
print(f"{text}: {callback}")
self.sc = MplCanvas(self, width=5, height=4, dpi=100)
self.mpl_toolbar = NavigationToolbar2QT(self.sc, None)
zoomact = QAction(QIcon("zoom.png"), "zoom", self)
zoomact.setShortcut("Ctrl+z")
zoomact.triggered.connect(self.mpl_toolbar.zoom)
self.toolbar = self.addToolBar("zoom")
self.toolbar.addAction(zoomact)
self.sc.axes.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])
widget = QWidget()
self.setCentralWidget(widget)
layout = QVBoxLayout(widget)
layout.addWidget(self.sc)
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
if __name__ == "__main__":
main()
Another option is to remove the QActions from the NavigationToolbar2QT:
import sys
import matplotlib
matplotlib.use("Qt5Agg")
from PyQt5.QtWidgets import QAction, QApplication, QMainWindow, QVBoxLayout, QWidget
from PyQt5.QtGui import QIcon
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
print("Text: Name of method")
for text, _, _, callback in NavigationToolbar2QT.toolitems:
if isinstance(text, str):
print(f"{text}: {callback}")
self.sc = MplCanvas(self, width=5, height=4, dpi=100)
self.toolbar = NavigationToolbar2QT(self.sc, self)
self.addToolBar(self.toolbar)
self.toolbar.clear()
zoomact = QAction(QIcon("zoom.png"), "zoom", self)
zoomact.setShortcut("Ctrl+z")
zoomact.triggered.connect(self.toolbar.zoom)
self.toolbar.addAction(zoomact)
self.sc.axes.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])
widget = QWidget()
self.setCentralWidget(widget)
layout = QVBoxLayout(widget)
layout.addWidget(self.sc)
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
if __name__ == "__main__":
main()
Changing the icon in matplotlib navigation toolbar using GTK backend
So the original problem was that on certain Windows computers (not all), the native images for the Navigation Toolbar of matplotlib were showing as broken links. When using self.toolitems
you can only specify the image name. Why is that? Well the GTK matplotlib backend has:
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(
str(cbook._get_data_path('images',
f'{image_file}-symbolic.svg'))),
Gtk.IconSize.LARGE_TOOLBAR)
So the file extension is already specified in the backend. My expectation was that cbook._get_data_path
was yielding a different path on the Windows computers where the broken links came up (as opposed to the ones where they did come up). As mentioned, cbook._get_data_path
is essentially a call to matplotlib.get_data_path()
and that could be used in the code to match the paths on the computers where this did and did not work. In the matplotlib API we have:
def get_data_path():
"""Return the path to Matplotlib data."""
return str(Path(__file__).with_name("mpl-data"))
The problem was that the paths came back the same for both. So this seemed to imply that the problem was in the GTK backend itself. I read somewhere that some computers have a problem with the SVG images, so you should use the PNG images instead. I changed the GTK backend to reflect that and this fixed the problem:
f'{image_file}-symbolic.svg' -> f'{image_file}.png'
I don't think it's the best idea to edit the matplotlib GTK backend script, obviously. Some others have suggested to simply take the PNG files, rename them as SVG files, and replace the originals in mpl-data
with those instead. That also seems sloppy. I am not sure what the best method would be, but for now I have this working and that is enough.
How to add an edit option to Tkinter Matplotlib navigation toolbar?
Matplotlib
can use different backends
to display plot window.
Normally it uses PyQt
but you can run it with Tk
import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('tkagg')
y = [i**2 for i in range(101)]
plt.plot(y)
plt.show()
PyQt
uses NavigationToolbar2QT which adds button to run function edit_properties
but Tk
uses NavigationToolbar2Tk which doesn't have this function.
They also use different code to create window with subplot options.
The same problem is when you create Tkinter
program.It has to use NavigationToolbar2Tk
which doesn't have edit_properties
.
You will have to create own NavgationToolbar
and add own function to edit properites.
import tkinter as tk # PEP8: `import *` is not preferred
import tkinter.ttk as ttk
import tkinter.messagebox as msg
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.pyplot as plt
class MyToolbar(NavigationToolbar2Tk):
def __init__(self, canvas, parent):
self.toolitems = (
('Home', 'Lorem ipsum dolor sit amet', 'home', 'home'),
('Back', 'consectetuer adipiscing elit', 'back', 'back'),
('Forward', 'sed diam nonummy nibh euismod', 'forward', 'forward'),
(None, None, None, None),
('Pan', 'tincidunt ut laoreet', 'move', 'pan'),
('Zoom', 'dolore magna aliquam', 'zoom_to_rect', 'zoom'),
# new button in toolbar
("Customize", "Edit axis, curve and image parameters", "subplots", "edit_parameters"),
(None, None, None, None),
('Subplots', 'putamus parum claram', 'subplots', 'configure_subplots'),
('Save', 'sollemnes in futurum', 'filesave', 'save_figure'),
)
super().__init__(canvas, parent)
def edit_parameters(self):
print("You have to create edit_parameters()")
msg.showwarning("Warning", "You have to create edit_parameters()")
# --- main ---
y = [i**2 for i in range(101)]
root = tk.Tk()
fig = plt.Figure(figsize=(5, 5), dpi=100)
plot1 = fig.add_subplot(111)
plot1.plot(y)
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().pack(fill='both', expand=True)
#canvas.draw()
#toolbar = NavigationToolbar2Tk(canvas, root)
toolbar = MyToolbar(canvas, root) # <-- uses own Toolbar
#toolbar.update()
root.mainloop()
Matplotlib Navigation Toolbar: remove Edit curves lines and axes parameters
You can remove it by adding the following to your NavigationToolbar
class
actions = self.findChildren(QtGui.QAction)
for a in actions:
if a.text() == 'Customize':
self.removeAction(a)
break
The reason you can't remove this particular button by modifying toolitems
is because it gets added to the toolbar separately after all the toolitems
entries have been added.
for text, tooltip_text, image_file, callback in self.toolitems:
if text is None:
self.addSeparator()
else:
a = self.addAction(self._icon(image_file + '.png'),
text, getattr(self, callback))
self._actions[callback] = a
if callback in ['zoom', 'pan']:
a.setCheckable(True)
if tooltip_text is not None:
a.setToolTip(tooltip_text)
if figureoptions is not None:
a = self.addAction(self._icon("qt4_editor_options.png"),
'Customize', self.edit_parameters)
a.setToolTip('Edit curves line and axes parameters')
Matplotlib navigation toolbar resets legendstyle
The issue is that the code run when you check the "(Re-)Generate legend" removes any legend you have and replaces it with the default legend using the artists discovered.
The relevant code is at
https://github.com/matplotlib/matplotlib/blob/e73d4e056588abc201335d8a491fd9cb37d4c296/lib/matplotlib/backends/qt_editor/figureoptions.py#L237-L246
if generate_legend:
draggable = None
ncol = 1
if axes.legend_ is not None:
old_legend = axes.get_legend()
draggable = old_legend._draggable is not None
ncol = old_legend._ncol
new_legend = axes.legend(ncol=ncol)
if new_legend:
new_legend.set_draggable(draggable)
which looks like only pull some of the state from the existing legend (the number of columns and if it is draggable) but not the other setting you are using.
I am going to create an issue on github, I think extending this to do a better job of cloning the configuration is a good first issue.
https://github.com/matplotlib/matplotlib/issues/17775
How to remove toolbar buttons from matplotlib
You can do it by adding following lines of code before creating a plot object:
import matplotlib as mpl
mpl.rcParams['toolbar'] = 'None'
If you want to delete some buttons selectively, you need to redefine the toolitems
variable instead:
from matplotlib import backend_bases
# mpl.rcParams['toolbar'] = 'None'
backend_bases.NavigationToolbar2.toolitems = (
('Home', 'Reset original view', 'home', 'home'),
('Back', 'Back to previous view', 'back', 'back'),
('Forward', 'Forward to next view', 'forward', 'forward'),
(None, None, None, None),
('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'),
(None, None, None, None),
('Save', 'Save the figure', 'filesave', 'save_figure'),
)
I have removed two lines from the original variable mpl.backend_bases.NavigationToolbar2.toolitems
which normally reads:
toolitems = (
('Home', 'Reset original view', 'home', 'home'),
('Back', 'Back to previous view', 'back', 'back'),
('Forward', 'Forward to next view', 'forward', 'forward'),
(None, None, None, None),
('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'),
('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'),
('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'),
(None, None, None, None),
('Save', 'Save the figure', 'filesave', 'save_figure'),
)
EDIT
I have realized that it works with backend 'TkAgg'. For the backend 'Qt5Agg' we need to do some additional monkey patching just after modifying toolitems
. Namely:
if matplotlib.get_backend() == 'Qt5Agg':
from matplotlib.backends.backend_qt5 import NavigationToolbar2QT
def _update_buttons_checked(self):
# sync button checkstates to match active mode (patched)
if 'pan' in self._actions:
self._actions['pan'].setChecked(self._active == 'PAN')
if 'zoom' in self._actions:
self._actions['zoom'].setChecked(self._active == 'ZOOM')
NavigationToolbar2QT._update_buttons_checked = _update_buttons_checked
Putting NavigationToolbar in different Window/ Panel as Figure
Since nobody has replied to my post yet, I want to post a possible approach that I just found here.
In this example, the toolbar is created but not implemented in the UI and hidden through self.toolbar.Hide()
.
The functionalities of the toolbar are then accessed via wx buttons and the toolbars attributes: self.toolbar.zoom()
. This way, the Buttons can be placed somewhere else in the window and possibly even in another window, though I have not tested the latter.
I hope this will at least help others somewhat if noone comes around with a better solution.
As for the wx._core.wxAssertionError
, I still do not know how to solve it. Especially, since this error appeared almost every time I wanted to try out some sample code that creates the toolbar in one way or another. Sometimes my workaround in the post helped but other times it did not.
How do I make the matplotlib toolbar able to dock to the side in a pyqt5 window?
The QToolBar provided by matplotlib is configured to only allow dragging on the top and information as seen in the source code:
self.setAllowedAreas(QtCore.Qt.ToolBarArea(
_to_int(_enum("QtCore.Qt.ToolBarArea").TopToolBarArea) |
_to_int(_enum("QtCore.Qt.ToolBarArea").BottomToolBarArea)))
The solution is to establish all areas:
self.toolbar.setAllowedAreas(QtCore.Qt.ToolBarArea.AllToolBarAreas)
Related Topics
How to Move to One Folder Back in Python
Pandas - Tokenizing Data Expected 1 Field Saw Multiple
How to Find the Unit Digits of a Specific Number
Sqlalchemy, Prevent Duplicate Rows
How to Smooth a Curve in the Right Way
Append Dataframes Together in for Loop
How to Open Excel File Fast in Python
Get Row Value of Maximum Count After Applying Group by in Pandas
Split List into Two Parts Based on Some Delimiter in Each List Element in Python
How to Test If a List Contains Another List as a Contiguous Subsequence
Python Searching for Partial Matches in a List
Create an Array With a Pre Determined Mean and Standard Deviation
Using Continue in a Try and Except Inside While-Loop
How to Create a Common Function to Execute a Python Script in Jenkins
Inserting a Python Datetime.Datetime Object into MySQL