By using this answer to produce a LiveGraph and this answer to update variables to a thread, I was able to generate a graph that updates itself each second and whose amplitude is determined by a slider (code below). Both answers were incredibly helpful!
%matplotlib notebook
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from threading import Thread, Lock
import time
import ipywidgets as widgets
from IPython.display import display
import numpy as np
'''#################### Live Graph ####################'''
# Create a new class which is a live graph
class LiveGraph(object):
def __init__(self, baseline):
self.x_data, self.y_data = [], []
self.figure = plt.figure()
self.line, = plt.plot(self.x_data, self.y_data)
self.animation = FuncAnimation(self.figure, self.update, interval=1200)
# define variable to be updated as a list
self.baseline = [baseline]
self.lock = Lock()
self.th = Thread(target=self.thread_f, args = (self.baseline,), daemon=True)
# start thread
self.th.start()
def update_baseline(self,baseline):
# updating a list updates the thread argument
with self.lock:
self.baseline[0] = baseline
# Updates animation
def update(self, frame):
self.line.set_data(self.x_data, self.y_data)
self.figure.gca().relim()
self.figure.gca().autoscale_view()
return self.line,
def show(self):
plt.show()
# Function called by thread that updates variables
def thread_f(self, base):
x = 0
while True:
self.x_data.append(x)
x += 1
self.y_data.append(base[0])
time.sleep(1)
'''#################### Slider ####################'''
# Function that updates baseline to slider value
def update_baseline(v):
global g
new_value = v['new']
g.update_baseline(new_value)
slider = widgets.IntSlider(
value=10,
min=0,
max=200,
step=1,
description='value:',
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=True,
readout_format='d'
)
slider.observe(update_baseline, names = 'value')
'''#################### Display ####################'''
display(slider)
g = LiveGraph(slider.value)
Still, I would like to put the graph inside a bigger interface which has other widgets. It seems that I should put the LiveGraph inside the Output widget, but when I replace the 'Display section' of my code by the code shown below, no figure is displayed.
out = widgets.Output(layout={'border': '1px solid black'})
with out:
g = LiveGraph(slider.value)
vbox = widgets.VBox([slider,out], align_self='stretch',justify_content='center')
vbox
Is there a way to embed this LiveGraph in the output widget or in a box widget?
I found a solution by avoiding using FuncAnimation and the Output widget altogether while keeping my backend as inline.
Also changing from matplotlib to bqplot was essential!
The code is shown below (be careful because, as it is, it keeps increasing a list).
Details of things I tried:
I had no success updating the graph by a thread when using the Output Widget (tried clearing axes with ax.clear, redrawing the whole plot - since it is a static backend - and also using clear_output() command).
Also, ipywidgets does not allow placing a matplotlib figure straight inside a container, but it does if it is a bqplot figure!
I hope this answer helps anyone trying to integrate ipywidgets with a plot that constantly updates itself within an interface full of other widgets.
%matplotlib inline
import bqplot.pyplot as plt
from threading import Thread, Lock
import time
import ipywidgets as widgets
from IPython.display import display
import numpy as np
fig = plt.figure()
t, value = [], []
lines = plt.plot(x=t, y=value)
# Function that updates baseline to slider value
def update_baseline(v):
global base, lock
with lock:
new_value = v['new']
base = new_value
slider = widgets.IntSlider(
value=10,
min=0,
max=200,
step=1,
description='value:',
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=True,
readout_format='d'
)
base = slider.value
slider.observe(update_baseline, names = 'value')
def thread_f():
global t, value, base, lines
x = 0
while True:
t.append(x)
x += 1
value.append(base)
with lines.hold_sync():
lines.x = t
lines.y = value
time.sleep(0.1)
lock = Lock()
th = Thread(target=thread_f, daemon=True)
# start thread
th.start()
vbox = widgets.VBox([slider,fig], align_self='stretch',justify_content='center')
vbox
P.S. I'm new to using threads, so be careful as the thread may not be properly stopped with this code.
bqplot==0.12.29
ipywidgets==7.6.3
numpy==1.20.2
Related
I'm writing a medium-sized application to review some data. The structure is that plots will be held in a QTabWidget interface, with a plot control widget to adjust x limits (and later, there may be more features in the control widget). I have included a minimum reproducible example below.
Currently, I pass the axis of a figure to my control widget, and within that widget change the x limits after clicking a button. I have verified that the axis is being passed to the widget (with a print statement). I can programmatically set the axis limits (see line 91: self.Fig_ax.set_xlim(5, 20000) ) in the widget __init__ function and it works, but in the button click function, that same syntax does not do anything. However, with print statements, I verified that the axis is still being passed to the button click function.
I am very confused as to why the set_xlims method works in __init__ but not upon button press. Use: Run the code, enter a number in the X Min and X Max fields, click the Apply X Limits button. For the sake of the example, I hardcoded the button click axis shift to have defined limits rather than use what is entered into the fields, but those fields do get printed to the console for debugging purposes.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created
"""
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
## import matplotlib and animation
import functools
import random as rd
import numpy as np
from numpy import array, sin, pi, arange
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import pandas as pd
## import threading
import time
from matplotlib.backends.qt_compat import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
## New:
from PyQt5.QtWebEngineWidgets import *
######################################################8
class AppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.title = 'DDBRT'
self.setWindowTitle(self.title)
self.DDBRT_Widget = DDBRT(self) # Call the DDBRT
self.setCentralWidget(self.DDBRT_Widget) # set it as the central widget in the window
self.show()
####
####
''' End AppWindow '''
# August 27 2019 Start building a custom QWidget that can be put into the tool in multiple instances to adjust xlims.
# This may also serve as a templatge for other custom widgets that can go in
class XLimControlWidget(QWidget):
def __init__(self, parent, **kwargs):
super(QWidget, self).__init__(parent)
# set layout:
self.XLCWLayout = QVBoxLayout(self)
# Insert X Min Box Label
self.XMinSelectLbl = QLabel('Set X Min:')
self.XLCWLayout.addWidget(self.XMinSelectLbl)
# Insert X Min Entry Field
self.XMinEntryField = QLineEdit('X Min')
self.XLCWLayout.addWidget(self.XMinEntryField)
# Insert X Max Box Label
self.XMaxSelectLbl = QLabel('Set X Min:')
self.XLCWLayout.addWidget(self.XMaxSelectLbl)
# Insert X Max Box Entry Field
self.XMaxEntryField = QLineEdit('X Max')
self.XLCWLayout.addWidget(self.XMaxEntryField)
# Insert Set Button
self.SetXLimsBtn = QPushButton('Apply X Limits')
self.XLCWLayout.addWidget(self.SetXLimsBtn)
# Adjust layout so this widget is compact:
self.XLCWLayout.setSpacing(0)
self.XLCWLayout.setContentsMargins(0, 0, 0, 0)
# Note, that doesn't actually work and it still looks ugly
# That's annoying, but not worth figuring out how to fix right now.
# Need to focus on programming the behavior.
# Try out the kwargs pass to make sure passing something works.
for key, value in kwargs.items():
print('%s = %s' %(key, value))
####
self.Fig_ax = kwargs['Fig_ax_Key']
print('self.Fig_ax = %s of type %s' %(self.Fig_ax, type(self.Fig_ax)))
# Try the fig ax set xlim, which does work but doesn't.
self.Fig_ax.set_xlim(5, 20000)
self.SetXLimsBtn.clicked.connect(self.SetXLimsBtnClkd)
####
def SetXLimsBtnClkd(self): # Define what happens when the button is clicked.
self.xmin = float(self.XMinEntryField.text())
print('X Min will be ', self.xmin, ' of type ', type(self.xmin))
self.xmax = float(self.XMaxEntryField.text())
print('X Max will be ', self.xmax, ' of type ', type(self.xmax))
print('self.Fig_ax = %s of type %s' %(self.Fig_ax, type(self.Fig_ax)))
self.Fig_ax.set_xlim(20, 45)
# End desired goal:
# self.Fig_ax.set_xlim(self.xmin, self.xmax)
####
####
class DDBRT(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
#%% Set up multithreading
self.threadpool = QThreadPool() # Set up QThreadPool for multithreading so the GIL doesn't freeze the GUI
print("Multithreading with maximum %d theads" % self.threadpool.maxThreadCount())
#%% Layout:
## Set layout
self.MainLayout = QGridLayout(self)
## Create and embed CheckBox Placeholder
self.ChkBx_Placeholder = QCheckBox('ChkBxPlcholdr1');
self.MainLayout.addWidget(self.ChkBx_Placeholder, 3, 0)
## Create and embed tab container to hold plots, etc.
# Initialize tab container
self.TabsContainer = QTabWidget()
# Initialize tabs
self.tab0 = QWidget()
# Add tabs
self.TabsContainer.addTab(self.tab0, "Tab 0")
# Populate 0th tab
self.tab0.layout = QGridLayout(self)
self.pushButton0 = QPushButton("PyQt5 button")
self.tab0.layout.addWidget(self.pushButton0)
self.tab0.setLayout(self.tab0.layout)
# Add TabsContainer to widget
self.MainLayout.addWidget(self.TabsContainer, 3, 1) # self.MainLayout.addWidget(self.TabsContainer, 2, 2) # Works just fine too, but I can worry about layout finessing later because it's not that difficult, important, or urgent right now
self.setLayout(self.MainLayout)
#%% Plot XLs (accelerations)
XL_t = np.arange(0, 200000, 1)
XL_X = np.sin(XL_t/20000)
XL_Y = np.sin(XL_t/2000)
XL_Z = np.sin(XL_t/200)
self.tab8 = QWidget()
self.TabsContainer.addTab(self.tab8, "Tab 8: Acceleration mpl subplots")
self.tab8.layout = QHBoxLayout(self)
self.XL_Fig = Figure()
self.XL_X_ax = self.XL_Fig.add_subplot(3, 1, 1)
self.XL_X_ax.plot(XL_t, XL_X)
self.XL_X_ax.set_title('Acceleration X')
# self.XL_X_ax.grid(True)
self.XL_X_ax.set_xlabel('Time (s)')
self.XL_X_ax.set_ylabel('Acceleration')
#
self.XL_Y_ax = self.XL_Fig.add_subplot(3, 1, 2, sharex=self.XL_X_ax)
self.XL_Y_ax.plot(XL_t, XL_Y)
self.XL_Y_ax.set_title('Acceleration Y')
# self.XL_Y.grid(True)
self.XL_Y_ax.set_xlabel('Time (s)')
self.XL_Y_ax.set_ylabel('Acceleration')
#
self.XL_Z_ax = self.XL_Fig.add_subplot(3, 1, 3, sharex=self.XL_X_ax)
self.XL_Z_ax.plot(XL_t, XL_Z)
self.XL_Z_ax.set_title('Acceleration Z')
# self.XL_Z.grid(True)
self.XL_Z_ax.set_xlabel('Time (s)')
self.XL_Z_ax.set_ylabel('Acceleration')
#
self.XL_Canvas = FigureCanvas(self.XL_Fig)
self.XL_Canvas.print_figure('test')
# # Create an XLPlot container widget and add the canvas and navigation bar to it
self.XL_PlotContainer = QWidget()
self.XL_PlotContainer.layout = QVBoxLayout(self)
self.XL_PlotContainer.layout.addWidget(self.XL_Canvas)
self.XLMPLToolbar = NavigationToolbar(self.XL_Canvas, self)
self.XL_PlotContainer.layout.addWidget(self.XLMPLToolbar, 3)
self.XL_PlotContainer.setLayout(self.XL_PlotContainer.layout) # Looks redundant but it's needed to display the widgets
# add XLPlotContainer Widget to tab
self.tab8.layout.addWidget(self.XL_PlotContainer, 1)
self.tab8.setLayout(self.tab8.layout)
# add XLCWidget to tab
self.kwargs = {"Fig_ax_Key": self.XL_X_ax}
self.XLXLCW = XLimControlWidget(self, **self.kwargs)
self.tab8.layout.addWidget(self.XLXLCW)
####
####
#%%
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = AppWindow()
sys.exit(app.exec_())
I expect the button click to change the axes on all subplots (since I linked the subplots when I set them up), but the x limits do not change at all. The button click function does run, as shown by the print statements.
In case anyone else finds this question later, here is the solution that worked: I passed the entire figure to my control widget, used a = self.Fig.get_axes() to get a list of my axes, and a[0].set_xlim(20, 200) ; self.Fig.canvas.draw_idle() to update the figure with the button click.
I have a separate plot thread with matplotlib and multiprocesses. Now if I interactively zoom in to the window, autoscale_view() does not work anymore (fixed with using autoscale()). But the "home" button in the Toolbox is still not working: It seems to call autoscale_view() and does not show the updated view but the old view (at point when zoomed in). Example code:
import matplotlib
matplotlib.use("qt4agg")
from matplotlib import pyplot as plt
import multiprocessing
def reset_view():
plt.get
xdata = []
ydata = []
temp = 0
test_ax = plt.gca()
test_line, = plt.plot(xdata, ydata)
plt.show(block=False)
for i in range(10):
temp = temp+1
xdata.append(temp)
ydata.append(temp)
test_line.set_data(xdata, ydata)
test_ax.relim()
test_ax.autoscale(tight= False)
plt.show(block=False)
plt.pause(2)
plot_thread = multiprocessing.Process(target = reset_view, args = ())
reset_view()
if __name__ == '__main__':
plot_thread.start()
Try zooming in during plotting and pressing the Home Button after. Is there a way to either make the home button use autoscale() instead of autoscale_view() or reset & update the toolbar history, so that it doesn't jump back to old views?
P.s.: "Home"-button = reset original view
I finally was able to figure it out by trial & error. There is a function to update the toolbar. This updates the toolbar's history and sets the home() function to the new view. Solution:
figure = plt.gcf() #Get current figure
toolbar = figure.canvas.toolbar #Get the toolbar handler
toolbar.update() #Update the toolbar memory
plt.show(block = False) #Show changes
I did the following example code just to test how to integrate an animated matplotlib plot with pygtk. However, I get some unexpected behaviors when I run it.
First, when I run my program and click on the button (referred to as button1 in the code), there is another external blank window which shows up and the animated plot starts only after closing this window.
Secondly, when I click on the button many times, it seems that there is more animations which are created on top of each other (which gives the impression that the animated plot speeds up). I have tried to call animation.FuncAnimation inside a thread (as you can see in the comment at end of the function on_button1_clicked), but the problem still the same.
Thirdly, is it a good practice to call animation.FuncAnimation in a thread to allow the user to use the other functions of the gui ? Or should I rather create a thread inside the method animate (I guess this will create too many threads quickly) ? I am not sure how to proceed.
Here is my code:
import gtk
from random import random
import numpy as np
from multiprocessing.pool import ThreadPool
import matplotlib.pyplot as plt
import matplotlib.animation as animation
#from matplotlib.backends.backend_gtk import FigureCanvasGTK as FigureCanvas
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
#from matplotlib.backends.backend_gtkcairo import FigureCanvasGTKCairo as FigureCanvas
class HelloWorld:
def __init__(self):
interface = gtk.Builder()
interface.add_from_file('interface.glade')
self.dialog1 = interface.get_object("dialog1")
self.label1 = interface.get_object("label1")
self.entry1 = interface.get_object("entry1")
self.button1 = interface.get_object("button1")
self.hbox1 = interface.get_object("hbox1")
self.fig, self.ax = plt.subplots()
self.X = [random() for x in range(10)]
self.Y = [random() for x in range(10)]
self.line, = self.ax.plot(self.X, self.Y)
self.canvas = FigureCanvas(self.fig)
# self.hbox1.add(self.canvas)
self.hbox1.pack_start(self.canvas)
interface.connect_signals(self)
self.dialog1.show_all()
def gtk_widget_destroy(self, widget):
gtk.main_quit()
def on_button1_clicked(self, widget):
name = self.entry1.get_text()
self.label1.set_text("Hello " + name)
self.ani = animation.FuncAnimation(self.fig, self.animate, np.arange(1, 200), init_func=self.init, interval=25, blit=True)
'''
pool = ThreadPool(processes=1)
async_result = pool.apply_async(animation.FuncAnimation, args=(self.fig, self.animate, np.arange(1, 200)), kwds={'init_func':self.init, 'interval':25, 'blit':True} )
self.ani = async_result.get()
'''
plt.show()
def animate(self, i):
# Read XX and YY from a file or whateve
XX = [random() for x in range(10)] # just as an example
YY = [random() for x in range(10)] # just as an example
self.line.set_xdata( XX )
self.line.set_ydata( YY )
return self.line,
def init(self):
self.line.set_ydata(np.ma.array(self.X, mask=True))
return self.line,
if __name__ == "__main__":
HelloWorld()
gtk.main()
I'm working on creating a program that utilizes Tkinter and matplotlib. I have 2 lists of lists (one for x-axis, one for y-axis) and I'm looking to have a button that can switch between the lists within the list. I took much of the code from the question Interactive plot based on Tkinter and matplotlib, but I can't get the graph to update when the button is pressed. I'm quite new to using classes and having a bit of difficulty understanding them.
tft is the x-data tf1 is the y-data in my code.
Example of data:
x-data = [[1,2,3,4,5],[10,11,13,15,12,19],[20,25,27]]
y-data = [[5.4,6,10,11,6],[4,6,8,34,20,12],[45,25,50]]
My code below will graph one of the lists within a list, but won't switch between the lists within that list when the button is pressed. The correct value for event_num also prints in the command window (the issue of event_num was solved in a previous questions here).
There is no error that appears, the program only prints the number (when the button is pressed), but doesn't update the graph with the new data from the list.
Preliminary Code (no issues--or there shouldn't be any)
#Importing Modules
import glob
from Tkinter import *
from PIL import Image
from Text_File_breakdown import Text_File_breakdown
import re
import matplotlib.pyplot as plt
from datetime import datetime
#Initializing variables
important_imgs=[]
Image_dt=[]
building=[]
quick=[]
num=0
l=0
match=[]
#Getting the names of the image files
image_names=glob.glob("C:\Carbonite\EL_36604.02_231694\*.jpeg")
#image= Image.open(images_names[1])
#image.show()
#Text_File_breakdown(file,voltage limit,pts after lim, pts before lim)
tft,tf1,tf2=Text_File_breakdown('C:\Carbonite\EL_36604.02_231694.txt',3.0,5,5)
#tft= time of voltages tf1=Voltage signal 1 tf2=Voltage signal 2
#Test Settings: 'C:\Carbonite\EL_36604.02_231694.txt',3.0,5,5
#Getting the Dates from the image names
for m in image_names:
Idt=re.search("([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}.[0-9]{2}.[0-9]{2})", m)
Im_dat_tim=Idt.group(1)
Im_dat_tim=datetime.strptime(Im_dat_tim, '%Y-%m-%d %H.%M.%S')
Image_dt.append(Im_dat_tim)
Im_dat_tim=None
#Looking for the smallest difference between the voltage and image dates and associating an index number (index of the image_names variable) with each voltage time
for event in range(len(tft)):
for i in range(len(tft[event])):
diff=[tft[event][i]-Image_dt[0]]
diff.append(tft[event][i]-Image_dt[0])
while abs(diff[l])>=abs(diff[l+1]):
l=l+1
diff.append(tft[event][i]-Image_dt[l])
match.append(l)
l=0
#Arranging the index numbers (for the image_names variable) in a list of lists like tft variable
for count in range(len(tft)):
for new in range(len(tft[count])):
quick.append(match[num])
num=num+1
building.append(quick)
quick=[]
plt.close('all')
fig, ax = plt.subplots(1)
ax.plot(tft[1],tf1[1],'.')
# rotate and align the tick labels so they look better
fig.autofmt_xdate()
# use a more precise date string for the x axis locations in the
# toolbar
import matplotlib.dates as mdates
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
plt.title('Single Event')
Continuation of code/Portion of code where the issue is:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import numpy as np
class App:
def __init__(self, master):
self.event_num = 1
# Create a container
frame = Frame(master)
# Create 2 buttons
self.button_left = Button(frame,text="< Previous Event",
command=self.decrease)
self.button_left.grid(row=0,column=0)
self.button_right = Button(frame,text="Next Event >",
command=self.increase)
self.button_right.grid(row=0,column=1)
fig = Figure()
ax = fig.add_subplot(111)
fig.autofmt_xdate()
import matplotlib.dates as mdates
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas = FigureCanvasTkAgg(fig,master=master)
self.canvas.show()
self.canvas.get_tk_widget().grid(row=1,column=0)
frame.grid(row=0,column=0)
def decrease(self):
self.event_num -= 1
print self.event_num
self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
#self.canvas.draw(tft[self.event_num],tf1[self.event_num],'.')
#self.line.set_xdata(tft[event_num])
#self.line.set_ydata(tf1[event_num])
def increase(self):
self.event_num += 1
print self.event_num
self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
#self.canvas.draw(tft[self.event_num],tf1[self.event_num],'.')
#self.set_xdata(tft[event_num])
#self.set_ydata(tf1[event_num])
root = Tk()
app = App(root)
root.mainloop()
The problem is that you are constantly plotting on a different set of axes than you think. self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.') refers to the axes that you created outside your class, not the axes in the figure you created in the App class. The problem can be remedied by creating self.fig and self.ax attributes:
class App:
def __init__(self, master):
self.event_num = 1
# Create a container
frame = Frame(master)
# Create 2 buttons
self.button_left = Button(frame,text="< Previous Event",
command=self.decrease)
self.button_left.grid(row=0,column=0)
self.button_right = Button(frame,text="Next Event >",
command=self.increase)
self.button_right.grid(row=0,column=1)
self.fig = Figure()
self.ax = self.fig.add_subplot(111)
self.fig.autofmt_xdate()
import matplotlib.dates as mdates
self.ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas = FigureCanvasTkAgg(self.fig,master=master)
self.canvas.show()
self.canvas.get_tk_widget().grid(row=1,column=0)
frame.grid(row=0,column=0)
def decrease(self):
self.event_num -= 1
print self.event_num
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
def increase(self):
self.event_num += 1
print self.event_num
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
Another (possible) problem is that the data gets appended to the plot instead of being replaced. There are two ways to fix this:
Turn hold off: self.ax.hold(False) somewhere in __init__() before you plot anything.
Actually replace the plot data: Replace the line
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
with
self.line.set_xdata(tft[self.event_num])
self.line.set_ydata(tf1[self.event_num])
You are plotting to an axes in the global namespace but redrawing the canvas on another, unrelated global object. I think your issues are about global vs. local scope and the self object in object-oriented programming.
Python scoping
Variables created in your file without indentation are in a file's global scope. class and def statements create local scopes. References to a variable name are resolved by checking scopes in this order:
Local scope
Enclosing local scope (enclosing defs only, not classes)
Global scope
Builtins
That cascade is only for references. Assignments to a variable name assign to the current global or local scope, period. A variable of that name will be created in the current scope if it doesn't already exist.
The bindings of local variables are deleted when a function returns. Values that are not bound are eventually garbage collected. If an assignment binds a global variable to a local variable's value, the value will persist with the global.
Applying this to your code
There are two sets of figure/axes objects created in your code.
# From your top chunk of code
from Tkinter import *
import matplotlib.pyplot as plt
# This line creates a global variable ax:
fig, ax = plt.subplots(1)
# From your bottom chunk of code
class App(self, master):
def __init__(self, master):
fig = Figure()
# This line creates a local variable ax:
ax = fig.add_subplot(111)
# This line references the local variable ax:
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
# This line references the local variable ax, and binds its value
# to the attribute line of the self object,
# which is the global variable app:
self.line, = ax.plot(x[self.event_n],y[self.event_n])
def increase(self):
self.event_num += 1
# This line references the global variable ax
self.line, = ax.plot(x[self.event_n],y[self.event_n])
# This line updates the object's canvas but the plot
# was made on the global axes:
self.canvas.draw()
app = App(root)
You can fix this up by always referencing an Axes associated with your Tkinter root window:
self.fig, self.ax = plt.subplots(1, 1) to create enduring Axes.
self.ax.cla() to remove the previous selection's data
self.ax.plot() to add the data at the current self.event_num index
self.ax.set_xlim(x_min*0.9, x_max*0.9) and self.ax.set_ylim(y_min*1.1, y_max*1.1) to keep the axes window consistent so as to visualize movement of the mean of data among inner lists. The x_max, y_max, x_min, and y_min can be determined before the main event loop by flattening the x and y lists and then using builtins max() and min().
I have a matplotlib graph which I want repeated in two separate windows, under PyQt4. I've tried adding the widget to the layout of both, but then the widget vanishes from the first one. Is there any way to do this except creating two identical graphs and keeping them in sync?
The problem is that you can't add the same qt widget to two differents parents widgets because in the process of adding a widget Qt also make a reparent process which does what you see:
... the widget vanishes from the first one[window]...
So the solution is to make two canvas that share the same figure.
Here is an example code, this will show you two main windows each with two canvas and the four plots will be syncronized:
import sys
from PyQt4 import QtGui
import numpy as np
import numpy.random as rd
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
class ApplicationWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.main_widget = QtGui.QWidget(self)
vbl = QtGui.QVBoxLayout(self.main_widget)
self._fig = Figure()
self.ax = self._fig.add_subplot(111)
#note the same fig for two canvas
self.fc1 = FigureCanvas(self._fig) #canvas #1
self.fc2 = FigureCanvas(self._fig) #canvas #1
self.but = QtGui.QPushButton(self.main_widget)
self.but.setText("Update") #for testing the sync
vbl.addWidget(self.fc1)
vbl.addWidget(self.fc2)
vbl.addWidget(self.but)
self.setCentralWidget(self.main_widget)
#property
def fig(self):
return self._fig
#fig.setter
def fig(self, value):
self._fig = value
#keep the same fig in both canvas
self.fc1.figure = value
self.fc2.figure = value
def redraw_plot(self):
self.fc1.draw()
self.fc2.draw()
qApp = QtGui.QApplication(sys.argv)
aw1 = ApplicationWindow() #window #1
aw2 = ApplicationWindow() #window #2
aw1.fig = aw2.fig #THE SAME FIG FOR THE TWO WINDOWS!
def update_plot():
'''Just a random plot for test the sync!'''
#note that the update is only in the first window
ax = aw1.fig.gca()
ax.clear()
ax.plot(range(10),rd.random(10))
#calls to redraw the canvas
aw1.redraw_plot()
aw2.redraw_plot()
#just for testing the update
aw1.but.clicked.connect(update_plot)
aw2.but.clicked.connect(update_plot)
aw1.show()
aw2.show()
sys.exit(qApp.exec_())
While it's not a perfect solution, matplotlib has a built-in way to keep the limits, ticks, etc of two separate plots in sync.
E.g.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 4 * np.pi, 100)
y = np.cos(x)
figures = [plt.figure() for _ in range(3)]
ax1 = figures[0].add_subplot(111)
axes = [ax1] + [fig.add_subplot(111, sharex=ax1, sharey=ax1) for fig in figures[1:]]
for ax in axes:
ax.plot(x, y, 'go-')
ax1.set_xlabel('test')
plt.show()
Notice that all 3 plots will stay in-sync as you zoom, pan, etc.
There's probably a better way of doing it though.