How can I redraw only certain matplotlib artists? - python

I'm working on a custom interactive figure for electrophysiology data, anywhere from 10-400 lines (EEG or MEG data channels) plotted as a LineCollection with offsets. It is often useful to have a vertical line to assess how signal features on different channels align temporally, so I have a button_press_event listener that creates an axvline (or updates the xdata of the line, if it already exists). Redrawing the axvline is expensive if there are lots of channels in the LineCollection, but the supposedly more efficient redraw method (ax.draw_artist(my_vline)) doesn't work at all (it is quite possible that I am simply misunderstanding how draw_artist is supposed to work).
Code for reproduction
import matplotlib.pyplot as plt
plt.ion()
def make_vline(event):
ax = event.inaxes
if getattr(ax, 'my_vline', None) is None:
ax.my_vline = ax.axvline(event.xdata, linewidth=4, color='r')
else:
ax.my_vline.set_xdata(event.xdata)
# I thought any 1 of these 3 lines would move the vline to the click location:
ax.draw_artist(ax.my_vline) # this has no visible effect
ax.redraw_in_frame() # TypeError (see below for traceback)
ax.figure.canvas.draw_idle() # works, but slow when figure has many lines
fig, ax = plt.subplots()
callback_id = fig.canvas.mpl_connect('button_press_event', make_vline)
Actual outcome
If I use the ax.draw_artist(ax.my_vline) line, outcome is a blank axes no matter where I click (unless I then resize the figure, which triggers a redraw and then the line appears).
If i use the ax.redraw_in_frame() line, I get:
Traceback (most recent call last):
File "/opt/miniconda3/envs/mnedev/lib/python3.8/site-packages/matplotlib/cbook/__init__.py", line 224, in process
func(*args, **kwargs)
File "<ipython-input-1-08572d18e6b3>", line 11, in make_vline
ax.redraw_in_frame()
File "/opt/miniconda3/envs/mnedev/lib/python3.8/site-packages/matplotlib/axes/_base.py", line 2778, in redraw_in_frame
stack.push(artist.set_visible, artist.get_visible())
TypeError: push() takes 2 positional arguments but 3 were given
if I use ax.figure.canvas.draw_idle() it works as expected, but is really slow once the figure has actual data in it. Here is a longer code snippet you can run locally to see the slowness:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
plt.ion()
rng = np.random.default_rng()
def make_vline(event):
ax = event.inaxes
if getattr(ax, 'my_vline', None) is None:
ax.my_vline = ax.axvline(event.xdata, linewidth=4, color='r', zorder=3)
else:
ax.my_vline.set_xdata(event.xdata)
ax.figure.canvas.draw_idle() # works, but slow when figure has many lines
def add_line_collection(ax):
n_chans = 400
n_times = 10001
xs = np.linspace(0, 10, n_times)
ys = rng.normal(size=(n_chans, n_times)) * 1e-6
segments = [np.vstack((xs, ys[n])).T for n in range(n_chans)]
yoffsets = np.arange(n_chans)
offsets = np.vstack((np.zeros_like(yoffsets), yoffsets)).T
lc = LineCollection(segments, offsets=offsets, linewidths=0.5, colors='k')
ax.add_collection(lc)
ax.set_xlim(xs[0], xs[-1])
ax.set_ylim(yoffsets[0] - 0.5, yoffsets[-1] + 0.5)
ax.set_yticks(yoffsets)
fig, ax = plt.subplots()
add_line_collection(ax)
callback_id = fig.canvas.mpl_connect('button_press_event', make_vline)
Questions
when would ax.draw_artist(my_artist) actually work / what is it supposed to do?
Is my example a case where blitting would be beneficial?
Any other ideas for how to speed up (re)drawing here?
Matplotlib version
Operating system: Xubuntu 20.04
Matplotlib version: 3.3.1 (conda-forge)
Matplotlib backend: Qt5Agg
Python version: 3.8.5
Jupyter version (if applicable): n/a
Other libraries: numpy 1.19.1 (conda-forge)

I solved this with blitting, based on the MPL blitting tutorial:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
plt.ion()
rng = np.random.default_rng()
def make_vline(event):
fig.canvas.restore_region(fig.my_bg)
ax = event.inaxes
if getattr(ax, 'my_vline', None) is None:
ax.my_vline = ax.axvline(event.xdata, linewidth=4, color='r', zorder=3)
else:
ax.my_vline.set_xdata(event.xdata)
ax.draw_artist(ax.my_vline)
ax.figure.canvas.blit()
ax.figure.canvas.flush_events()
def add_line_collection(ax):
n_chans = 400
n_times = 10001
xs = np.linspace(0, 10, n_times)
ys = rng.normal(size=(n_chans, n_times)) * 1e-6
segments = [np.vstack((xs, ys[n])).T for n in range(n_chans)]
yoffsets = np.arange(n_chans)
offsets = np.vstack((np.zeros_like(yoffsets), yoffsets)).T
lc = LineCollection(segments, offsets=offsets, linewidths=0.5, colors='k')
ax.add_collection(lc)
ax.set_xlim(xs[0], xs[-1])
ax.set_ylim(yoffsets[0] - 0.5, yoffsets[-1] + 0.5)
ax.set_yticks(yoffsets)
fig, ax = plt.subplots()
add_line_collection(ax)
callback_id = fig.canvas.mpl_connect('button_press_event', make_vline)
plt.pause(0.1)
fig.my_bg = fig.canvas.copy_from_bbox(fig.bbox)
Note that this will not work if the figure is resized, you would need to rerun the copy_from_bbox line in a resize listener.

Related

matplotlib: how to put picture to a specific point of data-coordinate system

I'm trying to add picture at the plot in specific place in it by a given coordinates in data-coordinate system. However it runs a little bit unpredicted. Here is a code snippet I wrote:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(5,5))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.grid('on')
x = np.arange(0, 1, 0.005)
y = np.exp(-x/2.) * np.sin(2*np.pi*x)
ax.plot(x, y)
im_orange = plt.imread('./test100x100_orange.png')
im_blue = plt.imread('./test100x100_blue.png')
x0, y0 = ax.transData.transform((0,0))
print('transData(0,0) = {}'.format(ax.transData.transform((0,0))))
ax.figure.figimage(im_orange, alpha=0.5)
ax.figure.figimage(im_blue, x0, y0, alpha=0.5)
ax.transData.transform((0,0)) returns transData(0,0) = [45. 45.] which is unexpected since doesn't represent actual position of (0,0) on the plot. Here is result image as well:
My base question is how to put picture left bottom corner exactly at (0,0) point in data-coordinates? And if possible please explain such behaviour of matplotlib.
Update. A few experiments on top. I've run a slightly modified script (provided below by Iammuratc but with plt.savefig() instead) in 3 modes:
From python console (copy and paste):
Python script (something like python test.py:
Result is the same as before.
ipython from Jupiter Notebook:
Suboption A: plt.show()
Suboption B: plt.savefig()
Now it's even more confusing..
It works for me how you did it. You might check the image arrays in case they are shifted.
import numpy as np
import matplotlib.pyplot as plt
im_orange = np.zeros((128,128,3),'uint8')
im_orange[:,:,0] = 255
im_blue = np.zeros((128,128,3),'uint8')
im_blue[:,:,2] = 255
fig, ax = plt.subplots(figsize=(5,5))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.grid('on')
x = np.arange(0, 1, 0.005)
y = np.exp(-x/2.) * np.sin(2*np.pi*x)
ax.plot(x, y)
# im_orange = plt.imread('./test100x100_orange.png')
# im_blue = plt.imread('./test100x100_blue.png')
x0, y0 = ax.transData.transform((0,0))
print('transData(0,0) = {}'.format(ax.transData.transform((0,0))))
ax.figure.figimage(im_orange,x0,y0, alpha=0.5)
ax.figure.figimage(im_blue, x0, y0, alpha=0.5)
plt.show()

Animating a time-dependent LineCollection using matplotlib

As stated above, I am trying to animate a set of data that varies over time (position). I would like my graph to only show the position data but animate the position history over time. I have started with this example here, and got it working. Now, instead of the whole line animating, I would like for the line to be drawn from left to right. I also need the line to be colored relative to a secondary set of data, which I have been able to accomplish with a LineCollection.
My code:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
from matplotlib.collections import LineCollection
from matplotlib.colors import ListedColormap, BoundaryNorm
# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
line = LineCollection([], cmap=plt.cm.jet)
line.set_array(np.linspace(0, 2, 1000))
ax.add_collection(line)
x = np.linspace(0, 2, 10000)
y = np.sin(2 * np.pi * (x))
# initialization function: plot the background of each frame
def init():
line.set_segments([])
return line,
# animation function. This is called sequentially
def animate(i, xss, yss, line):
xs = xss[:i]
ys = yss[:i]
points = np.array([xs, ys]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
line.set_segments(segments)
return line,
# call the animator. blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, fargs=[x, y, line], init_func=init, frames=200, interval=20)
plt.show()
I create a basic sine wave data set and again would like to animate the line being drawn from left to right. Right now, the LineCollection is being colored by the y-value of the line at the current x-position. Eventually, this will be a position data set pulled from a .csv file.
Finally, the issue. The code above runs without errors, however the line is not being drawn. I can see in my debugger that the xs and ys arrays are being added to during each step so that syntax seems to be working, just the updated LineCollection is not being displayed.
I am working on macOS Mojave 10.14.6.
Your code is correct, the line you're plotting is just very small. This is because the function you animate is given by
x = np.linspace(0, 2, 10000) # Note that `num=10000`
y = np.sin(2 * np.pi * (x))
which has 10000 points, but you only animate the first 200 points.
anim = animation.FuncAnimation(..., frames=200, interval=20)
Easy fix
num_frames = 200
x = np.linspace(0, 2, num_frames)
...
anim = animation.FuncAnimation(..., frames=num_frames, interval=20)

Animating a line plot over time in Python

Time series data is data over time. I am trying to animate a line plot of time series data in python. In my code below this translates to plotting xtraj as they and trange as the x. The plot does not seem to be working though.
I have found similar questions on Stack overflow but none of the solutions provided here seem to work. Some similar questions are matplotlib animated line plot stays empty, Matplotlib FuncAnimation not animating line plot and a tutorial referencing the help file Animations with Matplotlib.
I begin by creating the data with the first part and simulating it with the second. I tried renaming the data that would be used as y-values and x-values in order to make it easier to read.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
dt = 0.01
tfinal = 5.0
x0 = 0
sqrtdt = np.sqrt(dt)
n = int(tfinal/dt)
xtraj = np.zeros(n+1, float)
trange = np.linspace(start=0,stop=tfinal ,num=n+1)
xtraj[0] = x0
for i in range(n):
xtraj[i+1] = xtraj[i] + np.random.normal()
x = trange
y = xtraj
# animation line plot example
fig = plt.figure(4)
ax = plt.axes(xlim=(-5, 5), ylim=(0, 5))
line, = ax.plot([], [], lw=2)
def init():
line.set_data([], [])
return line,
def animate(i):
line.set_data(x[:i], y[:i])
return line,
anim = animation.FuncAnimation(fig, animate, init_func=init, frames=len(x)+1,interval=200, blit=False)
plt.show()
Any help would be highly appreciated. I am new to working in Python and particularly trying to animate plots. So I must apologize if this question is trivial.
Summary
So to summarize my question how does one animate time series in Python, iterating over the time steps (x-values).
Check this code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
dt = 0.01
tfinal = 1
x0 = 0
sqrtdt = np.sqrt(dt)
n = int(tfinal/dt)
xtraj = np.zeros(n+1, float)
trange = np.linspace(start=0,stop=tfinal ,num=n+1)
xtraj[0] = x0
for i in range(n):
xtraj[i+1] = xtraj[i] + np.random.normal()
x = trange
y = xtraj
# animation line plot example
fig, ax = plt.subplots(1, 1, figsize = (6, 6))
def animate(i):
ax.cla() # clear the previous image
ax.plot(x[:i], y[:i]) # plot the line
ax.set_xlim([x0, tfinal]) # fix the x axis
ax.set_ylim([1.1*np.min(y), 1.1*np.max(y)]) # fix the y axis
anim = animation.FuncAnimation(fig, animate, frames = len(x) + 1, interval = 1, blit = False)
plt.show()
The code above reproduces this animation:

Is it possible to change speed during an animation in matplotlib

The question, in brief, is: is it possible (with the tools of matplotlib.animation or other modules for python) to obtain a slow-motion on certain frames of the animation?
Some context:
I have a matplotlib animated plot in which I am varying one variable and showing a contour plot over two other ones. My idea was to slow down the animation while I am near the maximum of the function, so that I can more clearly pinpoint it, while accelerate far from it where there is not much interest.
At the moment, my best idea is to double the frames closest to the maximum, but can someone have a better idea?
Thank you everyone!
Code snippet:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
X = np.linspace(1,10, 100)
Y = np.linspace(1,10, 100)
R = np.linspace(-1, 1, 100)
ims = []
for r in R:
z = func(X, Y, r)
im = plt.imshow(z)
ims.append(im)
if check_r(r):
ims.append(im)
where func() is a function that return a (len(X), len(Y)) array that depends on r (for instance Z[i,j] = X[i]**r * Y[j]**(1-r) or whatever, while check_r() test if r is within the range of the values that need to be maximized.
Your idea is the best, I think. And I've found another way using matplotlib animation. The idea is that use frames as slow delay, by making same points.
In this example just sin curve is plotted but it will be applied other functions.
(most of code is took from here)
import numpy as np
import matplotlib.animation as animation
import matplotlib.pylab as plt
import pandas as pd
TWOPI = 2*np.pi
fig, ax = plt.subplots()
# making frames "delay"
frames = np.arange(0.0, TWOPI, 0.1)
frames = np.insert(frames, 17, [1.7]*5)
frames = np.insert(frames, 16, [1.6]*5)
frames = np.insert(frames, 15, [1.5]*5)
t = np.arange(0.0, TWOPI, 0.001)
s = np.sin(t)
l = plt.plot(t, s)
ax = plt.axis([0,TWOPI,-1,1])
redDot, = plt.plot([0], [np.sin(0)], 'ro')
def animate(i):
redDot.set_data(i, np.sin(i))
return redDot,
myAnimation = animation.FuncAnimation(fig, animate, frames=frames,
interval=100, blit=True, repeat=True)

How to update a plot in matplotlib

I'm having issues with redrawing the figure here. I allow the user to specify the units in the time scale (x-axis) and then I recalculate and call this function plots(). I want the plot to simply update, not append another plot to the figure.
def plots():
global vlgaBuffSorted
cntr()
result = collections.defaultdict(list)
for d in vlgaBuffSorted:
result[d['event']].append(d)
result_list = result.values()
f = Figure()
graph1 = f.add_subplot(211)
graph2 = f.add_subplot(212,sharex=graph1)
for item in result_list:
tL = []
vgsL = []
vdsL = []
isubL = []
for dict in item:
tL.append(dict['time'])
vgsL.append(dict['vgs'])
vdsL.append(dict['vds'])
isubL.append(dict['isub'])
graph1.plot(tL,vdsL,'bo',label='a')
graph1.plot(tL,vgsL,'rp',label='b')
graph2.plot(tL,isubL,'b-',label='c')
plotCanvas = FigureCanvasTkAgg(f, pltFrame)
toolbar = NavigationToolbar2TkAgg(plotCanvas, pltFrame)
toolbar.pack(side=BOTTOM)
plotCanvas.get_tk_widget().pack(side=TOP)
You essentially have two options:
Do exactly what you're currently doing, but call graph1.clear() and graph2.clear() before replotting the data. This is the slowest, but most simplest and most robust option.
Instead of replotting, you can just update the data of the plot objects. You'll need to make some changes in your code, but this should be much, much faster than replotting things every time. However, the shape of the data that you're plotting can't change, and if the range of your data is changing, you'll need to manually reset the x and y axis limits.
To give an example of the second option:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 6*np.pi, 100)
y = np.sin(x)
# You probably won't need this if you're embedding things in a tkinter plot...
plt.ion()
fig = plt.figure()
ax = fig.add_subplot(111)
line1, = ax.plot(x, y, 'r-') # Returns a tuple of line objects, thus the comma
for phase in np.linspace(0, 10*np.pi, 500):
line1.set_ydata(np.sin(x + phase))
fig.canvas.draw()
fig.canvas.flush_events()
You can also do like the following:
This will draw a 10x1 random matrix data on the plot for 50 cycles of the for loop.
import matplotlib.pyplot as plt
import numpy as np
plt.ion()
for i in range(50):
y = np.random.random([10,1])
plt.plot(y)
plt.draw()
plt.pause(0.0001)
plt.clf()
This worked for me. Repeatedly calls a function updating the graph every time.
import matplotlib.pyplot as plt
import matplotlib.animation as anim
def plot_cont(fun, xmax):
y = []
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
def update(i):
yi = fun()
y.append(yi)
x = range(len(y))
ax.clear()
ax.plot(x, y)
print i, ': ', yi
a = anim.FuncAnimation(fig, update, frames=xmax, repeat=False)
plt.show()
"fun" is a function that returns an integer.
FuncAnimation will repeatedly call "update", it will do that "xmax" times.
This worked for me:
from matplotlib import pyplot as plt
from IPython.display import clear_output
import numpy as np
for i in range(50):
clear_output(wait=True)
y = np.random.random([10,1])
plt.plot(y)
plt.show()
I have released a package called python-drawnow that provides functionality to let a figure update, typically called within a for loop, similar to Matlab's drawnow.
An example usage:
from pylab import figure, plot, ion, linspace, arange, sin, pi
def draw_fig():
# can be arbitrarily complex; just to draw a figure
#figure() # don't call!
plot(t, x)
#show() # don't call!
N = 1e3
figure() # call here instead!
ion() # enable interactivity
t = linspace(0, 2*pi, num=N)
for i in arange(100):
x = sin(2 * pi * i**2 * t / 100.0)
drawnow(draw_fig)
This package works with any matplotlib figure and provides options to wait after each figure update or drop into the debugger.
In case anyone comes across this article looking for what I was looking for, I found examples at
How to visualize scalar 2D data with Matplotlib?
and
http://mri.brechmos.org/2009/07/automatically-update-a-figure-in-a-loop
(on web.archive.org)
then modified them to use imshow with an input stack of frames, instead of generating and using contours on the fly.
Starting with a 3D array of images of shape (nBins, nBins, nBins), called frames.
def animate_frames(frames):
nBins = frames.shape[0]
frame = frames[0]
tempCS1 = plt.imshow(frame, cmap=plt.cm.gray)
for k in range(nBins):
frame = frames[k]
tempCS1 = plt.imshow(frame, cmap=plt.cm.gray)
del tempCS1
fig.canvas.draw()
#time.sleep(1e-2) #unnecessary, but useful
fig.clf()
fig = plt.figure()
ax = fig.add_subplot(111)
win = fig.canvas.manager.window
fig.canvas.manager.window.after(100, animate_frames, frames)
I also found a much simpler way to go about this whole process, albeit less robust:
fig = plt.figure()
for k in range(nBins):
plt.clf()
plt.imshow(frames[k],cmap=plt.cm.gray)
fig.canvas.draw()
time.sleep(1e-6) #unnecessary, but useful
Note that both of these only seem to work with ipython --pylab=tk, a.k.a.backend = TkAgg
Thank you for the help with everything.
All of the above might be true, however for me "online-updating" of figures only works with some backends, specifically wx. You just might try to change to this, e.g. by starting ipython/pylab by ipython --pylab=wx! Good luck!
Based on the other answers, I wrapped the figure's update in a python decorator to separate the plot's update mechanism from the actual plot. This way, it is much easier to update any plot.
def plotlive(func):
plt.ion()
#functools.wraps(func)
def new_func(*args, **kwargs):
# Clear all axes in the current figure.
axes = plt.gcf().get_axes()
for axis in axes:
axis.cla()
# Call func to plot something
result = func(*args, **kwargs)
# Draw the plot
plt.draw()
plt.pause(0.01)
return result
return new_func
Usage example
And then you can use it like any other decorator.
#plotlive
def plot_something_live(ax, x, y):
ax.plot(x, y)
ax.set_ylim([0, 100])
The only constraint is that you have to create the figure before the loop:
fig, ax = plt.subplots()
for i in range(100):
x = np.arange(100)
y = np.full([100], fill_value=i)
plot_something_live(ax, x, y)

Categories

Resources