I need two Windows that maintain size and focus when data changes.
Size and focus are intended to be adjusted manually .
I managed myself to do what I want, but a Warning is shown :
"MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance. In a future version, a new instance will always be created and returned. Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
ax1 = window1.add_subplot(111)".
Can someone explain what is wrong with my code?
Thanks
import matplotlib.pyplot as plt
import numpy as np
def Creo_ventana(primero,data):
plt.ion()
window1=plt.figure('WINDOW1')
ax1 = window1.add_subplot(111)
window2=plt.figure('WINDOW2')
ax2 = window2.add_subplot(111)
y_min1, y_max1 = ax1.get_ylim()
x_min1, x_max1 = ax1.get_xlim()
size1=ax1.figure.get_size_inches()
y_min2, y_max2 = ax2.get_ylim()
x_min2, x_max2 = ax2.get_xlim()
size2=ax2.figure.get_size_inches()
if primero ==False:
ax1.cla()
ax1.set_xlim(x_min1, x_max1)
ax1.set_ylim(y_min1, y_max1)
ax1.figure.set_size_inches(size1)
ax1.autoscale(False)
ax1.autoscale_view (True,False,False)
ax1.set_adjustable('box')
ax1.plot(data)
window1.canvas.draw()
window1.canvas.flush_events()
if primero ==False:
ax2.cla()
ax2.set_xlim(x_min2, x_max2)
ax2.set_ylim(y_min2, y_max2)
ax2.figure.set_size_inches(size2)
ax2.autoscale(False)
ax2.autoscale_view (True,False,False)
ax2.set_adjustable('box')
ax2.plot(data)
window2.canvas.draw()
window2.canvas.flush_events()
if __name__ == "__main__":
primero= True
correr = 'y'
while correr =='y':
data = np.random.rand(100)*10
Creo_ventana(primero,data)
primero= False
correr=input ('otro? y/n :')
if not correr == 'y':
break
Related
QUESTION: Whats the cleanest and simplest way to use Python's MATPLOTLIB animation function without the use of global array's or constantly appending a global "list of data points" to a plot?
Here is an example of a animated graph that plots the bid and ask sizes of a stock ticker. In this example the variables time[], ask[], and bid[] are used as global variables.
How do we modify the matplotlib animate() function to not use global variables?
so I'm trying to remove "all" global variables and just run one function call...
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
from random import randint
stock = {'ask': 12.82, 'askSize': 21900, 'bid': 12.81, 'bidSize': 17800}
def get_askSize():
return stock["askSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def get_bidSize():
return stock["bidSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def animate(i):
pt_ask = get_askSize()
pt_bid = get_bidSize()
time.append(i) #x
ask.append(pt_ask) #y
bid.append(pt_bid) #y
ax.clear()
ax.plot(time, ask)
ax.plot(time, bid)
ax.set_xlabel('Time')
ax.set_ylabel('Volume')
ax.set_title('ask and bid size')
ax.set_xlim([0,40])
#axis = axis_size(get_bidSize, get_askSize)
ylim_min = (get_askSize() + get_bidSize())/6
ylim_max = (get_askSize() + get_bidSize())
ax.set_ylim([ylim_min,ylim_max])
# create empty lists for the x and y data
time = []
ask = []
bid = []
# create the figure and axes objects
fig, ax = plt.subplots()
# run the animation
ani = FuncAnimation(fig, animate, frames=40, interval=500, repeat=False)
plt.show()
As #Warren mentioned, you can use the fargs parameter to pass in shared variables to be used in your animation function.
You should also precompute all of your points, and then use your frames to merely act as an expanding window on those frames. This will be a much more performant solution and prevents you from needing to convert between numpy arrays and lists on every tick of your animation in order to update the underlying data for your lines.
This also enables you to precompute your y-limits to prevent your resultant plot from jumping all over the place.
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
rng = np.random.default_rng(0)
def animate(i, ask_line, bid_line, data):
i += 1
x = data['x'][:i]
ask_line.set_data(x, data['ask'][:i])
bid_line.set_data(x, data['bid'][:i])
stock = {'ask': 12.82, 'askSize': 21900, 'bid': 12.81, 'bidSize': 17800}
frames = 40
data = {
'x': np.arange(0, frames),
'ask': stock['askSize'] + rng.integers(0, 9000, size=frames),
'bid': stock['bidSize'] + rng.integers(0, 9000, size=frames),
}
fig, ax = plt.subplots()
ask_line, = ax.plot([], [])
bid_line, = ax.plot([], [])
ax.set(xlabel='Time', ylabel='Volume', title='ask and bid size', xlim=(0, 40))
ax.set_ylim(
min(data['ask'].min(), data['bid'].min()),
max(data['ask'].max(), data['bid'].max()),
)
# run the animation
ani = FuncAnimation(
fig, animate, fargs=(ask_line, bid_line, data),
frames=40, interval=500, repeat=False
)
plt.show()
You can use the fargs parameter of FuncAnimation to provide additional arguments to your animate callback function. So animate might start like
def animate(i, askSize, bidSize):
...
and in the call of FuncAnimation, you would add the parameter fargs=(askSize, bidSize). Add whatever variables (in whatever form) that you need to make available within the animate function.
I use this in my example of the use of FuncAnimation with AnimatedPNGWriter in the package numpngw; see Example 8. In that example, my callback function is
def update_line(num, x, data, line):
"""
Animation "call back" function for each frame.
"""
line.set_data(x, data[num, :])
return line,
and FuncAnimation is created with
ani = animation.FuncAnimation(fig, update_line, frames=len(t),
init_func=lambda : None,
fargs=(x, sol, lineplot))
You are using animation wrong, as you are adding and removing lines at each iteration, which makes the animation a lot slower. For line plots, the best way to proceed is:
initialize the figure and axes
initialize empty lines
inside the animate function, update the data of each line.
Something like this:
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
from random import randint
stock = {'ask': 12.82, 'askSize': 21900, 'bid': 12.81, 'bidSize': 17800}
def get_askSize():
return stock["askSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def get_bidSize():
return stock["bidSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def add_point_to_line(x, y, line):
# retrieve the previous data in the line
xd, yd = [list(t) for t in line.get_data()]
# append the new point
xd.append(x)
yd.append(y)
# set the new data
line.set_data(xd, yd)
def animate(i):
pt_ask = get_askSize()
pt_bid = get_bidSize()
# append a new value to the lines
add_point_to_line(i, pt_ask, ax.lines[0])
add_point_to_line(i, pt_bid, ax.lines[1])
# update axis limits if necessary
ylim_min = (get_askSize() + get_bidSize())/6
ylim_max = (get_askSize() + get_bidSize())
ax.set_ylim([ylim_min,ylim_max])
# create the figure and axes objects
fig, ax = plt.subplots()
# create empty lines that will be populated on the animate function
ax.plot([], [])
ax.plot([], [])
ax.set_xlabel('Time')
ax.set_ylabel('Volume')
ax.set_title('ask and bid size')
ax.set_xlim([0,40])
# run the animation
ani = FuncAnimation(fig, animate, frames=40, interval=500, repeat=False)
plt.show()
let say I have this code:
num_rows = 10
num_cols = 1
fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in xrange(num_rows):
ax = axs[i]
ax.plot(np.arange(10), np.arange(10)**i)
plt.show()
the result figure has too much info and now I want to pick 1 of the axes and draw it alone in a new figure
I tried doing something like this
def on_click(event):
axes = event.inaxes.get_axes()
fig2 = plt.figure(15)
fig2.axes.append(axes)
fig2.show()
fig.canvas.mpl_connect('button_press_event', on_click)
but it didn't quite work. what would be the correct way to do it? searching through the docs and throw SE gave hardly any useful result
edit:
I don't mind redrawing the chosen axes, but I'm not sure how can I tell which of the axes was chosen so if that information is available somehow then it is a valid solution for me
edit #2:
so I've managed to do something like this:
def on_click(event):
fig2 = plt.figure(15)
fig2.clf()
for line in event.inaxes.axes.get_lines():
xydata = line.get_xydata()
plt.plot(xydata[:, 0], xydata[:, 1])
fig2.show()
which seems to be "working" (all the other information is lost - labels, lines colors, lines style, lines width, xlim, ylim, etc...)
but I feel like there must be a nicer way to do it
thanks
Copying the axes
The inital answer here does not work, we keep it for future reference and also to see why a more sophisticated approach is needed.
#There are some pitfalls on the way with the initial approach.
#Adding an `axes` to a figure can be done via `fig.add_axes(axes)`. However, at this point,
#the axes' figure needs to be the figure the axes should be added to.
#This may sound a bit like running in circles but we can actually set the axes'
#figure as `axes.figure = fig2` and hence break out of this.
#One might then also position the axes in the new figure to take the usual dimensions.
#For this a dummy axes can be added first, the axes can change its position to the position
#of the dummy axes and then the dummy axes is removed again. In total, this would look as follows.
import matplotlib.pyplot as plt
import numpy as np
num_rows = 10
num_cols = 1
fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in xrange(num_rows):
ax = axs[i]
ax.plot(np.arange(10), np.arange(10)**i)
def on_click(event):
axes = event.inaxes
if not axes: return
fig2 = plt.figure()
axes.figure=fig2
fig2.axes.append(axes)
fig2.add_axes(axes)
dummy = fig2.add_subplot(111)
axes.set_position(dummy.get_position())
dummy.remove()
fig2.show()
fig.canvas.mpl_connect('button_press_event', on_click)
plt.show()
#So far so good, however, be aware that now after a click the axes is somehow
#residing in both figures, which can cause all sorts of problems, e.g. if you
# want to resize or save the initial figure.
Instead, the following will work:
Pickling the figure
The problem is that axes cannot be copied (even deepcopy will fail). Hence to obtain a true copy of an axes, you may need to use pickle. The following will work. It pickles the complete figure and removes all but the one axes to show.
import matplotlib.pyplot as plt
import numpy as np
import pickle
import io
num_rows = 10
num_cols = 1
fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in range(num_rows):
ax = axs[i]
ax.plot(np.arange(10), np.arange(10)**i)
def on_click(event):
if not event.inaxes: return
inx = list(fig.axes).index(event.inaxes)
buf = io.BytesIO()
pickle.dump(fig, buf)
buf.seek(0)
fig2 = pickle.load(buf)
for i, ax in enumerate(fig2.axes):
if i != inx:
fig2.delaxes(ax)
else:
axes=ax
axes.change_geometry(1,1,1)
fig2.show()
fig.canvas.mpl_connect('button_press_event', on_click)
plt.show()
Recreate plots
The alternative to the above is of course to recreate the plot in a new figure each time the axes is clicked. To this end one may use a function that creates a plot on a specified axes and with a specified index as input. Using this function during figure creation as well as later for replicating the plot in another figure ensures to have the same plot in all cases.
import matplotlib.pyplot as plt
import numpy as np
num_rows = 10
num_cols = 1
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
labels = ["Label {}".format(i+1) for i in range(num_rows)]
def myplot(i, ax):
ax.plot(np.arange(10), np.arange(10)**i, color=colors[i])
ax.set_ylabel(labels[i])
fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in xrange(num_rows):
myplot(i, axs[i])
def on_click(event):
axes = event.inaxes
if not axes: return
inx = list(fig.axes).index(axes)
fig2 = plt.figure()
ax = fig2.add_subplot(111)
myplot(inx, ax)
fig2.show()
fig.canvas.mpl_connect('button_press_event', on_click)
plt.show()
If you have, for example, a plot with three lines generated by the function plot_something, you can do something like this:
fig, axs = plot_something()
ax = axs[2]
l = list(ax.get_lines())[0]
l2 = list(ax.get_lines())[1]
l3 = list(ax.get_lines())[2]
plot(l.get_data()[0], l.get_data()[1])
plot(l2.get_data()[0], l2.get_data()[1])
plot(l3.get_data()[0], l3.get_data()[1])
ylim(0,1)
I use bokeh in an ipython notebook and would like to have a button next to a plot to switch on or off labels of the data points. I found a solution using IPython.html.widgets.interact, but this solution resets the plot for each update including zooming and padding
This is the minimal working code example:
from numpy.random import random
from bokeh.plotting import figure, show, output_notebook
from IPython.html.widgets import interact
def plot(label_flag):
p = figure()
N = 10
x = random(N)+2
y = random(N)+2
labels = range(N)
p.scatter(x, y)
if label_flag:
pass
p.text(x, y, labels)
output_notebook()
show(p)
interact(plot, label_flag=True)
p.s. If there is an easy way to do this in matplotlib I would also switch back again.
By using bokeh.models.ColumnDataSource to store and change the plot's data I was able to achieve what I wanted.
One caveat is, that I found no way to make it work w/o refresh w/o calling output_notebook twice in two different cells. If I remove one of the two output_notebook calls the gui of the tools-button looks breaks or changing a setting also results in a reset of the plot.
from numpy.random import random
from bokeh.plotting import figure, show, output_notebook
from IPython.html.widgets import interact
from bokeh.models import ColumnDataSource
output_notebook()
## <-- new cell -->
p = figure()
N = 10
x_data = random(N)+2
y_data = random(N)+2
labels = range(N)
source = ColumnDataSource(
data={
'x':x_data,
'y':y_data,
'desc':labels
}
)
p.scatter('x', 'y', source=source)
p.text('x', 'y', 'desc', source=source)
output_notebook()
def update_plot(label_flag=True):
if label_flag:
source.data['desc'] = range(N)
else:
source.data['desc'] = ['']*N
show(p)
interact(update_plot, label_flag=True)
Is it possible to setup plot to show more data when expanded?
Matplotlib plots scale when resized. To show specific area one can use set_xlim and such on axes. I have an ecg-like plot showing realtime data, its y limits are predefined, but I want to see more data along x if I expand window or just have big monitor.
Im using it in a pyside app and I could just change xlim on resize but I want more clean and generic solution.
One way to do this is to implement a handler for resize_event. Here is a short example how this might be done. You can modify it for your needs:
import numpy as np
import matplotlib.pyplot as plt
def onresize(event):
width = event.width
scale_factor = 100.
data_range = width/scale_factor
start, end = plt.xlim()
new_end = start+data_range
plt.xlim((start, new_end))
if __name__ == "__main__":
fig = plt.figure()
ax = fig.add_subplot(111)
t = np.arange(100)
y = np.random.rand(100)
ax.plot(t,y)
plt.xlim((0, 10))
cid = fig.canvas.mpl_connect('resize_event', onresize)
plt.show()
I am working on a Tkinter-GUI to interactively generate Matplotlib-plots, depending on user-input. For this end, it needs to re-plot after the user changes the input.
I have gotten it to work in principle, but would like to include the NavigationToolbar. However, I cannot seem to get the updating of the NavigationToolbar to work correctly.
Here is a basic working version of the code (without the user input Entries):
# Import modules
from Tkinter import *
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
# global variable: do we already have a plot displayed?
show_plot = False
# plotting function
def plot(x, y):
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
ax1.plot(x,y)
return fig
def main():
x = np.arange(0.0,3.0,0.01)
y = np.sin(2*np.pi*x)
fig = plot(x, y)
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2TkAgg(canvas, toolbar_frame)
global show_plot
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
# GUI
root = Tk()
draw_button = Button(root, text="Plot!", command = main)
draw_button.grid(row=0, column=0)
fig = plt.figure()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().grid(row=0,column=1)
toolbar_frame = Frame(root)
toolbar_frame.grid(row=1,column=1)
root.mainloop()
Pressing "Plot!" once generates the plot and the NavigationToolbar.
Pressing it a second time replots, but generates a second NavigationToolbar (and another every time "Plot!" is pressed). Which sounds like grid_forget() is not working.
However, when I change
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
to
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
else:
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
then the NavigationToolbar does vanish when "Plot!" is pressed a second time (but then there is, as expected, no new NavigationToolbar to replace the old). So grid_forget() is working, just not as expected.
What am I doing wrong? Is there a better way to update the NavigationToolbar?
Any help greatly appreciated!
Lastalda
Edit:
I found that this will work if you destroy the NavigationToolbar instead of forgetting it. But you have to completely re-create the widget again afterwards, of course:
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar_frame = Frame(root)
global show_plot
if show_plot: # if plot already present, remove plot and destroy NavigationToolbar
canvas.get_tk_widget().grid_forget()
toolbar_frame.destroy()
toolbar_frame = Frame(root)
toolbar = NavigationToolbar2TkAgg(canvas, toolbar_frame)
toolbar_frame.grid(row=21,column=4,columnspan=3)
canvas.get_tk_widget().grid(row=1,column=4,columnspan=3,rowspan=20)
show_plot = True
However, the updating approach showed by Hans below is much nicer since you don't have to destroy and recreate anything. I just wanted to highlight that the issue with my approach (apart from the inelegance and performance) was probably that I didn't use destroy().
A slightly different approach might be to reuse the figure for subsequent plots by clearing & redrawing it. That way, you don't have to destroy & regenerate neither the figure nor the toolbar:
from Tkinter import Tk, Button
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
# plotting function: clear current, plot & redraw
def plot(x, y):
plt.clf()
plt.plot(x,y)
# just plt.draw() won't do it here, strangely
plt.gcf().canvas.draw()
# just to see the plot change
plotShift = 0
def main():
global plotShift
x = np.arange(0.0,3.0,0.01)
y = np.sin(2*np.pi*x + plotShift)
plot(x, y)
plotShift += 1
# GUI
root = Tk()
draw_button = Button(root, text="Plot!", command = main)
draw_button.grid(row=0, column=0)
# init figure
fig = plt.figure()
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2TkAgg(canvas, root)
canvas.get_tk_widget().grid(row=0,column=1)
toolbar.grid(row=1,column=1)
root.mainloop()
When you press the "Plot!" button it calls main. main creates the navigation toolbar. So, each time you press the button you get a toolbar. I don't know much about matplot, but it's pretty obvious why you get multiple toolbars.
By the way, grid_forget doesn't destroy the widget, it simply removes it from view. Even if you don't see it, it's still in memory.
Typically in a GUI, you create all the widgets exactly once rather than recreating the same widgets over and over.