For a single Maxima:
Set the peak_distances arg to a large number.
annot_peaks(x,y, ax, peak_distances=10000, y_position_modifier=14)

More than 1 Maxima / Annotate Peaks:
Set the peak_distances arg to a smaller number.
annot_peaks(x,y, ax, peak_distances=30, y_position_modifier=14)

Function for Automatic Annotation of Peaks:
For more than one maxima the we can use very similar code to @ImportanceOfBeingErnest's annot_max(x,y) function; with a couple of important differences and a for-loop for each peak's annotation:
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import find_peaks
from sklearn.preprocessing import minmax_scale
def annot_peaks(x:np.array,y:np.array, ax=None, peak_distance=30, y_position_modifier=1):
yindices, _ = find_peaks(y, distance=peak_distance)
xmax = x[yindices]
ymax = y[yindices]
ymodifier = {k:v for k,v in zip(y, y_position_modifier-minmax_scale(y, feature_range=(0,y_position_modifier)))}
if not ax:
ax=plt.gca()
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
arrowprops=dict(arrowstyle="->", color="k",
connectionstyle="arc3,rad=0")
kw = dict(xycoords='data',textcoords="data",
arrowprops=arrowprops, bbox=bbox_props, ha="right", va="top")
for xmx, ymx in zip(xmax,ymax):
text= "x={:.1f},\ny={:.1f}".format(xmx, ymx)
ax.annotate(text, xy=(xmx, ymx), xytext=(xmx*1.1, ymx*(1.5+ymodifier[ymx])), **kw)
x = np.linspace(-2,8, num=301)
y = np.sinc((x-2.21)*3)
fig, ax = plt.subplots()
ax.plot(x,y)
annot_peaks(x,y, ax, peak_distance=30, y_position_modifier=14)
ax.set_ylim(-0.3,1.5)
plt.show()
Find peaks:
Above uses the scipy.signal.find_peaks() function with a distance arg to determine the horizontal distance between each peak (i.e. smaller value gives more peaks):
from scipy.signal import find_peaks
yindices, _ = find_peaks(y, distance=10)
xmax = x[yindices]
ymax = y[yindices]
Annotation positioning:
Many peaks means careful annotation positioning. To specify an annotation's y-position, a minmax scaled value of y is inverted using a q argument. (This is later used as a text position modifier, i.e. ypos=ymx+(1.1*modifier)). The output is stored in a dictionary-lookup for convenient lookup.
from sklearn.preprocessing import minmax_scale
q = 1
ymodifier = {k:v for k,v in zip(y, q-minmax_scale(y, feature_range=(0,q)))}
ymodifier[ y[0] ]
- To manage different data magnitudes (e.g.
y=range(0,2) or y=range(0,1000)), we can modify an arg q. For the data above q=14, or the nicer nomenclature: y_position_modifier=14.