Matplotlib overlapping annotations / text Matplotlib overlapping annotations / text python python

Matplotlib overlapping annotations / text


I just wanted to post here another solution, a small library I wrote to implement this kind of things: https://github.com/Phlya/adjustTextAn example of the process can be seen here:enter image description here

Here is the example image:

import matplotlib.pyplot as pltfrom adjustText import adjust_textimport numpy as nptogether = [(0, 1.0, 0.4), (25, 1.0127692669427917, 0.41), (50, 1.016404709797609, 0.41), (75, 1.1043426359673716, 0.42), (100, 1.1610446924342996, 0.44), (125, 1.1685687930691457, 0.43), (150, 1.3486407784550272, 0.45), (250, 1.4013999168008104, 0.45)]together.sort()text = [x for (x,y,z) in together]eucs = [y for (x,y,z) in together]covers = [z for (x,y,z) in together]p1 = plt.plot(eucs,covers,color="black", alpha=0.5)texts = []for x, y, s in zip(eucs, covers, text):    texts.append(plt.text(x, y, s))plt.xlabel("Proportional Euclidean Distance")plt.ylabel("Percentage Timewindows Attended")plt.title("Test plot")adjust_text(texts, only_move={'points':'y', 'texts':'y'}, arrowprops=dict(arrowstyle="->", color='r', lw=0.5))plt.show()

enter image description here

If you want a perfect figure, you can fiddle around a little. First, let's also make text repel the lines - for that we just create lots of virtual points along them using scipy.interpolate.interp1d.

We want to avoid moving the labels along the x-axis, because, well, why not do it for illustrative purposes. For that we use the parameter only_move={'points':'y', 'text':'y'}. If we want to move them along x axis only in the case that they are overlapping with text, use move_only={'points':'y', 'text':'xy'}. Also in the beginning the function chooses optimal alignment of texts relative to their original points, so we only want that to happen along the y axis too, hence autoalign='y'. We also reduce the repelling force from points to avoid text flying too far away due to our artificial avoidance of lines. All together:

from scipy import interpolatep1 = plt.plot(eucs,covers,color="black", alpha=0.5)texts = []for x, y, s in zip(eucs, covers, text):    texts.append(plt.text(x, y, s))f = interpolate.interp1d(eucs, covers)x = np.arange(min(eucs), max(eucs), 0.0005)y = f(x)        plt.xlabel("Proportional Euclidean Distance")plt.ylabel("Percentage Timewindows Attended")plt.title("Test plot")adjust_text(texts, x=x, y=y, autoalign='y',            only_move={'points':'y', 'text':'y'}, force_points=0.15,            arrowprops=dict(arrowstyle="->", color='r', lw=0.5))plt.show()

enter image description here


Easy solution here: (for jupyter notebooks)

%matplotlib notebookimport mplcursorsplt.plot.scatter(y=YOUR_Y_DATA, x =YOUR_X_DATA)mplcursors.cursor(multiple = True).connect(    "add", lambda sel: sel.annotation.set_text(          YOUR_ANOTATION_LIST[sel.target.index]))

Right click on a dot to show its anotation.

Left click on an anotation to close it.

Right click and drag on an anotation to move it.

enter image description here


With a lot of fiddling, I figured it out. Again credit for the original solution goes to the answer for Matplotlib overlapping annotations .

I don't however know how to find the exact width and height of the text. If someone knows, please post an improvement (or add a comment with the method).

import sysimport matplotlibimport matplotlib.pyplot as pltimport numpy as npdef get_text_positions(text, x_data, y_data, txt_width, txt_height):    a = zip(y_data, x_data)    text_positions = list(y_data)    for index, (y, x) in enumerate(a):        local_text_positions = [i for i in a if i[0] > (y - txt_height)                             and (abs(i[1] - x) < txt_width * 2) and i != (y,x)]        if local_text_positions:            sorted_ltp = sorted(local_text_positions)            if abs(sorted_ltp[0][0] - y) < txt_height: #True == collision                differ = np.diff(sorted_ltp, axis=0)                a[index] = (sorted_ltp[-1][0] + txt_height, a[index][1])                text_positions[index] = sorted_ltp[-1][0] + txt_height*1.01                for k, (j, m) in enumerate(differ):                    #j is the vertical distance between words                    if j > txt_height * 2: #if True then room to fit a word in                        a[index] = (sorted_ltp[k][0] + txt_height, a[index][1])                        text_positions[index] = sorted_ltp[k][0] + txt_height                        break    return text_positionsdef text_plotter(text, x_data, y_data, text_positions, txt_width,txt_height):    for z,x,y,t in zip(text, x_data, y_data, text_positions):        plt.annotate(str(z), xy=(x-txt_width/2, t), size=12)        if y != t:            plt.arrow(x, t,0,y-t, color='red',alpha=0.3, width=txt_width*0.1,                 head_width=txt_width, head_length=txt_height*0.5,                 zorder=0,length_includes_head=True)# start new plotplt.clf()plt.xlabel("Proportional Euclidean Distance")plt.ylabel("Percentage Timewindows Attended")plt.title("Test plot")together = [(0, 1.0, 0.4), (25, 1.0127692669427917, 0.41), (50, 1.016404709797609, 0.41), (75, 1.1043426359673716, 0.42), (100, 1.1610446924342996, 0.44), (125, 1.1685687930691457, 0.43), (150, 1.3486407784550272, 0.45), (250, 1.4013999168008104, 0.45)]together.sort()text = [x for (x,y,z) in together]eucs = [y for (x,y,z) in together]covers = [z for (x,y,z) in together]p1 = plt.plot(eucs,covers,color="black", alpha=0.5)txt_height = 0.0037*(plt.ylim()[1] - plt.ylim()[0])txt_width = 0.018*(plt.xlim()[1] - plt.xlim()[0])text_positions = get_text_positions(text, eucs, covers, txt_width, txt_height)text_plotter(text, eucs, covers, text_positions, txt_width, txt_height)plt.savefig("test.png")plt.show()

Creates http://i.stack.imgur.com/xiTeU.pngenter image description here

The more complicated graph is now http://i.stack.imgur.com/KJeYW.png, still a bit iffy but much better!enter image description here