Embedding an interactive Matplotlib plot into Tkinter? - python

I have code for an interactive plot, which allows viewing a 3D image through scrolling slice-wise with the mouse roll. It includes also a slide bar to adjust contrast.
I have been trying to embed this into a Tkinter GUI, for example with help of this sample code: https://matplotlib.org/examples/user_interfaces/embedding_in_tk.html
But I don't really understand where my code is supposed to go in there.
This is the application I currently have:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Slider
class IndexTracker(object):
def __init__(self, ax, X):
self.ax = ax
ax.set_title('use scroll wheel to navigate images')
self.X = X
rows, cols, self.slices = X.shape
self.ind = self.slices//2
self.im = ax.imshow(self.X[:, :, self.ind], cmap='gray')
self.update()
def onscroll(self, event):
print("%s %s" % (event.button, event.step))
if event.button == 'up':
self.ind = (self.ind + 1) % self.slices
else:
self.ind = (self.ind - 1) % self.slices
self.update()
def contrast(self, event):
print('Changing contrast')
print(smax.val)
self.im.set_clim([0,smax.val])
self.update()
def update(self):
self.im.set_data(self.X[:, :, self.ind])
self.ax.set_ylabel('slice %s' % self.ind)
self.im.axes.figure.canvas.draw()
##### Create some random volumetric data
im = np.array(np.random.rand(10,10,10))
##### Initialize Tracker object with the data and Slider
fig, ax = plt.subplots(1,1)
axmax = fig.add_axes([0.25, 0.01, 0.65, 0.03])
smax = Slider(axmax, 'Max', 0, np.max(im), valinit=50)
tracker = IndexTracker(ax, im)
fig.canvas.mpl_connect('scroll_event', tracker.onscroll)
smax.on_changed(tracker.contrast)
plt.show()
I don't understand what is exacty that I need to embed into the Tkinter application, is it fig, or IndexTracker ? How do I replace fig.canvas.mpl_connect('scroll_event', tracker.onscroll) so it works within the TKinter GUI?

There is nothing special embedding this into tkinter - you create a FigureCanvasTkAgg object first, and then do the rest. The only thing you need to change is instead of plt, you need to use Figure which is shown in the sample you quoted.
import numpy as np
from matplotlib.widgets import Slider
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class IndexTracker(object):
...
import tkinter as tk
root = tk.Tk()
fig = Figure()
canvas = FigureCanvasTkAgg(fig, root)
canvas.get_tk_widget().pack(fill="both", expand=True)
im = np.array(np.random.rand(10,10,10))
ax = fig.subplots(1,1)
axmax = fig.add_axes([0.25, 0.01, 0.65, 0.03])
smax = Slider(axmax, 'Max', 0, np.max(im), valinit=50)
tracker = IndexTracker(ax, im)
canvas.mpl_connect('scroll_event', tracker.onscroll)
canvas.mpl_connect('button_release_event', tracker.contrast) #add this for contrast change
root.mainloop()

Related

Matplotlib FuncAnimation Created twice - duplicate when embbeded in tkinter

I have a troubleing bug that i just could not understands it's origin. Several days of attempts and still no luck.
I'm trying to create a line cursor that correspond to played audio with FuncAnimation and for some reason, the animation is created twice ONLY when the callback (line_select_callback) that activates the function is triggered from RectangleSelector widget after drawing wiith the mouse. when I use a standard TK button to activate the SAME function (line_select_callback), it operates well.
some debugging code with reevant prints is present.
I've created minimal working example.
My guess is it has something to do with the figure that is not attached to the tk window, and is silently activated in addition to the embedded figure, I'm not really sure.
Any help will be very much appreciated, Thanks! :)
import os
import threading
import tkinter as tk
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg)
from matplotlib.widgets import RectangleSelector
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from matplotlib import animation
class LineAnimation:
def __init__(self, fig, ax):
print(' enter LineAnimation ctor')
# Parameters
self.ax = ax
self.fig = fig
self.xdata, self.ydata = [], []
self.ln, = plt.plot([], [], 'ro')
# Print figures list
figures = [manager.canvas.figure
for manager in matplotlib._pylab_helpers.Gcf.get_all_fig_managers()]
print('figures BEFORE animation: ', figures)
self.animation = animation.FuncAnimation(fig=self.fig,
func=self.update,
init_func=self.init,
frames=np.linspace(0, 2 * np.pi, 128),
interval=25,
blit=True, repeat=False,
cache_frame_data=False)
self.fig.canvas.draw()
# Print figures list
figures = [manager.canvas.figure
for manager in matplotlib._pylab_helpers.Gcf.get_all_fig_managers()]
print('figures AFTER animation: ', figures, '\n')
def init(self):
# Prints for debugging
print('\nenter init animate')
print('Thread id: ', threading.get_ident())
print('Process id: ', os.getpid(), '\n')
# Init
self.ax.set_xlim(0, 2*np.pi)
self.ax.set_ylim(-1, 1)
return self.ln,
def update(self, frame):
self.xdata.append(frame)
self.ydata.append(np.sin(frame))
self.ln.set_data(self.xdata, self.ydata)
return self.ln,
class Example:
def __init__(self):
# init window
self.root = tk.Tk(className=' Species segmentation')
self.fig, self.ax = plt.subplots()
# init sine audio file
self.fs = 44100
self.dur = 2
self.freq = 440
self.x = np.sin(2*np.pi*np.arange(self.fs*self.dur)*self.freq/self.fs)
# plt.ion()
# Embedd in tk
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().grid()
# Plot something
self.N = 100000
self.xp = np.linspace(0, 10, self.N)
self.ax.plot(self.xp, np.sin(2*np.pi*self.xp))
self.ax.set_title(
"Plot for demonstration purpuse")
# init Rectangle Selector
self.RS = RectangleSelector(self.ax, self.line_select_callback,
drawtype='box', useblit=True,
button=[1, 3], # avoid using middle button
minspanx=5, minspany=5,
spancoords='pixels', interactive=True,
rectprops={'facecolor': 'yellow', 'edgecolor': 'black', 'alpha': 0.15, 'fill': True})
self.canvas.draw()
# plt.show()
tk.mainloop()
def line_select_callback(self, eclick, erelease):
print('enter line_select_callback')
self.anim = LineAnimation(
self.fig,
self.ax)
self.fig.canvas.draw()
# plt.show()
Example()
I managed to isolate the cause for this issue: The presence of the
rectangle selector (which uses blitting) and the use of animation (which also uses blitting) on the same axes.
I've managed to create the animation properly, but only when I disabled the rectangle selector
self.RS.set_active(False)
self.RS.update()
self.canvas.flush_events()
and removed his artists (i needed to do that manually in my code) using:
for a in self.RS.artists:
a.set_visible(False)
after that, The animation worked properly.

Is it possible to control a matplotlib graph using a Tkinter Scrollbar?

I would like to make an equivalent of the function FuncAnimation from matplotlib.animation, in which I could control the current plotted data using the scrollbar.
Say you have a data array which contains data points to be plotted at each time i. When using FuncAnimation, you first need to define a function ( here animate(i) ) which will be called for each time i = 1 to len(data[:,0]) :
def animate(i):
ax.plot(data[i,:])
anim = FuncAnimation(fig, animate, interval=100, frames=len(data[:,0]))
plt.draw()
plt.show()
but you cannot control the time i, like with a play/stop functionality. What I would like to do is to call the function animate(i), with i being the position of the scrollbar.
I found this example ( using the events from matplotlib:
https://matplotlib.org/3.2.1/users/event_handling.html )
but the mpl_connect doesn't have a "scrollbar_event".
import tkinter
from random import randint
import matplotlib as plt
import numpy as np
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
root = tkinter.Tk()
root.wm_title("Embedding in Tk")
#create figure
fig = Figure(figsize=(5, 4), dpi=100)
ax = fig.add_axes([0,0,1,1])
ax.imshow(np.array([[0,10],[23,40]]))
#create canvas with figure
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
def on_key_press(event):
ax.clear()
ax.imshow(np.array([[randint(0,30),randint(0,30)],[randint(0,30),randint(0,30)]]))
canvas.draw_idle()
key_press_handler(event, canvas)
print("you pressed {}".format(event.key))
#connect canvas to event function
canvas.mpl_connect("key_press_event", on_key_press)
def _quit():
root.quit() # stops mainloop
root.destroy() # this is necessary on Windows to prevent
# Fatal Python Error: PyEval_RestoreThread: NULL tstate
button = tkinter.Button(master=root, text="Quit", command=_quit)
button.pack(side=tkinter.BOTTOM)
tkinter.mainloop()
Actually the scroll functionality is given by matplotlib widgets !!
The example below works fine :
import matplotlib
import tkinter as Tk
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from myPytHeader import *
matplotlib.use('TkAgg')
root = Tk.Tk()
root.wm_title("Embedding in TK")
fig = plt.Figure(figsize=(8, 6))
canvas = FigureCanvasTkAgg(fig, root)
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
nDt = nbLines("grid.dat")
nDx = nbGridPoints("grid.dat")
grid = np.zeros( (nDt,nDx) ) ; loadData("grid.dat", grid)
valu = np.zeros( (nDt,nDx) ) ; loadData("valu.dat", valu)
ax=fig.add_subplot(111)
fig.subplots_adjust(bottom=0.25)
ax_time = fig.add_axes([0.12, 0.1, 0.78, 0.03])
s_time = Slider(ax_time, 'Time', 0, nDt, valinit=0, valstep=1)
def update(val):
frame = int(s_time.val)
ax.clear()
ax.set(xlim=(-0.05, 1.05), ylim=(-0.05, 1.25))
ax.grid()
ax.scatter(grid[frame,:], valu[frame,:], color='b', marker='.')
fig.canvas.draw_idle()
s_time.on_changed(update)
Tk.mainloop()
After all these years I've found solutions to my problems here, I am in debt !!!
Here is the final solution I came with.
I hope it can be useful to someone someday somehow !!
import matplotlib
import numpy as np
import tkinter as tk
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# load data
nDt = 1000
nDx = 400
grd = np.zeros( (nDt,nDx) )
val = np.zeros( (nDt,nDx) )
for t in np.arange(nDt):
for x in np.arange(nDx):
grd[t,x] = x / nDx
val[t,x] = (x / nDx) * (t/nDt) * np.sin(10 * 2*np.pi * (t-x)/nDt)
matplotlib.use('TkAgg')
root = tk.Tk()
root.wm_title("Embedding in TK")
fig = plt.Figure(figsize=(8, 6))
canvas = FigureCanvasTkAgg(fig, root)
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
ax=fig.add_subplot(111)
fig.subplots_adjust(bottom=0.25)
ax.set(xlim=(-0.05, 1.05), ylim=(-1.05, 1.05))
ax.grid()
scat = ax.scatter(grd[0,:], val[0,:], color='b', marker='.')
ax_time = fig.add_axes([0.12, 0.1, 0.78, 0.03])
s_time = Slider(ax_time, 'Time', 0, nDt, valinit=0, valstep=1)
i_anim = 0
i_relative = 0
i_current = 0
def updateGraph(i):
y_i = val[i,:]
scat.set_offsets(np.c_[grd[i,:], y_i])
def updateFromAnim(i):
global i_anim
global i_current
global i_relative
i_anim = i
i_current = i + i_relative
s_time.set_val(i_current)
updateGraph(i_current)
def updateFromScroll(val):
global i_anim
global i_current
global i_relative
i_relative = int(s_time.val) - i_anim
i_current = int(s_time.val)
updateGraph(i_current)
def onClick():
global anim_running
if anim_running:
anim.event_source.stop()
anim_running = False
else:
anim.event_source.start()
anim_running = True
start_button = tk.Button(root, text="START/STOP", command=onClick)
start_button.pack()
anim_running = True
anim = FuncAnimation(fig, updateFromAnim, interval=100, frames=nDt)
s_time.on_changed(updateFromScroll)
tk.mainloop()

matplotlib.widgets.TextBox interaction is slow when figure contains several subplots

Below is python code to demonstrate the problem.
If there are 2 rows and 2 columns of images, for example, typing/erasing in the textbox is reasonably fast. However, if there are 5 rows and 5 columns, typing/erasing in the textbox is quite slow. If the xticks and yticks are drawn, interaction is even slower. So, it seems as if the entire figure is redrawn after every keystroke.
Is there a solution for this (apart from putting the textbox on a separate figure)?
(My development platform is MacOS Mojave, Python 3.7.5.)
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.widgets import TextBox
class Textbox_Demo(object):
def __init__(self):
self.fig = plt.figure(figsize=(8,8))
self.string = 'label'
self.rows = 5 # reducing rows speeds up textbox interaction
self.cols = 5 # reducing cols speeds up textbox interaction
self.plot_count = self.rows * self.cols
self.gs = gridspec.GridSpec(self.rows, self.cols,
left=0.05, right=1-0.02, top=1-.02, bottom=0.10, wspace=0.3, hspace=0.4)
for k in range(self.plot_count):
ax = self.fig.add_subplot(self.gs[k])
#ax.set_xticks([]) # showing axes slows textbox interaction
#ax.set_yticks([]) # showing axes slows textbox interaction
data = np.atleast_2d(np.sin(np.linspace(1,255,255) * 50))
ax.imshow(data, aspect="auto", cmap='ocean')
# this is the user-input textbox
tb_axis = plt.axes([0.125, 0.02, 0.8, 0.05])
self.tb = TextBox(tb_axis, 'Enter label:', initial=self.string, label_pad=0.01)
self.tb.on_submit(self.on_submit)
plt.show()
def on_submit(self, text):
pass
if __name__ == "__main__":
Textbox_Demo()
Matplotlib's TextBox is inherently slow, because it uses the drawing tools provided by matplotlib itself and hence redraws the complete figure upon changes.
I would propose to use a text box of a GUI kit instead. For example for PyQt this might look like:
import numpy as np
import sys
from matplotlib.backends.backend_qt5agg import (
FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.backends.qt_compat import QtCore, QtWidgets
import matplotlib.gridspec as gridspec
from matplotlib.figure import Figure
class Textbox_Demo(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QVBoxLayout(self._main)
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.fig = Figure(figsize=(8,8))
self.canvas = FigureCanvas(self.fig)
layout.addWidget(self.canvas)
self.addToolBar(NavigationToolbar(self.canvas, self))
self._textwidget = QtWidgets.QWidget()
textlayout = QtWidgets.QHBoxLayout(self._textwidget)
self.textbox = QtWidgets.QLineEdit(self)
self.textbox.editingFinished.connect(self.on_submit)
# or, if wanting to have changed apply directly:
# self.textbox.textEdited.connect(self.on_submit)
textlayout.addWidget(QtWidgets.QLabel("Enter Text: "))
textlayout.addWidget(self.textbox)
layout.addWidget(self._textwidget)
self.fill_figure()
def fill_figure(self):
self.string = 'label'
self.rows = 5 # reducing rows speeds up textbox interaction
self.cols = 5 # reducing cols speeds up textbox interaction
self.plot_count = self.rows * self.cols
self.gs = gridspec.GridSpec(self.rows, self.cols,
left=0.05, right=1-0.02, top=1-.02, bottom=0.10, wspace=0.3, hspace=0.4)
for k in range(self.plot_count):
ax = self.fig.add_subplot(self.gs[k])
#ax.set_xticks([]) # showing axes slows textbox interaction
#ax.set_yticks([]) # showing axes slows textbox interaction
data = np.atleast_2d(np.sin(np.linspace(1,255,255) * 50))
ax.imshow(data, aspect="auto", cmap='ocean')
def on_submit(self):
text = self.textbox.text()
print(text)
pass
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = Textbox_Demo()
app.show()
qapp.exec_()

python matplotlib how to open an image with class

I'm pretty new with matplotlib and I tried to write a class to open and close image through matplotlib, here is the code:
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
class ptlShow():
def __init__(self, file, pos):
plt.rcParams['toolbar'] = 'None'
fig, ax = plt.subplots(figsize=(1, 1.4))
fig.subplots_adjust(0, 0, 1, 1)
ax.axis("off")
im = plt.imread(file)
ax.imshow(im)
fig.canvas.manager.window.overrideredirect(1)
plt.get_current_fig_manager().window.wm_geometry(pos)#
plt.show()
def close(self):
plt.close
a = ptlShow('1.jpg', '+700+100')
b = ptlShow('2.jpg', '+500+100')
a.close()
b.close()
but finally I have only one instance of image and close doesn't work, what I'm doing wrong ! Thanks!
plt.show() is meant to be called exactly once at the end of the script as it takes over the event loop. Code coming after that is not executed until all figures are closed.
You would probably want to close a figure by clicking on it, so you can register the close method to a button_press_event. Note that plt.close is just the function - you would want to call it: plt.close().
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
class ptlShow():
def __init__(self, file, pos):
plt.rcParams['toolbar'] = 'None'
self.fig, ax = plt.subplots(figsize=(1, 1.4))
self.fig.subplots_adjust(0, 0, 1, 1)
ax.axis("off")
im = plt.imread(file)
ax.imshow(im)
self.fig.canvas.manager.window.overrideredirect(1)
self.fig.canvas.manager.window.wm_geometry(pos)#
self.fig.canvas.mpl_connect("button_press_event", self.close)
def close(self, event=None):
plt.close(self.fig)
a = ptlShow('1.jpg', '+700+100')
b = ptlShow('2.jpg', '+500+100')
plt.show()
In order to close the window after some time t, you can use tkinters .after method, .after(t, func):
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
class ptlShow():
def __init__(self, file, pos):
plt.rcParams['toolbar'] = 'None'
self.fig, ax = plt.subplots(figsize=(1, 1.4))
self.fig.subplots_adjust(0, 0, 1, 1)
ax.axis("off")
im = plt.imread(file)
ax.imshow(im)
self.fig.canvas.manager.window.overrideredirect(1)
self.fig.canvas.manager.window.wm_geometry(pos)#
self.fig.canvas.mpl_connect("button_press_event", self.close)
self.fig.canvas.mpl_connect("draw_event", self.delayed_close)
def delayed_close(self,event=None):
self.fig.canvas.manager.window.after(1000, self.close)
def close(self, event=None):
plt.close(self.fig)
a = ptlShow('house.png', '+700+100')
b = ptlShow('house.png', '+500+100')
plt.show()

Preserve zoom settings in interactive navigation of matplotlib figure

Is there a way to preserve the interactive navigation settings of a figure such that the next time the figure is updated the Zoom/Pan characteristics don't go back to the default values? To be more specific, if a zoom in a figure, and then I update the plot, is it possible to make the new figure appear with the same zoom settings of the previous one? I am using Tkinter.
You need to update the image instead of making a new image each time. As an example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
class DummyPlot(object):
def __init__(self):
self.imsize = (10, 10)
self.data = np.random.random(self.imsize)
self.fig, self.ax = plt.subplots()
self.im = self.ax.imshow(self.data)
buttonax = self.fig.add_axes([0.45, 0.9, 0.1, 0.075])
self.button = Button(buttonax, 'Update')
self.button.on_clicked(self.update)
def update(self, event):
self.data += np.random.random(self.imsize) - 0.5
self.im.set_data(self.data)
self.im.set_clim([self.data.min(), self.data.max()])
self.fig.canvas.draw()
def show(self):
plt.show()
p = DummyPlot()
p.show()
If you want to plot the data for the first time when you hit "update", one work-around is to plot dummy data first and make it invisible.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
class DummyPlot(object):
def __init__(self):
self.imsize = (10, 10)
self.data = np.random.random(self.imsize)
self.fig, self.ax = plt.subplots()
dummy_data = np.zeros(self.imsize)
self.im = self.ax.imshow(dummy_data)
self.im.set_visible(False)
buttonax = self.fig.add_axes([0.45, 0.9, 0.1, 0.075])
self.button = Button(buttonax, 'Update')
self.button.on_clicked(self.update)
def update(self, event):
self.im.set_visible(True)
self.data += np.random.random(self.imsize) - 0.5
self.im.set_data(self.data)
self.im.set_clim([self.data.min(), self.data.max()])
self.fig.canvas.draw()
def show(self):
plt.show()
p = DummyPlot()
p.show()
Alternately, you could just turn auto-scaling off, and create a new image each time. This will be significantly slower, though.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
class DummyPlot(object):
def __init__(self):
self.imsize = (10, 10)
self.fig, self.ax = plt.subplots()
self.ax.axis([-0.5, self.imsize[1] - 0.5,
self.imsize[0] - 0.5, -0.5])
self.ax.set_aspect(1.0)
self.ax.autoscale(False)
buttonax = self.fig.add_axes([0.45, 0.9, 0.1, 0.075])
self.button = Button(buttonax, 'Update')
self.button.on_clicked(self.update)
def update(self, event):
self.ax.imshow(np.random.random(self.imsize))
self.fig.canvas.draw()
def show(self):
plt.show()
p = DummyPlot()
p.show()

Categories

Resources