I'm new to programming, and I tried to find the solution to related questions here, but it get me nowhere and I'm starting to bang the walls with my head now.
The problem is the following: I need to create a program with GUI for a university project. The idea is that I take data from big datasets, then a user can input a country name, and data for the selected countries will form a plot. I have 2 plots with different data I want to use, and I have 2 separate buttons for each type:
```
#function that graphs the first plot
def vaccine_cases_plot(vaccine_list, cases_list):
plt.scatter(
y = vaccine_list,
x = cases_list)
plt.ylabel("Percentage of fully vaccinated people")
plt.xlabel("Number of new cases per Million as of 01.12.2021")
plt.ioff()
return plt.gcf()
#function that graphs the second plot
def vaccine_gdp_plot(vaccine_list, gdp_list):
plt.scatter(
y = vaccine_list,
x = gdp_list)
plt.ylabel("Percentage of fully vaccinated people")
plt.xlabel("GDP per capita in 2019(USD)")
plt.ioff()
return plt.gcf()
#helper function to display plot on canvas
plt.gcf().subplots_adjust(left=0.15)
matplotlib.use("TkAgg")
def draw_figure(canvas, figure):
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
figure_canvas_agg.get_tk_widget().pack(side="right", fill="both", expand=1)
figure_canvas_agg.draw()
return figure_canvas_agg
#empty lists where data for countries, selected by user, will be stored
vaccines_to_plot = []
cases_to_plot = []
gdp_to_plot = []
#pysimplegui interface layout
interface_column = [
[
sg.Text("Select a countries you would like to see on the graph:", font=("Arial", 14))
],
[
sg.In(size =(25, 1), enable_events=True, key="country_selected"),
sg.Button("Add country")
],
[
sg.Text("Selected countries are: ", size=(20,10), font=("Arial", 14), key = "selected_countries")
],
[
sg.Button("Clear selection", key = "clear")
],
[
sg.Button("GDP vs. Vaccination rate", key = "gdp_vaccine"),
sg.Button("Vaccination vs. new cases per day", key = "vaccine_cases")
],
[
sg.Text(font=("Arial", 12), key="warning_message")
]
]
graph_column = [
[sg.Canvas(size=(500,500), key="canvas")]
]
layout = [
[
sg.Column(interface_column),
sg.VSeperator(),
sg.Column(graph_column),
]
]
window = sg.Window("Vaxxi-nation", layout, margins=(50, 50), finalize=True)
canv = window["canvas"].TKCanvas
#main loop
while True:
event, values = window.read()
if event == sg.WIN_CLOSED:
break
#User inputs name of the country, respective data goes to the lists, name of the country is
displayed
if event == "Add country":
selected_country = values["country_selected"]
if selected_country in new_cases_per_m and selected_country in fully_vaccinated and
selected_country in countries_gdp:
try:
vaccines_to_plot.append(float(fully_vaccinated[selected_country]))
cases_to_plot.append(float(new_cases_per_m[selected_country]))
gdp_to_plot.append(round(float(countries_gdp[selected_country]), 2))
display = window["selected_countries"]
display.update(display.get() + selected_country + '\n')
except:
window["warning_message"].update("One of the datasets doesn't have data for this country")
else:
window["warning_message"].update("One of the datasets doesn't have data for this country")
#Button to graph the first plot
if event == "gdp_vaccine":
draw_figure(canv, vaccine_gdp_plot(vaccines_to_plot, gdp_to_plot))
#button to graph the second plot
if event == "vaccine_cases":
draw_figure(canv, vaccine_cases_plot(vaccines_to_plot, cases_to_plot))
if event == "clear":
display.update('')
vaccines_to_plot = []
cases_to_plot = []
gdp_to_plot = []
event, values = window.read()
window.close()
```
Now, when I press one of the buttons for the first time, the plot displays on canvas just as I want it to be. But if I'd like to display the other plot, instead of being rewritten over the previous one, it creates a new plot, replaces the old one with it, and makes a copy of a new plot on the right.
What I want to have, is when a new button is pressed (or the old one is pressed again) the new plot should replace the old one. I looked up for many ways to do that, but I have a feeling that there should be something simple and obvious that I fail to see here. The last thing I tried and gave up after was to delete the canvas each time the button is pressed and then draw it anew. I tried to do it like this:
#first button is clicked
if event == "gdp_vaccine":
canv.TKCanvas.delete("all")
draw_figure(canv, vaccine_gdp_plot(vaccines_to_plot, gdp_to_plot))
But it literally did nothing (although no error was thrown). I will really appreciate any help here since I've been struggling with it for the whole day already and there's no way this thing should be that complicated.
Following code just to delete all items in sg.Canvas, not canvas of matplotlib figure in sg.Canvas.
canv.TKCanvas.delete("all")
It is not easy to read your long and incomplete code.
Here's my code to demo two graphs and redraw again and again, maybe it can help you.
import math
from matplotlib import use as use_agg
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import PySimpleGUI as sg
def pack_figure(graph, figure):
canvas = FigureCanvasTkAgg(figure, graph.Widget)
plot_widget = canvas.get_tk_widget()
plot_widget.pack(side='top', fill='both', expand=1)
return plot_widget
def plot_figure(index, theta):
fig = plt.figure(index) # Active an existing figure
ax = plt.gca() # Get the current axes
x = [degree for degree in range(1080)]
y = [math.sin((degree+theta)/180*math.pi) for degree in range(1080)]
ax.cla() # Clear the current axes
ax.set_title(f"Sensor Data {index}")
ax.set_xlabel("X axis")
ax.set_ylabel("Y axis")
ax.set_xscale('log')
ax.grid()
plt.plot(x, y) # Plot y versus x as lines and/or markers
fig.canvas.draw() # Rendor figure into canvas
# Use Tkinter Agg
use_agg('TkAgg')
layout = [[sg.Graph((640, 480), (0, 0), (640, 480), key='Graph1'), sg.Graph((640, 480), (0, 0), (640, 480), key='Graph2')]]
window = sg.Window('Matplotlib', layout, finalize=True)
# Initial
graph1 = window['Graph1']
graph2 = window['Graph2']
plt.ioff() # Turn the interactive mode off
fig1 = plt.figure(1) # Create a new figure
ax1 = plt.subplot(111) # Add a subplot to the current figure.
fig2 = plt.figure(2) # Create a new figure
ax2 = plt.subplot(111) # Add a subplot to the current figure.
pack_figure(graph1, fig1) # Pack figure under graph
pack_figure(graph2, fig2)
theta1 = 0 # theta for fig1
theta2 = 90 # theta for fig2
plot_figure(1, theta1)
plot_figure(2, theta2)
while True:
event, values = window.read(timeout=10)
if event == sg.WINDOW_CLOSED:
break
elif event == sg.TIMEOUT_EVENT:
theta1 = (theta1 + 40) % 360
plot_figure(1, theta1)
theta2 = (theta2 + 40) % 260
plot_figure(2, theta2)
window.close()
Related
I have the following code, I want my GUI to change my two graphs everytime I press the button, but only the one in the left changes. Can someone please help me.
It's very strange because I took care of saving all the canvas and that way I can edit them with first using .forget() but for some reason it really doesn't work for the first graph.
import PySimpleGUI as sg
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# VARS CONSTS:
# New figure and plot variables so we can manipulate them
_VARS = {'window': False,
'fig_agg1': False,
'pltFig1': False,
'fig_agg2': False,
'pltFig2': False}
dataSize = 1000 # For synthetic data
# Helper Functions
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
def update_graph(zoom,or1,or2):
updateChart(1,zoom,or1,or2)
updateChart(2,zoom,or1,or2)
# \\ -------- PYSIMPLEGUI -------- //
AppFont = 'Any 16'
sg.theme('DarkTeal12')
#layout = [[sg.Canvas(key='figCanvas')],
# [sg.Button('Update', font=AppFont), sg.Button('Exit', font=AppFont)]]
control_col=sg.Column([
[sg.Frame('Zoom',layout = [[sg.Slider(range = (0,100), orientation = 'h', key = '-ZOOM-')]])],
[sg.Frame('Orientation 1',layout = [[sg.Slider(range = (-180,180), orientation = 'h', key = '-OR1-')]])],
[sg.Frame('Orientation 2',layout = [[sg.Slider(range = (-180,180), orientation = 'h', key = '-OR2-')]])],
[sg.Checkbox('Blackhole 1' , key = '-BH1-')],
[sg.Checkbox('Blackhole 2' , key = '-BH2-')],
[sg.Checkbox('Blackhole 3' , key = '-BH3-')],
[sg.Button('Show', key = '-SHOW-')]
])
graph1_col=sg.Column([[sg.Canvas(key = '-CANVAS1-')]])
graph2_col=sg.Column([[sg.Canvas(key = '-CANVAS2-')]])
layout=[[control_col,graph1_col,graph2_col]]
#_VARS['window'] = sg.Window('Such Window',
# layout,
# finalize=True,
# resizable=True,
# location=(100, 100),
# element_justification="right")
_VARS['window'] = sg.Window('Visualization', layout, finalize = True)
# \\ -------- PYSIMPLEGUI -------- //
# \\ -------- PYPLOT -------- //
def makeSynthData():
xData = np.random.randint(100, size=dataSize)
yData = np.random.randint(100, size=dataSize)
zData = np.random.randint(100, size=dataSize)
return (xData, yData, zData)
def drawChart(number):
_VARS['pltFig'+str(number)] = plt.figure()
dataXYZ = makeSynthData()
#plt.plot(dataXYZ[0], dataXYZ[1],dataXYZ[2] '.k')
ax = plt.axes(projection='3d')
ax.scatter3D(dataXYZ[0], dataXYZ[1], dataXYZ[2], c=dataXYZ[2], cmap='Greens')
_VARS['fig_agg'+str(number)] = draw_figure(
_VARS['window']['-CANVAS'+str(number)+'-'].TKCanvas, _VARS['pltFig'+str(number)])
# Recreate Synthetic data, clear existing figre and redraw plot.
def updateChart(number,zoom,or1,or2):
_VARS['fig_agg'+str(number)].get_tk_widget().forget()
dataXYZ = makeSynthData()
# plt.cla()
plt.clf()
#plt.plot(dataXYZ[0], dataXYZ[1], '.k')
ax = plt.axes(projection='3d')
ax.scatter3D(dataXYZ[0], dataXYZ[1], dataXYZ[2], c=dataXYZ[2], cmap='Greens')
ax.view_init(or1, or2)
_VARS['fig_agg'+str(number)] = draw_figure(
_VARS['window']['-CANVAS'+str(number)+'-'].TKCanvas, _VARS['pltFig'+str(number)])
# \\ -------- PYPLOT -------- //
drawChart(1)
drawChart(2)
# MAIN LOOP
while True:
event, values = _VARS['window'].read(timeout = 50)
if event == sg.WIN_CLOSED:
break
if event == '-SHOW-':
print(values)
update_graph(
values['-ZOOM-'],
values['-OR1-'],
values['-OR2-'])
_VARS['window'].close()
For plt.figure
Option num: A unique identifier for the figure.
If a figure with that identifier already exists, this figure is made active and returned.
Following code set which figure activated to draw
def drawChart(number):
_VARS['pltFig'+str(number)] = plt.figure()
After
drawChart(1)
drawChart(2)
Active figure set to figure 2 and not been changed until script end.
Try to update following statement in your code.
def drawChart(number):
_VARS['pltFig'+str(number)] = plt.figure(num=number)
def updateChart(number,zoom,or1,or2):
_VARS['fig_agg'+str(number)].get_tk_widget().forget()
dataXYZ = makeSynthData()
plt.figure(num=number)
I'm working on UI for a virus simulation me and a friend are making and I'm really struggling to change the background color of the UI. I took most of the code from the one of the demo projects because i couldn't figure out how to implement matplotlib with pysimplegui so there's some things I don't fully understand but usually with pysimplegui it's as simple as sg.theme="color to change the main background color but it isn't working this time. Any help would be really appreciated, thanks.
import PySimpleGUI as sg
import numpy as np
import tkinter
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
def draw_figure(canvas, fig):
if canvas.children:
for child in canvas.winfo_children():
child.destroy()
figure_canvas_agg = FigureCanvasTkAgg(fig, master=canvas)
figure_canvas_agg.draw()
figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)
# ------------------------------- PySimpleGUI CODE
layout = [
[sg.Text("Population size: "), sg.Input(key="-POPULATIONSIZE-")],
[sg.Text("Duration: "), sg.Input(key="-DURATION-")],
[sg.Text("R Number: "), sg.Input(key="-RNUMBER-")],
[sg.Text("Starting Infections: "), sg.Input(key="-STARTINGINFECTIONS-")],
[sg.B('OK'), sg.B('Exit')],
[sg.Canvas(key='controls_cv')],
[sg.T('Figure:')],
[sg.Column(
layout=[
[sg.Canvas(key='fig_cv',
size=(400 * 2, 400)
)]
],
background_color='#DAE0E6',
pad=(0, 0)
)],
]
window = sg.Window('Virus Simulation', layout,)
while True:
event, values = window.read()
print(event, values)
if event in (sg.WIN_CLOSED, 'Exit'):
break
elif event is 'OK':
# ------------------------------- PASTE YOUR MATPLOTLIB CODE HERE
plt.figure(1)
fig = plt.gcf()
DPI = fig.get_dpi()
# ------------------------------- you have to play with this size to reduce the movement error when the mouse hovers over the figure, it's close to canvas size
fig.set_size_inches(404 * 2 / float(DPI), 404 / float(DPI))
# -------------------------------
x = list(range(1, 100))
y = list(range(1, 100))
plt.plot(x, y, color="r")
plt.title('Virus Infections Data')
plt.xlabel('Time in Days')
plt.ylabel('Infections')
plt.grid()
# ------------------------------- Instead of plt.show()
draw_figure(window['fig_cv'].TKCanvas, fig,)
window.close()
You can change the background color by specifying a Hex Color Code in the following argument under layout:
background_color='#DAE0E6'
You can use a Color Picker like this one https://htmlcolorcodes.com/color-picker/ to get your color
You can also use:
window = sg.Window('Virus Simulation', layout, background_color='hex_color_code')
To change the color of a window object
I'm a tad confused in 2 ways.
1 - I don't see a call to sg.theme() in your code, so I don't know where it was in the code. WHERE it is placed matters. Always place the theme as early as possible, definitely before making your layout.
2 - I don't know what "it isn't working this time means". Again, it's more of needing to see a complete example to get it.
The sample code in the question that you said normally works was weirdly formatted so something must have been scrambled.
The question shows: sg.theme="color
But as Jason has pointed out, the value passed to sg.theme() is more than a color. The "Theme Name Formula" is described in the main PySimpleGUI documentation here - https://pysimplegui.readthedocs.io/en/latest/#theme-name-formula. In case there's a problem getting to that section, here's what it says:
Theme Name Formula
Themes names that you specify can be "fuzzy". The text does not have to match exactly what you see printed. For example "Dark Blue 3" and "DarkBlue3" and "dark blue 3" all work.
One way to quickly determine the best setting for your window is to simply display your window using a lot of different themes. Add the line of code to set the theme - theme('Dark Green 1'), run your code, see if you like it, if not, change the theme string to 'Dark Green 2' and try again. Repeat until you find something you like.
The "Formula" for the string is:
Dark Color #
or
Light Color #
Color can be Blue, Green, Black, Gray, Purple, Brown, Teal, Red. The # is optional or can be from 1 to XX. Some colors have a lot of choices. There are 13 "Light Brown" choices for example.
If you want to only change the background color of your theme, then you can use individual color names or hex values. sg.theme_background_color('#FF0000') or sg.theme_background_color('red') will set the background color to red.
Hope that helps with themes.
Nice work on using the PSG coding conventions. Looking at your code was effortless as a result. Zero guesswork as to what I was seeing. Great to see and it helps in numerous ways when you use them.
I tinkered to change the background color and found this solution.
Create a theme as a dictionary
Add the theme with theme_add_new function
Specify this theme
The question code edited:
import PySimpleGUI as sg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
new_theme = {"BACKGROUND": '#DAE0E6', "TEXT": sg.COLOR_SYSTEM_DEFAULT, "INPUT": sg.COLOR_SYSTEM_DEFAULT,
"TEXT_INPUT": sg.COLOR_SYSTEM_DEFAULT, "SCROLL": sg.COLOR_SYSTEM_DEFAULT,
"BUTTON": sg.OFFICIAL_PYSIMPLEGUI_BUTTON_COLOR, "PROGRESS": sg.COLOR_SYSTEM_DEFAULT, "BORDER": 1,
"SLIDER_DEPTH": 1, "PROGRESS_DEPTH": 0
}
sg.theme_add_new('MyTheme', new_theme)
sg.theme('MyTheme')
def draw_figure(canvas, fig):
if canvas.children:
for child in canvas.winfo_children():
child.destroy()
figure_canvas_agg = FigureCanvasTkAgg(fig, master=canvas)
figure_canvas_agg.draw()
figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)
# ------------------------------- PySimpleGUI CODE
layout = [
[sg.Text("Population size: "), sg.Input(key="-POPULATIONSIZE-")],
[sg.Text("Duration: "), sg.Input(key="-DURATION-")],
[sg.Text("R Number: "), sg.Input(key="-RNUMBER-")],
[sg.Text("Starting Infections: "), sg.Input(key="-STARTINGINFECTIONS-")],
[sg.B('OK'), sg.B('Exit')],
[sg.Canvas(key='controls_cv')],
[sg.T('Figure:')],
[sg.Column(
layout=[
[sg.Canvas(key='fig_cv',
size=(400 * 2, 400)
)]
],
# background_color='#DAE0E6',
pad=(0, 0)
)],
]
window = sg.Window('Virus Simulation', layout,)
while True:
event, values = window.read()
print(event, values)
if event in (sg.WIN_CLOSED, 'Exit'):
break
elif event == 'OK':
# ------------------------------- PASTE YOUR MATPLOTLIB CODE HERE
plt.figure(1)
fig = plt.gcf()
DPI = fig.get_dpi()
# ------------------------------- you have to play with this size to reduce the movement error when the mouse hovers over the figure, it's close to canvas size
fig.set_size_inches(404 * 2 / float(DPI), 404 / float(DPI))
# -------------------------------
x = list(range(1, 100))
y = list(range(1, 100))
plt.plot(x, y, color="r")
plt.title('Virus Infections Data')
plt.xlabel('Time in Days')
plt.ylabel('Infections')
plt.grid()
# ------------------------------- Instead of plt.show()
draw_figure(window['fig_cv'].TKCanvas, fig,)
window.close()
I wanted to give my little program a nice GUI with PySimpleGui when I ran into the problem that after the PySimpleGui window was closed the show() function from matplotlib blocked, even though the window of the figure is closed.
Here is an example code which doesn't determinate:
import PySimpleGUI as sg
import matplotlib.pyplot as plt
sg.theme('DarkAmber')
layout = [ [sg.Text('Some text on Row 1')],
[sg.Text('Enter something on Row 2'), sg.InputText()],
[sg.Button('Ok'), sg.Button('Cancel')] ]
window = sg.Window('Window Title', layout)
while True:
event, values = window.read()
if event == sg.WIN_CLOSED or event == 'Cancel': # if user closes window or clicks cancel
break
print('You entered ', values[0])
window.close()
plt.plot([1,2,3,4,5,6])
plt.show()
Yes, it also work on my platform.
WIN10, Python 3.9.5, PySimpleGUI 4.40.0.4, tkinter 8.6.9, Matplotlib 3.4.2
Following code show how I embed Matplotlib into PySimpleGUI.
import math
from matplotlib import use as use_agg
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import PySimpleGUI as sg
def pack_figure(graph, figure):
canvas = FigureCanvasTkAgg(figure, graph.Widget)
plot_widget = canvas.get_tk_widget()
plot_widget.pack(side='top', fill='both', expand=1)
return plot_widget
def plot_figure(index, theta):
fig = plt.figure(index) # Active an existing figure
ax = plt.gca() # Get the current axes
x = [degree for degree in range(1080)]
y = [math.sin((degree+theta)/180*math.pi) for degree in range(1080)]
ax.cla() # Clear the current axes
ax.set_title("Sensor Data")
ax.set_xlabel("X axis")
ax.set_ylabel("Y axis")
ax.set_xscale('log')
ax.grid()
plt.plot(x, y) # Plot y versus x as lines and/or markers
fig.canvas.draw() # Rendor figure into canvas
# Use Tkinter Agg
use_agg('TkAgg')
Firsttab = [[sg.Graph((640, 480), (0, 0), (640, 480),key='Graph1')]]
Secondtab = [[sg.Graph((640, 480), (0, 0), (640, 480),key='Graph2')]]
tab_group_layout = [
[sg.Tab('test', Firsttab,font='Courier 15', key='FirstTAB')],
[sg.Tab('test2', Secondtab,font='Courier 15', key='SecondTAB')],
]
column_layout= [[sg.TabGroup(tab_group_layout, enable_events=True,key='TABGROUP')]]
# PySimplGUI window
layout = [[sg.Column(column_layout, visible = True, key='GRAPHPANEL')],
[sg.Button(button_text = 'Graph', key='Start')]]
window = sg.Window('Matplotlib', layout, finalize=True)
# Set Button to repeat
window['Start'].Widget.configure(repeatdelay=50, repeatinterval=50)
# Initial
graph1 = window['Graph1']
graph2 = window['Graph2']
plt.ioff() # Turn the interactive mode off
fig1 = plt.figure(1) # Create a new figure
ax1 = plt.subplot(111) # Add a subplot to the current figure.
fig2 = plt.figure(2) # Create a new figure
ax2 = plt.subplot(111) # Add a subplot to the current figure.
pack_figure(graph1, fig1) # Pack figure under graph
pack_figure(graph2, fig2)
theta1 = 0 # theta for fig1
theta2 = 0 # theta for fig2
index = 1 # Current Tab
plot_figure(1, theta1)
plot_figure(2, theta2)
flag1, flag2 = False, False
while True:
event, values = window.read(timeout=100)
if event == sg.WINDOW_CLOSED:
break
elif event == sg.TIMEOUT_KEY:
if index == 1 and flag1:
theta1 = (theta1 + 10) % 360
plot_figure(index, theta1)
elif index == 2 and flag2:
theta2 = (theta2 + 10) % 260
plot_figure(index, theta2)
elif event == 'Start':
print('Start', index)
if index == 1:
flag1 = not flag1
elif index == 2:
flag2 = not flag2
print(flag1, flag2)
elif event == 'TABGROUP':
index = 1 if values[event].startswith('First') else 2
window.close()
Your code works fine on my machine. After pressing cancel on the GUI, the plot shows:
I am trying to get user input via input() after the user manipulates a plot using the standard zoom controls. Eg. User plays with the plot, figures out the desired X-value, and types it into the command-line prompt.
Plot can be either in a separate window (Spyder/Python) or in-line (in Jupiter Notebook).
After the user types in the value, the script continues (eg. asks for another value from the plot, or does some calculation with the values).
However, I can't get the plot to actually display and be responsive while the command-line is waiting for user-input. I have tried:
plot() statement first, input() statement second.
Spyder with Python 3.6 (I think), from source via MacPorts (updated Spyder as far as I could)
Spyder via Python 3.7 from ContinuumIO's Anaconda package, in IPython
Jupiter Notebook also from Anaconda
Numerous backends: macosx, qt, etc.
Notebook %matplotlib, notebook, inline, qt etc.
separate figure windows (Spyder & Python) vs. in-line figures (Jupyter Notebook)
fig.show( block=False ) and variations of this, eg. plt.show( block=False )
two different MacBooks (2017 and 2010 MacBook Pro's)
I did get the plot to actually update (previously it was either a blank space in a Notebook, or a blank separate figure window) by adding a matplotlib.pyplot.pause(0.5) between the plot() and input() statements. This was major progress, but once the script hits the input() statement, I get a spinning beachball on the Figure window (preventing zooming etc.) until I complete the input() statement by entering something, and then the script completes. At that point the plot is interactive.
It seems like the python console(s) can't handle more than one user-interaction simultaneously? Ie. input() is freezing all other user-interactivity?
I've been searching SO, google etc. for days now and haven't figured this out! The idea was to use this as a "quick and dirty" way to get user input from the plot, prior to undertaking the theoretically more complex task of acquiring user-clicks directly from the plot (which would have to snap to plotted data like data cursors).
Theory
The main execution thread blocks on user input, effectively pausing all other operations including rendering. You can mitigate this by doing plotting in another thread and passing UI input to that thread through a queue so that thread never blocks and stays responsive.
The docs have a great section on interactive figures, including ipython integrations.
Here are some examples:
Use non-blocking plot: plt.show(block=False)
Use matplotlib.animation
Use more complex multithreading and queues (good for integrating into UIs)
Some of the code below is from an old project of mine.
Example using input() with matplotlib.animation
Updates starting x location on input(), quits with q. Note that you can zoom and pan on the plot while waiting for user input. Also note the use of non-blocking plt.show() in mainloop():
import queue
import numpy as np # just used for mocking data, not necessary
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
animation_queue = queue.Queue()
update_rate_ms = 50
xdata = np.linspace(0, 2 * np.pi, 256)
ydata = np.sin(xdata)
zdata = np.cos(xdata)
def normal_plot_stuff():
"""Some run of the mill plotting."""
ax.set_title("Example Responsive Plot")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.plot(xdata, ydata, "C0", label="sin")
ax.plot(xdata, zdata, "C1", label="cos")
ax.legend(loc="lower right")
def animate(_, q):
"""Define a callback function for the matplotlib animation.
This reads messages from the queue 'q' to adjust the plot.
"""
while not q.empty():
message = q.get_nowait()
q.task_done()
x0 = float(message)
ax.set_xlim([x0, x0 + 5])
def mainloop():
"""The main loop"""
_ = FuncAnimation(fig, animate, interval=update_rate_ms, fargs=(animation_queue,))
normal_plot_stuff()
plt.show(block=False)
while True:
try:
uinput = input("Type starting X value or 'q' to quit: ")
if uinput == "q":
break
animation_queue.put_nowait(float(uinput))
except ValueError:
print("Please enter a valid number.")
mainloop()
Example with a live plot embedded in a UI
The window starting X and window size update as a user enters it in the text field. The matplotlib canvas is tied to the UI rendering for responsiveness.
"""
Imbed a live animation into a PySimpleGUI frontend.
The animation fires on a timer callback from matplotlib and renders to
a PySimpleGUI canvas (which is really just a wrapped tk canvas).
"""
import queue
import numpy as np # just used for mocking data, not necessary
import PySimpleGUI as sg # used just for example
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # used just for example
matplotlib.use("TkAgg")
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
animation_queue = queue.Queue()
update_rate_ms = 50
xdata = np.linspace(0, 2 * np.pi, 256)
ydata = np.sin(xdata)
zdata = np.cos(xdata)
def animate(_, q):
"""Define a callback function for the matplotlib animation."""
message = None
while not q.empty():
message = q.get_nowait()
q.task_done()
if not message: # ignore empty UI events
return
ax.clear()
if message[1]["sin"]: # if SIN enable checkbox is checked
ax.plot(xdata, ydata, "C0", label="sin")
ax.legend(loc="lower right")
if message[1]["cos"]: # if COS enable checkbox is checked
ax.plot(xdata, zdata, "C1", label="cos")
ax.legend(loc="lower right")
x0 = float(message[1]["x_start"])
size = float(message[1]["w_size"])
ax.set_xlim([x0, x0 + size])
ax.set_title("Example Responsive Plot")
ax.set_xlabel("X")
ax.set_ylabel("Y")
layout = [
[
sg.Text("Start X:"),
sg.Input(size=(5, 0), default_text=0, key="x_start"),
sg.Text("Window Size:"),
sg.Input(size=(10, 0), default_text=6.28, key="w_size"),
sg.Button("Exit"),
],
[
sg.Frame(
title="SIN",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True, key="sin", enable_events=True)],
],
),
sg.Frame(
title="COS",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True, key="cos", enable_events=True)],
],
),
],
[sg.Canvas(key="-CANVAS-")],
]
def plot_setup():
"""MUST maintain this order: define animation, plt.draw(), setup
window with finalize=True, then create, draw and pack the TkAgg
canvas.
"""
_ = FuncAnimation(fig, animate, interval=update_rate_ms, fargs=(animation_queue,))
plt.draw()
window = sg.Window(
"Responsive Plot Example",
layout,
font="18",
element_justification="center",
finalize=True,
)
# tie matplotlib renderer to pySimpleGui canvas
canvas = FigureCanvasTkAgg(fig, window["-CANVAS-"].TKCanvas)
canvas.draw()
canvas.get_tk_widget().pack(side="top", fill="both", expand=1)
return window
def mainloop():
"""Main GUI loop. Reads events and sends them to a queue for processing."""
window = plot_setup()
while True:
event, values = window.read(timeout=update_rate_ms)
if event in ("Exit", None):
break
animation_queue.put_nowait([event, values])
window.close()
mainloop()
Example with live data streaming
Specifically, notice that you can type different values into the window field at the top of the UI and the plot immediately updates without blocking/lagging. The ADC controls at the bottom are pretty meaningless for this example, but they do demonstrate more ways of passing UI data to the plotting thread.
"""
Imbed a live animation into a PySimpleGUI frontend, with extra plotting
and sensor control.
Live sensor data gets read from a separate thread and is converted to
PSI using calibration coefficients from a file.
The animation fires on a timer callback from matplotlib and renders to
a PySimpleGUI canvas (which is really just a wrapped tk canvas).
"""
import time
import queue
import random
import threading
from datetime import datetime
import numpy as np # just used for mocking data, not necessary
import PySimpleGUI as sg
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
matplotlib.use("TkAgg")
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
animation_queue = queue.Queue() # to pass GUI events to animation
raw_data_queue = queue.Queue() # to pass raw data to main thread
update_rate_ms = 50 # refresh time in ms
ts, adc0, adc1 = [], [], [] # live data containers
def get_sensors(msg):
"""Return the names of the currently selected sensors from the GUI."""
names = np.array(["A", "B", "C"])
s0 = [msg[2], msg[3], msg[4]] # adc0 sensor
s1 = [msg[6], msg[7], msg[8]] # adc1 sensor
return (names[s0][0], names[s1][0]) # boolean index to the names
def data_collection_thread(data_queue):
"""Simulate some live streamed data that and put it on a queue."""
t = 0
while True:
t += 1
x = np.sin(np.pi * t / 112) * 12000 - 10000
y = random.randrange(-23000, 3000)
line = f"{t}:{x}:{y}"
data_queue.put(line)
time.sleep(0.001)
def process_data(data_queue, message, t, x, y):
"""Consume and process the data from the live streamed data queue."""
while not data_queue.empty():
line = data_queue.get()
try:
t0, v0, v1 = line.split(":")
t.append(float(t0))
x.append(float(v0))
y.append(float(v1))
except ValueError:
pass # ignore bad data
data_queue.task_done()
try: # truncate to appropriate window size
n = int(message[0])
return t[-n:], x[-n:], y[-n:]
except (ValueError, TypeError):
return t, x, y # don't truncate if there is a bad window size
# draws live plot on a timer callback
def animate(_, q):
# get last message on event queue
message = None
while not q.empty():
message = q.get_nowait()
q.task_done()
# plot last n datapoints
try:
n = int(message[1][0]) # parse window size
adc0_window = adc0[-n:]
adc1_window = adc1[-n:]
ts_window = [i for i in range(len(adc0_window))]
ax.clear()
if message[1][1]: # if adc0 enable checkbox is checked
ax.plot(ts_window, adc0_window, "C0", label="adc0")
ax.legend(loc="lower right")
if message[1][5]: # if adc0 enable checkbox is checked
ax.plot(ts_window, adc1_window, "C1", label="adc1")
ax.legend(loc="lower right")
ax.set_title("Live Sensor Readings")
ax.set_xlabel("Time (ms)")
ax.set_ylabel("Pressure (psi)")
# save displayed data
if message[0] == "Save":
basename = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
plt.savefig(basename + ".png")
except (ValueError, TypeError):
pass # ignore poorly formatted messages from the GUI
layout = [
[ # row 1, some control buttons
sg.Text("Window Size (ms):"),
sg.Input(size=(5, 0), default_text=100),
sg.Button("Start"),
sg.Button("Pause"),
sg.Button("Save"),
sg.Button("Exit"),
],
[sg.Canvas(key="-CANVAS-")], # row 2, the animation
[ # row 3, some frames for the ADC options
sg.Frame(
title="ADC 0",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True)],
[
sg.Radio("Sensor A", 1, default=True),
sg.Radio("Sensor B", 1),
sg.Radio("Sensor C", 1),
],
],
),
sg.Frame(
title="ADC 1",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True)],
[
sg.Radio("Sensor A", 2),
sg.Radio("Sensor B", 2, default=True),
sg.Radio("Sensor C", 2),
],
],
),
],
]
# MUST maintain this order: define animation, plt.draw(), setup window
# with finalize=True, then create, draw and pack the TkAgg canvas
ani = animation.FuncAnimation(
fig, animate, interval=update_rate_ms, fargs=(animation_queue,)
)
plt.draw() # must call plot.draw() to start the animation
window = sg.Window(
"Read Pressure Sensors",
layout,
finalize=True,
element_justification="center",
font="18",
)
# tie matplotlib renderer to pySimpleGui canvas
canvas = FigureCanvasTkAgg(fig, window["-CANVAS-"].TKCanvas)
canvas.draw()
canvas.get_tk_widget().pack(side="top", fill="both", expand=1)
# kick off data collection thred
threading.Thread(
target=data_collection_thread, args=(raw_data_queue,), daemon=True
).start()
data_collection_enable = True
# main event loop for GUI
while True:
event, values = window.read(timeout=update_rate_ms)
# check for button events
if event in ("Exit", None):
break
if event == "Start":
data_collection_enable = True
if event == "Pause":
data_collection_enable = False
# send GUI events to animation
animation_queue.put_nowait((event, values))
# process data when not paused
if data_collection_enable:
ts, adc0, adc1 = process_data(raw_data_queue, values, ts, adc0, adc1)
else: # if paused, throw away live data
while not raw_data_queue.empty():
raw_data_queue.get()
raw_data_queue.task_done()
window.close()
Why don't you get the user to select the value using a mouse click? I've played around with this part of the documentation. With this simple solution you can get the x and y values of the image in general and the drawn figure using a mouse click. Then, it closes the plot. If you still need it, you may show it again.
from matplotlib.backend_bases import MouseButton
import matplotlib.pyplot as plt
import numpy as np
# Draw a sample
t = np.arange(0.0, 1.0, 0.01)
s = np.sin(2 * np.pi * t)
fig, ax = plt.subplots()
ax.plot(t, s)
def on_click(event):
if event.button is MouseButton.LEFT:
# get the x and y pixel coords of image
x_img, y_img = event.x, event.y
if event.inaxes:
# get x and y of the drawn figure
x_fig, y_fig = event.xdata, event.ydata
print('x and y of image: %.1f %.1f' %(x_img, y_img))
print('x and y of figure: %.3f %.3f' %(x_fig, y_fig))
plt.close()
plt.connect('button_press_event', on_click)
plt.show()
This is a sample for the output:
x and y of image: 546.0 214.0
x and y of figure: 0.974 -0.140
You can try inserting plt.waitforbuttonpress() statement between the plot() and input() statements. more info at this link - https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.waitforbuttonpress.html
the script should then display the plot, wait for user to press some button and then ask for input. You can also add a optional timer. Before going to input statement, it will wait for user to press some button on keyboard.
I want a picker on plot_date but it is not responding on clicks. even other events will not connect to the graphs.
This class will get tweets from a local database for sentiment analyse.
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.dates as md
from matplotlib.dates import DayLocator, HourLocator, DateFormatter, drange
import matplotlib.pyplot as plt
import tkinter as Tk
from Core.Database import Database
from numpy import arange
matplotlib.use('TkAgg')
plt.style.use('ggplot')
class SentimentGraph:
figure = None
axes = None
timeStamps = []
sentiment_score = []
def __init__(self, create_figure=True):
# get data from database.
self.get_data()
# create figure.
if create_figure:
self.figure = plt.figure()
# draw graph in figure
self.draw_graph(self.figure)
def draw_graph(self, figure):
neutral = 0
negative = 0
positive = 0
for score in self.sentiment_score:
if score == 0:
neutral += 1
elif score > 0:
positive += 1
elif score < 0:
negative += 1
self.figure, axes = plt.subplots(ncols=2, nrows=1)
ax1, ax2 = axes.ravel()
# The slices will be ordered and plotted counter-clockwise.
labels = 'neutral', 'Negative', 'Positive'
sizes = [neutral, positive, negative]
colors = ['yellowgreen', 'lightcoral', 'lightskyblue']
explode = (0, 0.1, 0.1) # only "explode" the 2nd slice (i.e. 'Hogs')
ax1.pie(sizes, explode=explode, labels=labels, colors=colors,
autopct='%1.1f%%', shadow=True, startangle=90,
radius=0.25, center=(0, 0), frame=True)
# Set aspect ratio to be equal so that pie is drawn as a circle.
ax1.axis('equal')
ax1.axis('off')
ax2.plot_date(self.timeStamps, self.sentiment_score, alpha=0.5, picker=True)
def onclick(event):
index = event.ind
xy = event.artist.get_offsets()
print('--------------')
print(xy[index])
self.figure.canvas.mpl_connect('pick_event', onclick)
ax2.set_title("Sentiment score")
ax2.set_ylabel("Sentiment score")
xfmt = md.DateFormatter('%Y-%m-%d %H:%M')
ax2.xaxis.set_minor_locator(HourLocator(arange(0, 25, 6)))
ax2.xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax2.xaxis.set_major_formatter(xfmt)
ax2.fmt_xdata = md.DateFormatter('%Y-%m-%d %H:%M')
self.figure.autofmt_xdate()
def get_data(self):
db = Database()
result = db.query(
''' select sentiment_score, posted_at / 1000 as timestamp from tweets ''')
rows = result.fetchall()
for row in rows:
self.sentiment_score.append(row[0])
# convert unix timestamp to matplotlib compatible
date = matplotlib.dates.epoch2num(row[1])
self.timeStamps.append(date)
return True
if __name__ == "__main__":
# change config db file location
import config
config.DB_FILE = "../tweets.db"
# create window
root = Tk.Tk()
root.wm_title("time line")
graph = SentimentGraph()
def _quit():
root.quit() # stops mainloop
root.destroy() # this is necessary on Windows to prevent
# Fatal Python Error: PyEval_RestoreThread: NULL tstate
# a tk.DrawingArea
canvas = FigureCanvasTkAgg(graph.figure, master=root)
canvas.show()
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
toolbar = NavigationToolbar2TkAgg(canvas, root)
toolbar.update()
canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
button = Tk.Button(master=root, text='Quit', command=_quit)
button.pack(side=Tk.BOTTOM)
Tk.mainloop()
# If you put root.destroy() here, it will cause an error if
# the window is closed with the window manager.
The root of your problem is that you're not embedding your figure in your Tkinter application.
Instead, you're creating an entirely new tkinter widget and window when you call plt.figure or plt.subplots. You're then "piggybacking" another canvas on top of that pre-existing figure and using it in your application.
Because matplotlib thinks the plot belongs to the original figure you created with plt.subplots, it's not registering any mouse events.
When you're embedding a figure in another application you must use the figure that you create manually. You cannot call plt.figure or plt.subplots (or plt.anything, really).
To solve your problem, set up the canvas inside your application using a manually-created Figure object, similar to the embedding examples in the documentation.