How do I put a simple caption below my x axis in Matplotib? - python-3.x

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:

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

Annotate Percentage of Group within a Seaborn CountPlot

The below code gets the percentage of all collisions. However, I want to get the percentage within a group. E.G. Mid-Block (not related to intersection) has 2 labels, a 1(red) or a 2(green/blue). Currently, the percentages next to those bars are percentages of the whole (bar count / all collisions), but I need to display the percentage within just one y-axis label. E.G. for Mid-block (not related to intersection), bar count / all collisions within mid-block (not related to intersection). I do not know how to do this, so if someone could point me in the right direction or give me some code that I could study to understand, I'd be very grateful.
Thank you so much for your time.
plt.style.use('ggplot')
plt.figure(figsize = (20, 15))
ax = sb.countplot(y = "JUNCTIONTYPE", hue = "SEVERITYCODE", data = dfm)
plt.title('Number of Persons vs. Number of Collisions by Severity', fontsize = 30)
plt.xlabel('Number of Collisions', fontsize = 24)
plt.ylabel('Number of Persons', fontsize = 24)
plt.tick_params(labelsize=18);
plt.legend(fontsize = 18, title = "Severity", loc = 'lower right')
plt.text(5, 6, "Figure 8: Number of persons plotted against the number of collisions grouped by severity", fontsize = 16)
# labels = [item.get_text() for item in ax.get_yticklabels()]
# labels[0] = 'No'
# labels[1] = 'Yes'
# ax.set_yticklabels(labels)
for p in ax.patches:
width = p.get_width()
height = p.get_height()
x, y = p.get_xy()
ax.annotate(int(width),
((x + width), y),
xytext = (30, -25),
fontsize = 18,
color = '#000000',
textcoords = 'offset points',
ha = 'right',
va = 'center')
for p in ax.patches:
width = p.get_width()
height = p.get_height()
x, y = p.get_xy()
totals = []
for i in ax.patches:
totals.append(i.get_width())
total = sum(totals)
ax.text(width + 0.3, y + 0.38,
str(
round((width/total) * 100, 2))
+ '%',
fontsize=18)
You could pre-calculate the per-group percentage points and use the order in which seaborn / matplotlib draws the bars to reference them.
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
titanic = sns.load_dataset('titanic')
# prepare the dataset
df = (titanic
.groupby(["embark_town", "survived"])
.size()
.reset_index()
.replace({"survived": {0:"no", 1:"yes"}})
.rename(columns={0:"count"}))
# calculate survival % per town of embarkation
df["percent"] = (df
.groupby("embark_town")
.apply(lambda x: x["count"] / x["count"].sum()).values)
# sort the dataframe to match the drawing order
df.sort_values(by=["survived", "embark_town"], inplace=True)
# visualisation
plt.style.use('ggplot')
fig = sns.catplot(
x="count", y="embark_town", hue="survived",
kind="bar", data=df, height=4, aspect=2)
for i, bar in enumerate(fig.ax.patches):
height = bar.get_height()
fig.ax.annotate(
# reference the pre-calculated row in the dataframe
f"{df.iloc[i, 3] :.0%}",
xycoords="data",
xytext=(20, -15),
textcoords="offset points",
xy=(bar.get_width(), bar.get_y()),
ha='center', va='center')
# make space for annonations
plt.margins(x=0.2)
plt.show()

How to combine multiple spectrogram subplots to produce single plot?

I am visualizing four classes of the spectrogram. For a single class, My spectrogram code looks like this
Considering this as one image.
And the code to produce this, is
def spec(filename):
time_period = 0.5 # FFT time period (in seconds). Can comfortably process time frames from 0.05 seconds - 10 seconds
# ==============================================
fs_rate, signal_original = wavfile.read(filename)
total_time = int(np.floor(len(signal_original)/fs_rate))
sample_range = np.arange(0,total_time,time_period)
total_samples = len(sample_range)
print ("Frequency sampling", fs_rate)
print ("total time: ", total_time)
print ("sample time period: ", time_period)
print ("total samples: ", total_samples)
output_array = []
for i in sample_range:
# print ("Processing: %d / %d (%d%%)" % (i/time_period + 1, total_samples, (i/time_period + 1)*100/total_samples))
sample_start = int(i*fs_rate)
sample_end = int((i+time_period)*fs_rate)
signal = signal_original[sample_start:sample_end]
l_audio = len(signal.shape)
#print ("Channels", l_audio)
if l_audio == 2:
signal = signal.sum(axis=1) / 2
N = signal.shape[0]
#print ("Complete Samplings N", N)
secs = N / float(fs_rate)
# print ("secs", secs)
Ts = 1.0/fs_rate # sampling interval in time
#print ("Timestep between samples Ts", Ts)
t = scipy.arange(0, secs, Ts) # time vector as scipy arange field / numpy.ndarray
FFT = abs(scipy.fft(signal))
FFT_side = FFT[range(int(N/2))] # one side FFT range
freqs = scipy.fftpack.fftfreq(signal.size, t[1]-t[0])
fft_freqs = np.array(freqs)
freqs_side = freqs[range(int(N/2))] # one side frequency range
fft_freqs_side = np.array(freqs_side)
# Reduce to 0-5000 Hz
bucket_size = 5
buckets = 16
FFT_side = FFT_side[0:bucket_size*buckets]
fft_freqs_side = fft_freqs_side[0:bucket_size*buckets]
# Combine frequencies into buckets
FFT_side = np.array([int(sum(FFT_side[current: current+bucket_size])) for current in range(0, len(FFT_side), bucket_size)])
fft_freqs_side = np.array([int(sum(fft_freqs_side[current: current+bucket_size])) for current in range(0, len(fft_freqs_side), bucket_size)])
# FFT_side: Normalize (0-1)
max_value = max(FFT_side)
if (max_value != 0):
FFT_side_norm = FFT_side / max_value
# Append to output array
output_array.append(FFT_side_norm)
# ============================================
# Plotting
plt.figure(figsize=(4,7))
plt.subplot(411)
plt.subplots_adjust(hspace = 0.5)
plt.plot(t, signal, "g") # plotting the signal
plt.xlabel('Time')
plt.ylabel('Amplitude')
plt.subplot(412)
diff = np.diff(fft_freqs_side)
widths = np.hstack([diff, diff[-1]])
plt.bar(fft_freqs_side, abs(FFT_side_norm), width=widths) # plotting the positive fft spectrum
plt.xticks(fft_freqs_side, fft_freqs_side, rotation='vertical')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Count single-sided')
FFT_side_norm_line = FFT_side_norm.copy()
FFT_side_norm_line.resize( (1,buckets) )
plt.subplot(413)
plt.imshow(FFT_side_norm_line)
plt.xlabel('Image Representation 1D')
plt.show()
print("\n\n\n\n\n\n")
How can I combine four images plot like this, and make a single output image. Something like this
I'd suggest using fig.subfigures and plt.subplot_mosaic.
The plot above was obtained using this simple script:
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (8, 10), layout='constrained')
# next two lines make the trick
sfigs = fig.subfigures(2,2)
mosaics = [f.subplot_mosaic('t;t;t;f;f;f;i;.') for f in sfigs.flat]
# next, "how to" reference the subplots in subfigures
mosaics[0]['t'].plot(range(5), color='b')
mosaics[1]['t'].plot(range(5), color='k')
mosaics[2]['t'].plot(range(5), color='r')
mosaics[3]['t'].plot(range(5), color='g')
mosaics[0]['f'].plot(range(3), color='b')
mosaics[1]['f'].plot(range(3), color='k')
mosaics[2]['f'].plot(range(3), color='r')
mosaics[3]['f'].plot(range(3), color='g')
mosaics[0]['i'].imshow([range(10)]*2)
plt.show()
You can do it this way:
fig, axs = plt.subplots(2, 2)
axs[0, 0].plot(x, y)
axs[0, 0].set_title('Axis [0, 0]')
axs[0, 1].plot(x, y, 'tab:orange')
axs[0, 1].set_title('Axis [0, 1]')
axs[1, 0].plot(x, -y, 'tab:green')
axs[1, 0].set_title('Axis [1, 0]')
axs[1, 1].plot(x, -y, 'tab:red')
axs[1, 1].set_title('Axis [1, 1]')
for ax in axs.flat:
ax.set(xlabel='x-label', ylabel='y-label')
# Hide x labels and tick labels for top plots and y ticks for right plots.
for ax in axs.flat:
ax.label_outer()
The result will be like this:
Taken from https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subplots_demo.html

mplcursors: show and highlight coordinates of nearby local extreme

I have code that shows the label for each point in a matplotlib scatterplot using mplcursors, similar to this example. I want to know how to, form a list of values, make a certain point stand out, as in if I have a graph of points y=-x^2. When I go near the peak, it shouldn't show 0.001, but 0 instead, without the trouble needing to find the exact mouse placement of the top. I can't solve for each point in the graph, as I don't have a specific function.
Supposing the points in the scatter plot are ordered, we can investigate whether an extreme in a nearby window is also an extreme in a somewhat larger window. If, so we can report that extreme with its x and y coordinates.
The code below only shows the annotation when we're close to a local maximum or minimum. It also temporarily shows a horizontal and vertical line to indicate the exact spot. The code can be a starting point for many variations.
import matplotlib.pyplot as plt
import mplcursors
import numpy as np
near_window = 10 # the width of the nearby window
far_window = 20 # the width of the far window
def show_annotation(sel):
ind = sel.target.index
near_start_index = max(0, ind - near_window)
y_near = y[near_start_index: min(N, ind + near_window)]
y_far = y[max(0, ind - far_window): min(N, ind + far_window)]
near_max = y_near.max()
far_max = y_far.max()
annotation_str = ''
if near_max == far_max:
near_argmax = y_near.argmax()
annotation_str = f'local max:\nx:{x[near_start_index + near_argmax]:.3f}\ny:{near_max:.3f}'
maxline = plt.axhline(near_max, color='crimson', ls=':')
maxline_x = plt.axvline(x[near_start_index+near_argmax], color='grey', ls=':')
sel.extras.append(maxline)
sel.extras.append(maxline_x)
else:
near_min = y_near.min()
far_min = y_far.min()
if near_min == far_min:
near_argmin = y_near.argmin()
annotation_str = f'local min:\nx:{x[near_start_index+near_argmin]:.3f}\ny:{near_min:.3f}'
minline = plt.axhline(near_min, color='limegreen', ls=':')
minline_x = plt.axvline(x[near_start_index + near_argmin], color='grey', ls=':')
sel.extras.append(minline)
sel.extras.append(minline_x)
if len(annotation_str) > 0:
sel.annotation.set_text(annotation_str)
else:
sel.annotation.set_visible(False) # hide the annotation
# sel.annotation.set_text(f'x:{sel.target[0]:.3f}\n y:{sel.target[1]:.3f}')
N = 500
x = np.linspace(0, 100, 500)
y = np.cumsum(np.random.normal(0, 0.1, N))
box = np.ones(20) / 20
y = np.convolve(y, box, mode='same')
scat = plt.scatter(x, y, s=1)
cursor = mplcursors.cursor(scat, hover=True)
cursor.connect('add', show_annotation)
plt.show()

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