The primary problem here is when plotting too much data (~36 subplots) using FigureCanvasTkAgg
the Scrollbar
of Tkinter being too sluggish and slow when the user is scrolling. Is there any solution to this problem, I don't want my user to experience this. Thanks.
You can try this code here:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.root.title("Scrollable Matplotlib Plot with 9 Subplots")
# Create a frame for the canvas and scrollbars
frame = tk.Frame(root)
frame.pack(fill=tk.BOTH, expand=True)
# Create a canvas for the Matplotlib figure
self.canvas = tk.Canvas(frame)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Create vertical scrollbar
self.v_scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL, command=self.canvas.yview)
self.v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Create horizontal scrollbar
self.h_scrollbar = tk.Scrollbar(root, orient=tk.HORIZONTAL, command=self.canvas.xview)
self.h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
# Configure canvas with scrollbars
self.canvas.configure(yscrollcommand=self.v_scrollbar.set)
self.canvas.configure(xscrollcommand=self.h_scrollbar.set)
# Create a figure and multiple subplots
self.fig, self.axs = plt.subplots(6, 6, figsize=(10, 10)) # 3x3 grid of subplots
# Generate some data for the plots
x = np.linspace(0, 10, 100)
for i, ax in enumerate(self.axs.flat):
ax.plot(x, np.sin(x + i), label=f'Sine Wave {i+1}')
ax.set_title(f'Plot {i+1}')
ax.legend()
# Add the Matplotlib figure to the canvas
self.figure_canvas = FigureCanvasTkAgg(self.fig, master=self.canvas)
self.figure_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Bind the canvas to scrolling
self.canvas.create_window((0, 0), window=self.figure_canvas.get_tk_widget(), anchor='nw')
# Update the scroll region
self.update_scroll_region()
# Bind the resize event to update scroll region
self.root.bind("<Configure>", self.on_resize)
def update_scroll_region(self):
"""Update the scroll region based on the canvas content."""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def on_resize(self, event):
"""Update the scroll region when the window is resized."""
self.update_scroll_region()
# Create the main window
root = tk.Tk()
app = App(root)
root.mainloop()
The slowdown is caused by the scrollbar triggering an unnecessary redraw of the underlying matplotlib plot, and matplotlib is slow, i think this could be fixed on matplotlib side, but you could work around it.
You can skip the matplotlib redraw by subclassing the FigureCanvasTkAgg
and skipping the next redraw if the canvas is being scrolled, since the redraw happens in the future, not inside the scrolling callback.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
import time
class SkippableFigureCanvas(FigureCanvasTkAgg):
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)
self._skip_redraw = False
def skip_redraw(self):
self._skip_redraw = True
def draw(self):
if self._skip_redraw:
self._skip_redraw = False
return
super().draw()
class App:
def __init__(self, root):
self.root = root
self.root.title("Scrollable Matplotlib Plot with 9 Subplots")
self.last_update_time = time.time()
# Create a frame for the canvas and scrollbars
frame = tk.Frame(root)
frame.pack(fill=tk.BOTH, expand=True)
# Create a canvas for the Matplotlib figure
self.canvas = tk.Canvas(frame)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Create vertical scrollbar
self.v_scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL, command=self.canvas.yview)
self.v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Create horizontal scrollbar
self.h_scrollbar = tk.Scrollbar(root, orient=tk.HORIZONTAL, command=self.canvas.xview)
self.h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
def my_h_scroll_command(*args):
self.figure_canvas.skip_redraw()
self.canvas.xview(*args)
def my_v_scroll_command(*args):
self.figure_canvas.skip_redraw()
self.canvas.yview(*args)
self.v_scrollbar.configure(command=my_v_scroll_command)
self.h_scrollbar.configure(command=my_h_scroll_command)
# Configure canvas with scrollbars
self.canvas.configure(yscrollcommand=self.v_scrollbar.set)
self.canvas.configure(xscrollcommand=self.h_scrollbar.set)
# Create a figure and multiple subplots
self.fig, self.axs = plt.subplots(6, 6, figsize=(10, 10)) # 3x3 grid of subplots
# Generate some data for the plots
x = np.linspace(0, 10, 100)
for i, ax in enumerate(self.axs.flat):
ax.plot(x, np.sin(x + i), label=f'Sine Wave {i+1}')
ax.set_title(f'Plot {i+1}')
ax.legend()
# Add the Matplotlib figure to the canvas
self.figure_canvas = SkippableFigureCanvas(self.fig, master=self.canvas)
self.figure_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Bind the canvas to scrolling
self.canvas.create_window((0, 0), window=self.figure_canvas.get_tk_widget(), anchor='nw')
# Update the scroll region
self.update_scroll_region()
# Bind the resize event to update scroll region
self.root.bind("<Configure>", self.on_resize)
def update_scroll_region(self):
"""Update the scroll region based on the canvas content."""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def on_resize(self, event):
"""Update the scroll region when the window is resized."""
self.update_scroll_region()
# Create the main window
root = tk.Tk()
app = App(root)
root.mainloop()
the new parts are the SkippableFigureCanvas
which allows you to skip drawing the next matplotlib plot when needed, and this next part which attaches a callback that is called whenever the user moves the scrollbar to skip the next redraw of the matplotlib plot.
def my_h_scroll_command(*args):
self.figure_canvas.skip_redraw()
self.canvas.xview(*args)
def my_v_scroll_command(*args):
self.figure_canvas.skip_redraw()
self.canvas.yview(*args)
self.v_scrollbar.configure(command=my_v_scroll_command)
self.h_scrollbar.configure(command=my_h_scroll_command)
Note that the above "work around" may break things in the future if matplotlib "fixes" it on their side, use at your own risk, it is also bug-prone if you update the plot while the user is scrolling it ... so don't write code that updates the plot while scrolling, it could also break if you have animations or other things that update the plot over time. (you might need to manually call Draw
to force an update)