I would like to plot numerous lines and caption them with an annotation. However, due to the number of graphs and lines, I need to automatically position my annotations without overlapping other annotations or lines. I have tested the adjustText module without success. The textalloc module seems more promising. I use the following code :
import textalloc as ta
import numpy as np
import matplotlib.pyplot as plt
# Generate data
x = np.linspace(0, 10, 500)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x + np.pi/4)
# Create plot
fig, ax = plt.subplots()
ax.plot(x, y1, label='sin(x)', color='blue')
ax.plot(x, y2, label='cos(x)', color='green')
ax.plot(x, y3, label='sin(x + π/4)', color='red')
# Set an arbitrary label position
idx_label = np.abs(x - 9).argmin()
# Define labels and their corresponding y-values
curves = [y1, y2, y3]
labels = ['sin(x)', 'cos(x)', 'sin(x + π/4)']
colors = ['blue', 'green', 'red']
# Set x positions (same for all) and extract corresponding y values
x_pos = [x[idx_label]] * len(curves)
y_pos = [y[idx_label] for y in curves]
# Allocate text labels using textalloc to avoid overlap
texts = ta.allocate(
ax,
x_pos, y_pos,
labels,
textsize=10,
x_scatter=x+x+x,
y_scatter=y1+y3+y2, # background data to help avoid collisions
linewidth=0.5,
)
# Final plot adjustments
ax.set_title("Auto-placed Labels Without Overlap")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
plt.tight_layout()
plt.show()
I get the following result :

However, there are overlaps between annotations and the other lines. In addition, in the code I've set the x-coordinate of the point that's annotated, but I'd rather this position be decided automatically according to the place on the graph. How can I do that ?
From the doc, you have to pass the full lines as x_lines and y_lines inside ta.allocate for the textalloc to know which lines to avoid. You can also define the x_pos and y_pos by looking into the x and y lists at a point near the end (I chose x[-40], y[-40])
import textalloc as ta
import numpy as np
import matplotlib.pyplot as plt
# Generate data
x = np.linspace(0, 10, 500)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x + np.pi/4)
# Create plot
fig, ax = plt.subplots()
ax.plot(x, y1, label='sin(x)', color='blue')
ax.plot(x, y2, label='cos(x)', color='green')
ax.plot(x, y3, label='sin(x + π/4)', color='red')
# Define x and y positions for the labels
x_pos = np.array([x[-40], x[-40], x[-40]])
y_pos = np.array([y1[-40], y2[-40], y3[-40]])
labels = ['sin(x)', 'cos(x)', 'sin(x + π/4)']
# Apply text allocation
ta.allocate(
ax,
x_pos, y_pos,
labels,
x_lines=[x, x, x], # Pass the the full lines
y_lines=[y1, y2, y3],# Pass the the full lines
textsize=10,
draw_lines=True,
linewidth=0.5
)
# Final plot adjustments
ax.set_title("Auto-placed Labels Without Overlap")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
plt.tight_layout()
plt.show()
And here is the result:

you're fixing all label x positions near x=9, which constrains the layout and makes it harder to avoid overlaps. Instead, pick multiple candidate positions along each line and let textalloc choose the best.
Updated code:
import textalloc as ta
import numpy as np
import matplotlib.pyplot as plt
# Generate data
x = np.linspace(0, 10, 500)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x + np.pi / 4)
# Create plot
fig, ax = plt.subplots() # Increased figure size for more room
line1, = ax.plot(x, y1, label='sin(x)', color='blue')
line2, = ax.plot(x, y2, label='cos(x)', color='green')
line3, = ax.plot(x, y3, label='sin(x + π/4)', color='red')
# Define curves, labels, and colors
curves = [y1, y2, y3]
labels = ['sin(x)', 'cos(x)', 'sin(x + π/4)']
colors = [line1.get_color(), line2.get_color(), line3.get_color()]
# Automatic X-coordinate Strategy: Find X-coordinate where lines are most separated.
x_pos = []
y_pos = []
# Iterate through each curve to find a good initial point
for i, current_curve in enumerate(curves):
# Calculate vertical distances to all other curves at each x-point
distances_to_others = np.zeros_like(x, dtype=float)
for j, other_curve in enumerate(curves):
if i != j: # Don't compare a curve to itself
distances_to_others += np.abs(current_curve - other_curve)
# let's only consider a central portion of the x-range (e.g., from 10% to 90%).
start_percent = 0.1
end_percent = 0.9
start_idx = int(len(x) * start_percent)
end_idx = int(len(x) * end_percent)
# Find the index with the maximum total distance to other curves within the allowed range.
if end_idx > start_idx:
best_idx_in_slice = np.argmax(distances_to_others[start_idx:end_idx])
best_idx_for_this_curve = start_idx + best_idx_in_slice
else:
best_idx_for_this_curve = len(x) // 2
print(f"Warning: X-range too small for optimal placement for curve {labels[i]}. Using midpoint.")
x_pos.append(x[best_idx_for_this_curve])
y_pos.append(current_curve[best_idx_for_this_curve])
# Provide line data to textalloc for collision detection
x_lines_data = [x, x, x]
y_lines_data = [y1, y2, y3]
# Allocate text labels using textalloc
texts = ta.allocate(
ax,
x_pos, y_pos,
labels,
textsize=10,
x_lines=x_lines_data, # Essential for avoiding overlaps with lines
y_lines=y_lines_data, # Essential for avoiding overlaps with lines
linewidth=0.5,
draw_lines=True,
avoid_label_lines_overlap=True,
margin=0.02,
min_distance=0.015,
)
# Final plot adjustments
ax.set_title(("Auto-placed Labels Without Overlap"))
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
plt.tight_layout()
plt.show()
Output:

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With