Update colorbar in an animated version but without matplotlib.animation

Dear all,
I would like to animate a 2D plot with plt.imshow() in tkinter. I managed this without matplotlib.animation. But now I would like to add a colobar. The problem is that the colorbar’s numbers are overlapping all the time. That looks really ugly.
Is there a way to update the colorbar without matplotlib.animation, please?
I found this discussion, but I would like to do it without matplotlib animate.

The code of my function is (an extraction):


self.fig = Figure(figsize=(7.5, 4), dpi=80)
self.ax0 = self.fig.add_subplot()
self.canvas = FigureCanvasTkAgg(self.fig, master=self._frame)

def plot_cam_1d_scan(self, mot_selected, cam_image, camera):
        """ Plot 1D scan with a camera, one motor, one camera """
        div = make_axes_locatable(self.ax0)
        cax = div.append_axes('right', '5%', '5%')
        vmin=np.min(cam_image)
        vmax=np.max(cam_image)
        cam_im=self.ax0.imshow(cam_image, interpolation="none",vmin=vmin, vmax=vmax)
        self.fig.colorbar(cam_im, cax=cax)
        self.ax0.set_title(f'1D Scan {camera}')
        self.ax0.set_xlabel(f"{camera} X")
        self.ax0.set_ylabel(f"{camera} Y")
        self.fig.canvas.draw_idle()
        self.fig.canvas.flush_events()

This animates the image, but the colorbar text is overwritten all the time and text overlaps.
What could I do to avoid this please?
Thank you

The reason you are seeing overlapping number is that you are creating a new colorbar axes, now AxesImage, and new colorbar each time through your loop.

The “better version” in the accepted answer of the SO question you link is exactly what you want to do (but do the draw_idle etc at the end as well).

1 Like

Hi tacaswell,
Thank you for your reply!
I created now a MWE with tkinter. It shows the problem with the colorbar (and my version does not adapt in color in the 2d plot for some reason).

Sorry, I don’t understand you. I would not like to use matplotlib animate, but I don’t know how to correct for example my (running) MWE. Could you have maybe a look at it, please? Much appreciated.

PS: When one pushes multiple times the scan button, one sees overlapping text.


import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import *

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib import cm
from mpl_toolkits.axes_grid1 import make_axes_locatable

from matplotlib import pyplot as plt

import numpy as np



class GUI():
    def __init__(self):
        """ Creates the main frame and links other frames to it. Only this layer should be visible to the model and controller. """

        self._master = tk.Tk()

        self._frame = tk.Frame(self._master)
        self._frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
        self.fig = Figure(figsize=(7.5, 4), dpi=80)
        self.ax0 = self.fig.add_subplot()

        self.canvas = FigureCanvasTkAgg(self.fig, master=self._frame)
        self.canvas.get_tk_widget().pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
        # Navigation toolbar for the canvas
        toolbar = NavigationToolbar2Tk(self.canvas, self._frame)
        toolbar.update()
        
        #plot button
        self.plotBut = tk.Button(self._frame, text="Plot 1D Scan")
        self.plotBut.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
        self.plotBut.bind("<Button-1>", self.create_scan_plot)

        #lets create a destroy button
        self.terminateButton=ttk.Button(self._frame, text="Exit GUI", command=self.on_destroy)
        self.terminateButton.pack(side=tk.TOP, fill=tk.BOTH, expand=1)

    def on_destroy(self):
        self._master.destroy()


    def create_scan_plot(self, event):
  
        #clear graph
        self.ax0.clear()
        self.canvas.draw()

        div = make_axes_locatable(self.ax0)
        cax = div.append_axes('right', '5%', '5%')

        for j in range(20):
            img_array=self.create_data_2d(j)

            vmin=np.min(img_array)
            vmax=np.max(img_array)
    
            cam_im=self.ax0.imshow(img_array, interpolation="none",vmin=vmin, vmax=vmax)
            self.fig.colorbar(cam_im, cax=cax)

            self.fig.canvas.draw_idle()
            self.fig.canvas.flush_events()
        
    
    def create_data_2d(self, i):
        hmpf=np.diag(range(i+1))
        print(hmpf.shape)
        return hmpf
    


    def on_create_scanplot(self, m):
        """ Connects the 1D scan plot button to a method """
        self.sidepanel.plotBut.bind("<Button>", m)


if __name__ == '__main__':

   c = GUI()
   
    # start the GUI
   #c._master.mainloop()
   c._master.mainloop()

Could you maybe show me how to fix this in my MWE below please ?
Many thanks. I don’t see what to improve. Thank you and much appreciated.

        div = make_axes_locatable(self.ax0)
        cax = div.append_axes('right', '5%', '5%')

These lines are creating a new axes for the color bar every time without removing the old one. What you are seeing is the text from all of the axes being overlaid (even though the main part of the top axes is fully covering the others).

            cam_im=self.ax0.imshow(img_array, interpolation="none",vmin=vmin, vmax=vmax)
            self.fig.colorbar(cam_im, cax=cax)

These lines are creating a new AxesImage objects and colorbar. Create the image and color bar once and then update the data / color limits of the image like the SO answer

    arr = frames[i]
    vmax = np.max(arr)
    vmin = np.min(arr)
    im.set_data(arr)
    im.set_clim(vmin, vmax)
    tx.set_text('Frame {0}'.format(i))

You should either create all of these objects (ax0, cax, cam_im, cbar) at application initialization time and then mutate them (like you are currently doing with self.ax0. If you want to re-create them in your button callbacks, then you may need to do a self.figure.clf() to get a clean slate.

FuncAnimation is arranging for a function to be called on a periodic clock to update the figure, however there is nothing about the functions you pass to FuncAnimation that are inherently tied to it (other than the signature it expects). Anything you do in that function for animation is doable in a callback from a UI and the principles carry over very directly.

These lines are creating a new axes for the color bar every time without removing the old one. What you are seeing is the text from all of the axes being overlaid (even though the main part of the top axes is fully covering the others).

Thank you for your reply. Can I remove the old one? Or should they go into the loop?

Create the image and color bar once and then update the data / color limits of the image like the SO answer

Don’t you not have to know the arrays in advanced for this? Or how can I create an empty imshow-plot and an empty plot and fill it later? Later is after button action.
The diagramm is here only a placeholder for something else. While a motor moves, a camera should take snapshots and display them on the canvas area.
I would only like to create the image and the color bar after my button action and then it should be attached to the self.ax0 axis.
I don’t see how the code you posted can do this.

You should either create all of these objects (ax0, cax, cam_im, cbar) at application initialization time and then mutate them (like you are currently doing with self.ax0 .

If I am not mistaken, self.ax0 is empty at the start. This is only a MWE. In the full code self.ax0 is also used to plot lineplot on it. I have 2 buttons, depending which button is pushed, self.ax0 gets different information (1d plot or a 2d plot), but both are animated. And the plot function/the resulting plot is then attached to the self.ax0 code. self.ax0 is then part of the canvas of tkinter.
I cannot initialise cam_im because I get the information only later from the running hardware after a button action.
The canvas should be empty at the start of the plotting (either 2d or 1d).

If you want to re-create them in your button callbacks, then you may need to do a self.figure.clf() to get a clean slate.

Is it the figure I have to clear or not the canvas area from tkinter?

FuncAnimation is arranging for a function to be called on a periodic clock to update the figure


Hm, that is difficult to understand. tkinter works as well periodically, why would one need this at all then? I have in another method a 1D plot, that is animated after button action. I plot every iteration a longer list of my x, y values as line plot on to self.ax0. I send in every iteration of hardware movement the new values with the previous ones to the canvas area.
Imshow has this problem with the colorbar in comparison to the 1D plot.

Is this maybe a bit more clearer where I struggle?

I do not have time to write up a complete answer, I apologize for the terseness.

The Figure (and the other Artists) are Matplotlib’s internal understanding / model of what your visualization is. At draw time we rasterize that model to an RGBA array and paint that onto the tk (or any GUI toolkit) canvas (or save to a file).

When you create and artist and add it to the Figure it will then be included in the next draw. Be aware that artists may fully cover each other, but are still drawn (try setting a low alpha on things to debug this).

I disagree this is a MWE, that it is embedded in a tk GUI is not important to understanding how Matplotlib works.


import numpy as np
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
div = make_axes_locatable(ax)
cax = div.append_axes('right', '5%', '5%')

im = ax.imshow([[], []], extent=[0, 5, 0, 5], clim=(0, 1))
fig.colorbar(im, cax=cax)

plt.pause(1)

im.set_data(np.random.random((10, 10)))
ax.set_title('set some data')
plt.pause(1)

im.set_clim(.25, .75)
ax.set_title('set clim')
plt.pause(1)


im.set_extent([1, 3, 1, 3])
ax.set_title('set extent')
plt.pause(1)

The general pattern is:

  1. be very careful about when / where you make new Aristst and add them to the figure
  2. Prefer to update existing Airtst rather than making new one when ever possible.

In a bit of self-promotion you may also be interested in https://blueskyproject.io for hardware control.

Hi, tacaswell,

No problem at all. I will try again and understand and report my progress here.

I disagree this is a MWE, that it is embedded in a tk GUI is not important to understanding how Matplotlib works

Okay, for me it was important to see it in combination with the GUI.

I try again and write the progress here. Many thanks.
Thank you for pointing out Bluesky. I chose tkiner to start with a simple GUI environment, but you are right, more complex systems will probably benefit from Bluesky.