Updating pyplot.vlines in interactive plot - python-3.x

I need your help. Please consider the code below, which plots a sinusoid using pylab in IPython. A slider below the axis enables the user to adjust the frequency of the sinusoid interactively.
%pylab
# setup figure
fig, ax = subplots(1)
fig.subplots_adjust(left=0.25, bottom=0.25)
# add a slider
axcolor = 'lightgoldenrodyellow'
ax_freq = axes([0.3, 0.13, 0.5, 0.03], axisbg=axcolor)
s_freq = Slider(ax_freq, 'Frequency [Hz]', 0, 100, valinit=a0)
# plot
g = linspace(0, 1, 100)
f0 = 1
sig = sin(2*pi*f0*t)
myline, = ax.plot(sig)
# update plot
def update(value):
f = s_freq.val
new_data = sin(2*pi*f*t)
myline.set_ydata(new_data) # crucial line
fig.canvas.draw_idle()
s_freq.on_changed(update)
Instead of the above, I need to plot the signal as vertical lines, ranging from the amplitude of each point in t to the x-axis. Thus, my first idea was to use vlines instead of plot in line 15:
myline = ax.vlines(range(len(sig)), 0, sig)
This solution works in the non-interactive case. The problem is, plot returns an matplotlib.lines.Line2D object, which provides the set_ydata method to update data interactively. The object returned by vlines is of type matplotlib.collections.LineCollection and does not provide such a method.
My question: how do I update a LineCollection interactively?

Using #Aaron Voelker's comment of using set_segments and wrapping it up in a function:
def update_vlines(*, h, x, ymin=None, ymax=None):
seg_old = h.get_segments()
if ymin is None:
ymin = seg_old[0][0, 1]
if ymax is None:
ymax = seg_old[0][1, 1]
seg_new = [np.array([[xx, ymin],
[xx, ymax]]) for xx in x]
h.set_segments(seg_new)
Analog for hlines:
def update_hlines(*, h, y, xmin=None, xmax=None):
seg_old = h.get_segments()
if xmin is None:
xmin = seg_old[0][0, 0]
if xmax is None:
xmax = seg_old[0][1, 0]
seg_new = [np.array([[xmin, yy],
[xmax, yy]]) for yy in y]
h.set_segments(seg_new)

I will give examples for vlines here.
If you have multiple lines, #scleronomic solution works perfect. You also might prefer one-liner:
myline.set_segments([np.array([[x, x_min], [x, x_max]]) for x in xx])
If you need to update only maximums, then you can do this:
def update_maxs(vline):
vline[:,1] = x_min, x_max
return vline
myline.set_segments(list(map(update_maxs, x.get_segments())))
Also this example could be useful: LINK

Related

Matplotlib - Change draw order and moving annotation

I'm attempting to plot 2 pairs of (x,y) data and show how much distance is between them.
I have 2 issues with the plot it stands:
When the data points fall on the axis they are being draw behind them, I'd prefer them in front (red data point above).
The text annotation is fixed where it's drawn, this means either the data or the legend can cover it when data points are in the top right or top left quadrants.
The desired output would be a draw order of Axes -> Scatter -> Quiver and then for the text annotation to be drawn in whichever quadrant is not occupied by a data point or the legend.
For issue 1 I've tried combinations of clipon=True and zorder= for all the plot elements but can't seem to bring them in front.
For issue 2 I've considered checking which quadrants the data points are in, draw the legend and check which quadrant that is in and then finally draw the annotation in the remaining unoccupied quadrant(s). However I've struggled to get the correct legend position with legend.get_window_extent() and was hoping there was an easier method of moving the annotation, similar to rcParams["legend.loc"]='Best'. I can't see anything obvious at https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.text.html
Any help would be greatly appreciated, below is the code used to produce the plot. Thanks!
#!/usr/bin/env python3
import matplotlib
matplotlib.use('qt5agg')
import matplotlib.pyplot as plt
data = [[-0.4, 0.4], [0.2, -0.01]]
#data = [[0.4, 0.4], [0.2, -0.01]]
fig, ax = plt.subplots(figsize=(4.5, 3.25), num="Stack Example")
x, y = (zip(*data))
dx = x[1]-x[0]
dy = y[1]-y[0]
c = [0, 1]
scatter = ax.scatter(x, y, c=c, cmap='rainbow', s=250, marker="o")
legend = ax.legend(*scatter.legend_elements(),
title="Legend", fontsize=8, title_fontsize=8)
ax.add_artist(legend)
ax.quiver(x[0], y[0], dx, dy, angles='xy', scale_units='xy', scale=1, headwidth=3)
textstr = '\n'.join((
r'$dx$=%.2f mm' % (dx),
r'$dy$=%.2f mm' % (dy)))
ax.text(0.04, 0.95, textstr, transform=ax.transAxes, fontsize=9, verticalalignment='top')
ax.spines[['left', 'bottom']].set_position('zero')
ax.spines[['top', 'right']].set_visible(False)
ax.set_xlim([-0.5, 0.5])
ax.set_ylim([-0.5, 0.5])
ax.set_xticks([-0.5, -0.25, 0.25, 0.5])
ax.set_yticks([-0.5, -0.25, 0.25, 0.5])
ax.set_xlabel('$x$ $/$ $mm$', fontsize=9)
ax.xaxis.set_label_coords(1.0, 0.4)
ax.set_ylabel('$y$ $/$ $mm$', fontsize=9)
ax.yaxis.set_label_coords(0.57, 1.0)
plt.xticks(fontsize=9)
plt.yticks(fontsize=9)
plt.tight_layout()
fig.canvas.toolbar.setVisible(False)
plt.show()
UPDATE
I've fixed issue 2 as I mentioned above, it's not pretty but works for each of the usage cases I've tried so far.
def get_quadrants(data):
quadrants = []
for datapoint in data:
x = datapoint[0]
y = datapoint[1]
if x < 0 and y < 0:
quadrants.append(2)
elif x < 0 and y > 0:
quadrants.append(0)
elif x > 0 and y < 0:
quadrants.append(3)
else:
quadrants.append(1)
text_quadrant = max(sorted(set((range(4))) - set(quadrants)))
if len(set([2, 3]) - set(quadrants)) == 0:
text_quadrant = 0
if text_quadrant == 0:
x, y = 0.0, 0.95
elif text_quadrant == 1:
x, y = 0.75, 0.95
elif text_quadrant == 2:
x, y = 0.0, 0.15
else:
x, y = 0.75, 0.15
return x, y

How do I put a simple caption below my x axis in Matplotib?

I am trying to put a simple description of my plot right below the x axis with plt.text. Either there is not enough room or it's in my plot. Can someone help. Here is my code and what it looks like.
def econPlot1(plot1_data):
x = list(range(plot1_data.shape[0]))
y1 = plot1_data[:, 1]
# plotting the line 1 points
plt.plot(x, y1, label = "FFR")
# line 2 points
y2 = plot1_data[:, 2]
#fig = plt.figure()
plt.axis([0, 10, 0, 10])
t = ("This is a really long string that I'd rather have wrapped so that it "
"doesn't go outside of the figure, but if it's long enough it will go "
"off the top or bottom!")
plt.text(-1, 0, t, ha='center', rotation=0, wrap=True)
# plotting the line 2 points
plt.plot(x, y2, label = "Inflation")
plt.xlabel('time')
x_tick_indices = list(range(0, plot1_data.shape[0], 12))
x_tick_values = x_tick_indices
x_tick_labels = [plot1_data[i, 0] for i in x_tick_indices]
plt.xticks(x_tick_values, x_tick_labels, rotation ='vertical')
# Set a title of the current axes.
plt.title('FFR vs Inflation over time')
# show a legend on the plot
#plt.legend()
# Display a figure.
plt.show()
logging.debug('plot1 is created')
I managed to put your text at the bottom of the figure the following way:
import textwrap
# Operations on the source data
x = list(range(plot1_data.shape[0]))
y1 = plot1_data[:, 1]
y2 = plot1_data[:, 2]
x_tick_indices = list(range(0, plot1_data.shape[0], 12))
x_tick_values = x_tick_indices
x_tick_labels = [plot1_data[i, 0] for i in x_tick_indices]
t = "This is a really long string that I'd rather have wrapped so that it doesn't go "\
"outside of the figure, but if it's long enough it will go off the top or bottom!"
tt = textwrap.fill(t, width=70)
# Plotting
plt.plot(x, y1, label='FFR')
plt.plot(x, y2, label='Inflation')
plt.xlabel('Time')
plt.xticks(x_tick_values, x_tick_labels, rotation ='vertical')
plt.title('FFR vs Inflation over time')
plt.text(len(x) / 2, 0, tt, ha='center', va='top');
My experience indicates that plt.text does not support wrap parameter,
so I wrapped it using textwrap.fill.
I also didn't call plt.axis, relying on default limits for both x and y. If you need to set limits, do it rather only for y axis,
e.g. plt.ylim((0, 8)), but then you will have to adjust also y parameter
in plt.text.
For source data limited to 3 years (for each month in these 3 years and
Jan 1 the next year) I got the following result:

How to insert Gaussian/Normal distribution in multiple subplots?

So I have multiple plots, using subplot and I would like to add the Gaussian distribution on it. I have done it, in a for loop for each plot separately, but I am not sure how to do it using subplots. At the moment it does not show anything on the subplots.
def index_of(arrval, value):
if value < min(arrval):
return 0
return max(np.where(arrval <= value)[0])
# load file using loadtxt
for file in filename:
data = np.loadtxt(file,delimiter='\t', skiprows=2)
for x,y in data:
x = data[:,0]
y = data[:,1]
xs.append(x)
ys.append(y)
# Make the subplots
for i, (x, y) in enumerate(zip(xs, ys)):
ij = np.unravel_index(i, axs.shape)
axs[ij].plot(x, y,label = lsnames[i])
axs[ij].set_title(lsnames[i])
axs[ij].legend()
# Using one of the lmfit functions to get the Gaussian plot.
# But it does not show anything
gauss1 = GaussianModel(prefix='g1_')
gauss2 = GaussianModel(prefix='g2_')
pars = gauss1.guess(y, x=x)
pars.update(gauss2.make_params())
ix1 = index_of(x, 20)
ix2 = index_of(x, 40)
ix3 = index_of(x, 75)
gauss1.guess(y[ix1:ix2], x=x[ix1:ix2])
gauss2.guess(y[ix2:ix3], x=x[ix2:ix3])
mod = gauss1 + gauss2
mod = GaussianModel()
pars = mod.guess(y, x=x)
out = mod.fit(y, pars, x=x)
print(out.fit_report(min_correl=0.25))
plt.show()
Maybe I'm not fully understanding, but this seems like it could just be a looping question or even an indentation problem.
I think what you're asking to do is something like:
# loop over datasets, putting each in a subplot
for i, (x, y) in enumerate(zip(xs, ys)):
ij = np.unravel_index(i, axs.shape)
axs[ij].plot(x, y,label = lsnames[i])
axs[ij].set_title(lsnames[i])
axs[ij].legend()
# fit this dataset with 1 gaussian
mod = GaussianModel()
pars = mod.guess(y, x=x)
out = mod.fit(y, pars, x=x)
# plot best-fit
axs[ij].plot(x, out.best_fit, label='fit')
print("Data Set %d" % i)
print(out.fit_report(min_correl=0.25))
plt.show()
Your code was sort of confusingly making a model with two Gaussians and then not using it. It would be fine to use a more complicated model in the loop.
Hope that helps.

Animating multiple Circles in each frames in Python

I am trying to create the animation in this video using Python. But I stuck on the very first step. Till now I've created a Circle and a point rotating around its circumference. My code is given below. Now I want to plot the y values corresponding to x=np.arange(0, I*np.pi, 0.01) along the x-axis (as shown in update() function in the code). For this I have to define another function to plot these x and y and pass that function inside a new animation.FuncAnimation().
Is there any way to plot everything using only the update() function?
Note I have found a code of this animation in here. But it is written in Java!
My Code
import matplotlib.pyplot as plt
from matplotlib import animation
import numpy as np
W = 6.5
H = 2
radius = 1
I = 2
T = 3
N = 2
plt.style.use(['ggplot', 'dark_background'])
def create_circle(x, y, r):
circle = plt.Circle((x, y), radius=r, fill=False, alpha=0.7, color='w')
return circle
def create_animation():
fig = plt.figure()
ax = plt.axes(xlim=(-2, W + 2), ylim=(-H, H))
circle = create_circle(0, 0, radius)
ax.add_patch(circle)
line1, = ax.plot(0, 1, marker='o', markersize=3, color='pink', alpha=0.7)
def update(theta):
x = radius * np.cos(theta)
y = radius * np.sin(theta)
line1.set_data([0, x], [0, y])
return line1,
anim = []
anim.append(animation.FuncAnimation(fig, update,
frames=np.arange(0, I * np.pi, 0.01),
interval=10, repeat=True))
# anim.append(animation.FuncAnimation(fig, update_line, len(x),
# fargs=[x, y, line, line1], interval=10))
plt.grid(False)
plt.gca().set_aspect('equal')
plt.gca().spines['left'].set_visible(False)
plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)
plt.gca().spines['bottom'].set_visible(False)
plt.gca().set_xticks([])
plt.gca().set_yticks([])
plt.show()
if __name__ == '__main__':
create_animation()
Edit. I've improved the task by defining a global variable pos and changing the update() function in the following manner ...The animation now looks better but still having bugs!
Improved Portion
plot, = ax.plot([], [], color='w', alpha=0.7)
level = np.arange(0, I * np.pi, 0.01)
num = []
frames = []
for key, v in enumerate(level):
num.append(key)
frames.append(v)
def update(theta):
global pos
x = radius * np.cos(theta)
y = radius * np.sin(theta)
wave.append(y)
plot.set_data(np.flip(level[:pos] + T), wave[:pos])
line1.set_data([0, x], [0, y])
pos += 1
return line1, plot,
Edit Till now I've done the following:
def update(theta):
global pos
x, y = 0, 0
for i in range(N):
prev_x = x
prev_y = y
n = 2 * i + 1
rad = radius * (4 / (n * np.pi))
x += rad * np.cos(n * theta)
y += rad * np.sin(n * theta)
wave.append(y)
circle = create_circle(prev_x, prev_y, rad)
ax.add_patch(circle)
plot.set_data(np.flip(level[:pos] + T), wave[:pos])
line2.set_data([x, T], [y, y])
line1.set_data([prev_x, x], [prev_y, y])
pos += 1
return line1, plot, line2,
Output
Please help to correct this animation. Or, is there any efficient way to do this animation?
Edit Well, now the animation is partially working. But there is a little issue: In my code (inside the definition of update()) I have to add circles centered at (prev_x, prev_y) of radius defined as rad for each frame. For this reason I try to use a for loop in the definition of update() but then all the circles remains in the figure (see the output below). But I want one circle in each frame with the centre and radius as mentioned above. Also the same problem is with the plot. I try to use ax.clear() inside the for loop but it didn't work.

GNUPLOT : Plot sum of distributions given by parameters in text file

Suppose I have a text file with an arbitrary number of rows where each row gives some set of parameters that define a function (say the (x,y) location and sigmas (possibility unequal) of a 2D Gaussian). For example, in that case, the text file might contain:
100 112 3 4
97 38 8 9
88 79 3 9
...
...
102 152 9 5
I would like to plot (using pm3d) the SUM of all the distributions defined by the text file. How can that be done?
I would like to plot (using pm3d) the SUM of all the distributions defined by the text file. How can that be done?
I think that cannot be done in native gnuplot, at least not in any sane way that I know. That kind of number crunching is not really what gnuplot is designed to do.
Python, however, makes it pretty doable...
#!/usr/bin/env python2
import math
import numpy
import os
class Gaussian(object):
'''A 2D gaussian function (normalized), defined by
mx: x mean position
my: y mean position
sx: sigma in x
sy: sigma in y'''
def __init__(self, mx, my, sx, sy):
self.mx = float(mx)
self.my = float(my)
self.sx = float(sx)
self.sy = float(sy)
def value(self,x,y):
'''Evaluates the value of a Gaussian at a certain point (x,y)'''
prefactor = (1.0/(self.sx*self.sy*2*math.pi))
ypart = math.exp(-(x - self.mx)**2/(2*self.sx**2))
xpart = math.exp(-(y - self.my)**2/(2*self.sy**2))
return prefactor*ypart*xpart
def getLimits(self, devs):
'''Finds the upper and lower x and y limits for the distribution,
defined as points a certain number of deviations from the mean.'''
xmin = self.mx - devs*self.sx
xmax = self.mx + devs*self.sx
ymin = self.my - devs*self.sy
ymax = self.my + devs*self.sy
return (xmin, xmax, ymin, ymax)
def readGaussians(filename):
'''reads in gaussian parameters from an input file in the format
mx my sx sy
This makes some assumptions about how perfect the input file will be'''
gaussians = []
with open(filename, 'r') as f:
for line in f.readlines():
(mx, my, sx, sy) = line.split()
gaussians.append(Gaussian(mx, my, sx, sy))
return gaussians
def getPlotLimits(gaussians, devs):
'''finds the x and y limits of the field of gaussian distributions.
Sets the boundary a set number of deviations from the mean'''
# get the limits from the first gaussian and work from there
(xminlim, xmaxlim, yminlim, ymaxlim) = gaussians[0].getLimits(devs)
for i in range(1, len(gaussians)):
(xmin, xmax, ymin, ymax) = gaussians[i].getLimits(devs)
if (xmin < xminlim):
xminlim = xmin
if (xmax > xmaxlim):
xmaxlim = xmax
if (ymin < yminlim):
yminlim = ymin
if (ymax > ymaxlim):
ymaxlim = ymax
return (xminlim, xmaxlim, yminlim, ymaxlim)
def makeDataFile(gaussians, limits, res, outputFile):
'''makes a data file of x,y coordinates to be plotted'''
xres = res[0]
yres = res[1]
# make arrays of x and y values
x = numpy.linspace(limits[0], limits[1], xres)
y = numpy.linspace(limits[2], limits[3], yres)
# initialize grid of z values
z = numpy.zeros((xres, yres))
# compute z value at each x, y point
for i in range(len(x)):
for j in range(len(y)):
for gaussian in gaussians:
z[i][j] += gaussian.value(x[i], y[j])
# write out the matrix
with open(outputFile, 'w') as f:
for i in range(len(x)):
for j in range(len(y)):
f.write('%f %f %f\n' % (x[i], y[j], z[i][j]))
f.write('\n')
def makePlotFile(limits, outputFile):
'''makes a plot file for gnuplot'''
with open('plot.plt', 'w') as f:
f.write("#!/usr/bin/env gnuplot\n")
f.write("set terminal png font 'Courier'\n")
f.write("set output 'gaussians.png'\n")
f.write("set xr [%f:%f]\n" % (limits[0], limits[1]))
f.write("set yr [%f:%f]\n" % (limits[2], limits[3]))
f.write("set pm3d map\n")
f.write("plot '%s' with image\n" % outputFile)
# make plot file executable
os.system('chmod 755 plot.plt')
# plot
os.system('./plot.plt')
# name of input file
inputFile = 'data.dat'
# name of output (processed data)
outputFile = 'gaussians.dat'
# number of x and y points in plot
res = (100, 100)
# deviations from the mean by which to define Gaussian limits
devs = 3
# read in the gaussians from the data file
print 'reading in data...'
gaussians = readGaussians(inputFile)
# find the plot limits
limits = getPlotLimits(gaussians, devs)
# make the gaussian data file
print 'computing data for plotting...'
makeDataFile(gaussians, limits, res, outputFile)
# make the plot file
print 'plotting...'
makePlotFile(limits, outputFile)
This script produces the following output when run on your example data.

Resources