Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Annotate lines without overlapping

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 :

enter image description here

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 ?

like image 542
user28754841 Avatar asked Aug 11 '26 10:08

user28754841


2 Answers

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:

Resulting plot with limited overlap

like image 165
Nevpzo Avatar answered Aug 12 '26 23:08

Nevpzo


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:

enter image description here

like image 26
amrita yadav Avatar answered Aug 12 '26 22:08

amrita yadav



Donate For Us

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