Search code examples
pythonmatplotlibgtkpyinstaller

Changing the icon in matplotlib navigation toolbar using GTK backend


Similar questions have been asked before on Stack Overflow, however, none of them involve the GTK backend, and none of them involve the need to change the path being searched by the backend. Similar questions here would include this and this among others. The relevant code for the GTK backend is here.

As an aside, in case it is relevant, I am freezing this Python script using PyInstaller, and the reason I want to "change" the icon is because on certain Windows computers I am finding that the icons are broken links for the Navigation Toolbar despite the images appearing in the correct location:

\dist\<script name>\matplotlib\mpl-data\images\<image_file>-symbolic.svg

as well as the images having the format expected by the GTK backend (f'{image_file}-symbolic.svg'). I am not sure why this problem is happening and have spent a lot of time getting nowhere, so I figured a possible fix might be to stash the images as part of my repot in a location of my choosing and forcing them to be used by the script (in case it is some weird environment variable or path issue). Using the example below, I can change the tooltip successfully and the image/icon, but I wouldn't be able to change the location of the image. The third argument of toolitems controls the image:

list of toolitems to add to the toolbar, format is:
(
   text, # the text of the button (often not visible to users)
   tooltip_text, # the tooltip shown on hover (where possible)
   image_file, # name of the image for the button (without the extension)
   name_of_method, # name of the method in NavigationToolbar2 to call
)

Apparently the image must be in the path expected and must be a name without an extension. The code for the backend specifies:

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 I can take an svg image and put it in the path I gave above, with the appended -symbolic at the end and get this to work. However, is there a way to do this in a path different than the default directory fed to the backend? The code that I am currently using is below:

from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3

class MyToolbar(NavigationToolbar2GTK3):
  def __init__(self, figure_canvas, window):
    self.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'),
        )

    NavigationToolbar2GTK3.__init__(self, figure_canvas, window)

self.navigation_toolbar = MyToolbar(self._gtkFigure, main)
self.navigation_toolbar.update()

My expectation is that the broken links coming up on some computers are the result of cbook._get_data_path (used by the backend) coming up different for some reason, so if I could change this in the script somehow, perhaps I can patch the problem? Any assistance or pointers would be appreciated!

Edit: After digging some more it looks like _get_data_path in cbook is just a call to matplotlib.get_data_path(). From here it doesn't appear using a custom data path is possible in matplotlib (I was looking for a .set_data_path() method). One of the contributors suggests "grab the native tk widgets and perform the right calls to iconphoto() and friends on them." Since I am not using the Tkinter backend I am still not sure how to proceed. I am fairly certain that the method I outlined above will not work, and another method is required, similar to here.


Solution

  • 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.