How to Modify the Navigation Toolbar Easily in a Matplotlib Figure Window

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



Leave a reply



Submit