Matplotlib - Change draw order and moving annotation - python-3.x

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

Related

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:

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.

How to plot 3D voxels with given coordinates on a sphere using matplotlib

I'm currently trying to make a 3D voxels plot with know coordinates on a sphere. The x, y and z coordinates are lists filtered from a CSV. Additionally I have another list with same length as x, y and z containing the color of the voxel. With these list I want to create a 3D voxel sphere.
This I already accomplished using a 3D scatter plot (matplotlib) but the result is not very clear:
x = []
y = []
z = []
c = []
red = (1, 0, 0, 1)
green = (0, 1, 0, 1)
blue = (0, 0, 1, 1)
black = (0, 0, 0, 1)
for i in range(len(result)):
x.append(20 * math.sin(math.radians(90 - result[i][1])) * math.cos(math.radians(result[i][0])))
y.append(20 * math.sin(math.radians(90 - result[i][1])) * math.sin(math.radians(result[i][0])))
z.append(20 * math.cos(math.radians(90 - result[i][1])))
if result[i][2] == 1000:
c.append(black)
elif result[i][2] > 500:
c.append(red)
elif 200 < result[i][2] <= 500:
c.append(blue)
else:
c.append(green)
fig = pyplot.figure()
ax = Axes3D(fig)
ax.grid(True)
ax.scatter(x, y, z, c=c, s=500)
pyplot.show()
3D-Scatter

Using Python 3 matplotlib button - rescale y axis

My spectra data is named "tip16.spe", which has 244 frames. This file is imported as "dset.data[...,0]". I add "matplotlib buttons" function to the Python code. "yset_fun" is a function to call data of the frame number
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.2)
t = xset
s = yset_fun(0)
l, = plt.plot(t, s, lw=2.5)
class Index(object):
ind = 0
def next(self, event):
self.ind += 1
i = self.ind % len(dset.data[...,0])
ydata = yset_fun(i)
l.set_ydata(ydata)
plt.draw()
def prev(self, event):
self.ind -= 1
i = self.ind % len(dset.data[...,0])
ydata = yset_fun(i)
l.set_ydata(ydata)
plt.draw()
callback = Index()
axprev = plt.axes([0.7, 0.05, 0.1, 0.075])
axnext = plt.axes([0.81, 0.05, 0.1, 0.075])
bnext = Button(axnext, 'Next')
bnext.on_clicked(callback.next)
bprev = Button(axprev, 'Previous')
bprev.on_clicked(callback.prev)
Then it draws initial data of a fixed y range. But it is bad for data of higher intensity. I want to change it to dynamic range, which means flexible y range can show the whole data.
Could any expert of matplotlib help me to fix this issue?
Thanks in advance

Updating pyplot.vlines in interactive plot

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

Resources