Bind event to click on plot in tkinter canvas - python

I can bind a click event to a plot (i. e. to print the coordinates that were clicked) like so:
from matplotlib.backend_bases import MouseButton
import matplotlib.pyplot as plt
x = [1, 2, 3, 4]
y = [2, 3, 1, 4]
fig, ax = plt.subplots()
ax.plot(x, y)
def plotClick(event):
if event.button == MouseButton.LEFT:
print('Clicked at x=%f, y=%f' %(event.xdata, event.ydata))
plt.connect('button_press_event', plotClick)
plt.show()
I'd like to do the same thing with a plot that is contained within a canvas inside a tkinter window like so:
from matplotlib.backend_bases import MouseButton
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
window = tk.Tk()
fig = Figure(figsize=(5, 3))
ax = fig.add_subplot(111)
x = [1, 2, 3, 4]
y = [2, 3, 1, 4]
line, = ax.plot(x, y)
canvas = FigureCanvasTkAgg(fig)
canvas.draw()
canvas.get_tk_widget().pack()
def plotClick(event):
if event.button == MouseButton.LEFT:
print('Clicked at x=%f, y=%f' %(event.xdata, event.ydata))
window.mainloop()
What would I have to do to accomplish the same behavior here?
Note: I am aware that one can bind events directly to the canvas using
canvas.get_tk_widget().bind('<Button-1>', plotClick)
with
def plotClick(event):
print('Clicked at x=%f, y=%f' %(event.x, event.y))
That however uses pixel coordinates on the canvas instead of the coordinates in the plot.

Instead of using plt.connect, use
canvas.mpl_connect('button_press_event', plotClick)
Using this you can access the coordinates in the plot with event.xdata and event.ydata, but you can also still access the (pixel) coordinates on the canvas using event.x and event.y.

Related

PySimpleGUI with slider and DataFrame

I am trying to make a simple interactive plot, where a value of single variable can be changed by the slider. I already have non-interactive code which is working, where I use pandas DataFrame. I am using PySimpleGUI, to which I am completely new. I have managed to get a window with the slider, but no plot which changes. Where do I do an error in my code?
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import copy as copy
import PySimpleGUI as sg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
V_A=5
dfN_A=np.array([[-1, 1], [0, 3], [1, -1]])
dfN=copy.deepcopy(dfN_A)
dfN[:,-1]=dfN_A[:,-1]*V_A
DC = pd.DataFrame(dfN,
columns=['X','value'])
_VARS = {'window': False,
'fig_agg': False,
'pltFig': False,
'V_A': 5}
def draw_figure(canvas, figure):
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
figure_canvas_agg.draw()
figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
return figure_canvas_agg
AppFont = 'Any 16'
SliderFont = 'Any 14'
sg.theme('black')
layout = [[sg.Canvas(key='figCanvas', background_color='#FDF6E3')],
# pad ((left, right), (top, bottom))
[sg.Text(text="V_A :",
font=SliderFont,
background_color='#FDF6E3',
pad=((0, 0), (10, 0)),
text_color='Black'),
sg.Slider(range=(-10,10), size=(60, 10),
orientation='h', key='-SLIDERV_A-')],
[sg.Button('Exit', font=AppFont, pad=((540, 0), (0, 0)))]]
_VARS['window'] = sg.Window('Simple GUI', layout, finalize=True,resizable=True, element_justification='center', font='Helvetica 18')
def drawChart():
_VARS['pltFig'] = plt.figure()
fig = plt.gcf()
DC.plot(y='value',x='X')
_VARS['fig_agg'] = draw_figure(
_VARS['window']['figCanvas'].TKCanvas, _VARS['pltFig'])
def updateChart():
_VARS['fig_agg'].get_tk_widget().forget()
plt.cla()
plt.clf()
_VARS['fig_agg'] = draw_figure(
_VARS['window']['figCanvas'].TKCanvas, _VARS['pltFig'])
def updateDataV_A(val):
_VARS['V_A'] = val
updateChart()
drawChart()
while True:
event, values = _VARS['window'].read(timeout=2000)
if event == sg.WIN_CLOSED or event == 'Exit':
break
elif event == '-SliderV_A-':
updateDataV_A(int(values['-SliderV_A-']))
_VARS['window'].close()
There're some issues in your code. Try not to say much about those issues, for easily, so a different code here for you.
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
# 1. Define the class as the interface between matplotlib and PySimpleGUI
class Canvas(FigureCanvasTkAgg):
"""
Create a canvas for matplotlib pyplot under tkinter/PySimpleGUI canvas
"""
def __init__(self, figure=None, master=None):
super().__init__(figure=figure, master=master)
self.canvas = self.get_tk_widget()
self.canvas.pack(side='top', fill='both', expand=1)
# 2. Define a function to draw the figure
def plot_figure(var):
ax.cla()
ax.plot(x0, y0*var)
canvas.draw() # Rendor figure into canvas
# 3. Initial Values
var = 5
x0, y0 = np.array([-1, 0, 1]), np.array([1, 3, -1])
# 4. create PySimpleGUI window
sg.theme('DarkBlue3')
layout = [
[sg.Canvas(size=(640, 480), key='Canvas')],
[sg.Text(text="Var"),
sg.Slider(range=(-10, 10), default_value=var, size=(10, 20), expand_x=True, enable_events=True, orientation='h', key='Slider')],
[sg.Push(), sg.Button('Exit'), sg.Push()],
]
window = sg.Window('Simple GUI', layout, finalize=True, resizable=True)
# 5. Create a matplotlib canvas under sg.Canvas or sg.Graph
fig = Figure()
ax = fig.add_subplot()
canvas = Canvas(fig, window['Canvas'].Widget)
# 6. initial for figure
plot_figure(var)
# 7. PySimpleGUI event loop
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Exit'):
break
elif event == 'Slider':
var = values[event]
plot_figure(var)
# 8. Close window to exit
window.close()

How to make Matplotlib cursor display without mouse in python with Tkinter

I have one, two, or three plots in a canvas of Tkinter. Let's assume I don't have any Mouse. There only is a keyboard. I want a cursor on these plots that moves at the same horizontal axis on all graphs in the canvas and of course when the Tk window shows up the yellow cursors (mplcursors) on all graphs be at the first point of graphs without any mouse movement and clicks(if you run the following code there are two cursors yellow and green, yellow appears by clicking on the line). The yellow cursors must move together (the same x-axes point) by right/left arrow keys on the keyboard.
from matplotlib.widgets import MultiCursor
import matplotlib.pyplot as plt
import numpy as np
from tkinter import *
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import mplcursors
root = Tk()
fig = Figure(figsize=(8,6))
fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
x = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y = np.sin(x)
z = np.cos(x)
ax1.plot(x, y, label="sin function")
ax1.legend(loc="upper right")
ax2.plot(x, z, label="cos function")
canvas = FigureCanvasTkAgg(fig,root)
c1 = mplcursors.cursor(ax1, multiple=False,hover=False,highlight=False,bindings={"toggle_visible": "h", "toggle_enabled": "e","left": "left","right": "right"})
c2 = mplcursors.cursor(ax2, multiple=False,hover=False,highlight=False,bindings={"toggle_visible": "h", "toggle_enabled": "e","left": "left","right": "right"})
multi = MultiCursor(fig.canvas, (ax1, ax2), color='g', lw=0.5, horizOn=True, vertOn=True)
ax2.legend(loc="upper left")
canvas.draw()
#pack canavs
canvas.get_tk_widget().pack(side = BOTTOM, fill=BOTH, expand=True)
#create Navigation Toolbar
toolbar = NavigationToolbar2Tk(canvas, root) #add to canvas
canvas._tkcanvas.pack(side = TOP, fill=BOTH, expand=True)
root.mainloop()
Do you have any suggestions? Can I also make the Multi-Cursor (green lines) to move with mplcursors on plots and stick to them too?
I have tried multi-threading from this link, used mouse controlling from this link and getting pixels of first plot points by this question but I couldn't handle the first display event and it needed the mouse to be clicked on both plots separately to show the cursors.
My plots are more complicated, they are signals and I have multiple pages in my application.
The following code does what I wanted But couldn't solve to keep the green Multi-Cursor on the yellow ones! (Exploring yet)
the following code uses MouseEvent and _process_event which clicks on the plot by default, when calling it you should declare when this process happens?, on which plot, in which points!
from matplotlib.widgets import MultiCursor
import matplotlib.pyplot as plt
import numpy as np
from tkinter import *
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import mplcursors
import subprocess
from matplotlib.backend_bases import KeyEvent, MouseEvent
import copy
def _process_event(name, ax, coords, *args):
ax.viewLim # unstale viewLim.
if name == "__mouse_click__":
# So that the dragging callbacks don't go crazy.
_process_event("button_press_event", ax, coords, *args)
_process_event("button_release_event", ax, coords, *args)
return
display_coords = ax.transData.transform(coords)
if name in ["button_press_event", "button_release_event",
"motion_notify_event", "scroll_event"]:
event = MouseEvent(name, ax.figure.canvas, *display_coords, *args)
elif name in ["key_press_event", "key_release_event"]:
event = KeyEvent(name, ax.figure.canvas, *args, *display_coords)
else:
raise ValueError(f"Unknown event name {name!r}")
ax.figure.canvas.callbacks.process(name, event)
root = Tk()
fig = Figure(figsize=(8,6))
fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
x = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y = np.sin(x)
z = np.cos(x)
ax1.plot(x, y, label="sin function")
ax1.legend(loc="upper right")
ax2.plot(x, z, label="cos function")
canvas = FigureCanvasTkAgg(fig,root)
c1 = mplcursors.cursor(ax1, multiple=False,hover=False,highlight=False,bindings={"toggle_visible": "h", "toggle_enabled": "e","left": "left","right": "right"})
c2 = mplcursors.cursor(ax2, multiple=False,hover=False,highlight=False,bindings={"toggle_visible": "h", "toggle_enabled": "e","left": "left","right": "right"})
multi = MultiCursor(fig.canvas, (ax1, ax2), color='g', lw=0.5, horizOn=True, vertOn=True)
ax2.legend(loc="upper left")
_process_event("__mouse_click__", ax1, (x[0], y[0]), 1)
sel, = c1.selections
c1.add_selection(copy.copy(sel))
_process_event("__mouse_click__", ax2, (x[0], z[0]), 1)
sel2, = c2.selections
c2.add_selection(copy.copy(sel2))
canvas.draw()
#pack canavs
canvas.get_tk_widget().pack(side = BOTTOM, fill=BOTH, expand=True)
#create Navigation Toolbar
toolbar = NavigationToolbar2Tk(canvas, root) #add to canvas
canvas._tkcanvas.pack(side = TOP, fill=BOTH, expand=True)
root.mainloop()

New Graph not getting displayed on Canvas PySimpleGUI and Matplotlib

I need to reset the graph based on two buttons. It displays the first graph if button1 pressed but no the second one after pressing button2.
First graph is shown properly but need to clear the canvas
Code below:
import matplotlib.pyplot as plt
import matplotlib
import numpy
import numpy as np
import PySimpleGUI as psg
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
matplotlib.use('TkAgg')
def make_graph_and_put_on_canvas(x, y, xlabel, ylabel, graph_title, canvas):
figure,ax=plt.subplots()
ax.plot(x, y)
ax.set(xlabel=xlabel, ylabel=ylabel,
title=graph_title)
ax.grid()
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
figure_canvas_agg.draw()
figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
return figure_canvas_agg
if __name__ == '__main__':
layout = [[psg.B("Button1"),psg.B("Button2")],[psg.Canvas(key="canvas")]]
Graph = psg.Window(title="Graph", layout=layout, size=(500, 500))
while (True):
event, Value = Graph.read()
if event == psg.WINDOW_CLOSED:
Graph.close()
break
if event=="Button1":
#Make the first graph for y=2x
x=[0,1,2,3]
y=[0,2,4,6]
make_graph_and_put_on_canvas(x, y, "x", "y", "title",Graph["canvas"].TKCanvas)
if event=="Button2":
# Make the first graph for y=3x
x = [0, 1, 2, 3]
y = [0, 3, 6, 9]
make_graph_and_put_on_canvas(x, y, "x", "y", "title",Graph["canvas"].TKCanvas)
FigureCanvasTkAgg(figure, canvas) will create a new canvas in Graph["canvas"].TKCanvas.
With option side='top' when pack, you will get the new canvas from top to bottom of Graph["canvas"].TKCanvas. Second Button did make second graph but on bottom of your window, invisible for size = (500, 500) in your window.
call following code once if you want new graph show on same figure,
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
clear figure or axis by
clf() # clear figure
cla() # clear axis
update figure on canvas after all plots on figure done
figure_canvas_agg.draw()
Following code just work, not optimized.
import matplotlib.pyplot as plt
import matplotlib
import numpy
import numpy as np
import PySimpleGUI as psg
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
matplotlib.use('TkAgg')
def make_graph_and_put_on_canvas(x, y, xlabel, ylabel, graph_title, canvas):
ax.cla()
ax.plot(x, y)
ax.set(xlabel=xlabel, ylabel=ylabel, title=graph_title)
ax.grid()
figure_canvas_agg.draw()
return figure_canvas_agg
if __name__ == '__main__':
layout = [[psg.B("Button1"),psg.B("Button2")],[psg.Canvas(key="canvas")]]
Graph = psg.Window(title="Graph", layout=layout, size=(500, 500), finalize=True)
figure, ax = plt.subplots()
canvas = Graph["canvas"].Widget
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
while (True):
event, Value = Graph.read()
if event == psg.WINDOW_CLOSED:
break
if event=="Button1":
# Make the first graph for y=2x
x=[0,1,2,3]
y=[0,2,4,6]
make_graph_and_put_on_canvas(x, y, "x", "y", "title1", canvas)
if event=="Button2":
# Make the first graph for y=3x
x = [0, 1, 2, 3]
y = [0, 3, 6, 9]
make_graph_and_put_on_canvas(x, y, "x", "y", "title2", canvas)
Graph.close()

Toolbar in matplotlib not working in tkinter canvas

I am trying to create a interactive 3d plot that can be rotated, I was able to do this using the following, however I am unable to reset the orientation when I click on the home button. I tried all the answers posted before me none seem to work.
matplolib version - 3.2.2
import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
import numpy as np
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
np.random.seed(1968001)
def randrange(n, vmin, vmax):
'''
Helper function to make an array of random numbers having shape (n, )
with each number distributed Uniform(vmin, vmax).
'''
return (vmax - vmin) * np.random.rand(n) + vmin
fig = Figure()
# Unable to have rotation otherwise so defined this function
def func():
ax = fig.add_subplot(111, projection='3d')
n = 100
# For each set of style and range settings, plot n random points in the box
# defined by x in [23, 32], y in [0, 100], z in [zlow, zhigh].
for m, zlow, zhigh in [('o', -50, -25), ('^', -30, -5)]:
xs = randrange(n, 23, 32)
ys = randrange(n, 0, 100)
zs = randrange(n, zlow, zhigh)
ax.scatter(xs, ys, zs, marker=m)
ax.set_xlabel('X Label')
ax.set_ylabel('Y Label')
ax.set_zlabel('Z Label')
top = tk.Tk()
top_frame = ttk.Frame(top)
top_frame.pack()
canv = FigureCanvasTkAgg(fig, master=top_frame)
canv.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
func()
canv.draw()
toolbar = NavigationToolbar2Tk(canv, top_frame)
toolbar.update()
top.mainloop()
By home button this is what I mean
click here
Edit 1: I've changed the plt.figure to figure.Figure as pointed out by Henry Yik, but still, it's not working

How to enable function while matplotlibs zoom to rectangle is activated?

On a tkinter canvas I added the matplotlib navigation toolbar and plotted several lines which I want to pick and modify using a function. I also want to be able to zoom into the canvas and select the lines without activating and deactivating the 'zoom to rectangle' everytime I want to excecute the defined function. Is there a way to simultaneously use matplotlib's zoom function and my defined function?
import sys
if sys.version_info[0] < 3:
import Tkinter as Tk
else:
import tkinter as Tk
from matplotlib.figure import Figure
import numpy as np
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
root = Tk.Tk()
fig = Figure()
ax = fig.add_subplot(111)
canvas = FigureCanvasTkAgg(fig, master=root)
nav = NavigationToolbar2Tk(canvas, root)
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
x = np.arange(10)
ax.plot(x, x, picker=True)
ax.plot(x, 2 * x, picker=True)
ax.plot(x, 3 * x, picker=True)
ax.plot(x, 4 * x, picker=True)
def _onPick(event):
thisline = event.artist
thisline.set_linewidth(5)
fig.canvas.draw()
fig.canvas.callbacks.connect('pick_event', _onPick)
root.mainloop()
You can connect a "button_press_event" and find out yourself if the click occured at a place that is covered by a line. That is slightly more complicated than using the inbuilt picker.
x = np.arange(10)
ax.plot(x, x, picker=6)
ax.plot(x, 2 * x, picker=6)
ax.plot(x, 3 * x, picker=15)
ax.plot(x, 4 * x, picker=1)
def _onPick(event):
update = False
if event.inaxes == ax:
for line in ax.lines:
if line.get_picker():
cont, ind = line.contains(event)
if cont:
line.set_linewidth(5)
update=True
if update:
fig.canvas.draw_idle()
fig.canvas.callbacks.connect('button_press_event', _onPick)
Note that I redefined the picker to state a radius here, which might be useful to make sure one actually hits a line.

Categories

Resources