Search code examples
pythonmatplotlibtextcharts

overlapping in python plt.text, package adjust_text not working, how to fix it?


I have two pandas dataframe objects named 'freq' and 'freq1000', each being a vector of 5 numbers. I want to draw them on the same graph with plt.text on the graph to show the y-values. 'freq' and 'freq1000' are constructed from 'pr' and 'pr1000', both of which are generated by extremely lengthy procedures. To avoid overlapping of plt.text, I followed the link How to fix overlapping annotations / text and installed the package adjust_text. However it did not work as I want, and the graph I got still have overlapping for the figures on the graph, see the picture below:

enter image description here

My related imports and codes are included below

import pandas as pd
import matplotlib.pyplot as plt
from adjustText import adjust_text



freq = pd.DataFrame(np.average(pr, axis=0), index=pr.columns) 
freq1000 = pd.DataFrame(np.average(pr1000, axis=0), index=pr1000.columns)    
plt.plot(freq, label='n=500')
plt.plot(freq1000, label='n=1000')
plt.legend(loc='best')



plt.xlabel('x')
plt.ylabel('y')
plt.title('curve high vs low,gtype{}'.format(type_))
# plt.close()
texts=[]
for x, y in zip(freq.index, freq.values):
    texts.append(plt.text(x, y,'%.3f' % y))
adjust_text(texts, only_move={'points':'y', 'texts':'xy'}, arrowprops=dict(arrowstyle="->", color='r', lw=0.5))
texts=[]
for x, y in zip(freq1000.index, freq1000.values):
    texts.append(plt.text(x, y,'%.3f' % y))
adjust_text(texts, only_move={'points':'y', 'texts':'y'}, arrowprops=dict(arrowstyle="->", color='r', lw=0.5))

How should I fix the codes to solve the overlapping problem? Thanks!


Solution

  • This is an attempt at something with less overlap, but with some tuning required if things move around a lot. ax.annotate does most of the work, but there are some padding parameters for extra adjustment. The main idea is to shift the blue labels left, and the orange ones right.

    One of the orange labels overlaps with the line. You can manually force it off if required for a one-off plot, but I haven't done that in order to keep the answer more generalisable and robust to new cases.

    enter image description here

    from matplotlib import pyplot as plt
    
    pr_x = [-1, -0.5, 0, 0.5, 1]
    pr_y = [1, 0.683, 0.067, 0.5, 1]
    color_pr = 'tab:blue'
    
    pr1000_x = [-1, -0.5, 0, 0.5, 1]
    pr1000_y = [1, 0.983, 0.05, 0.917, 1]
    color_pr1000 = 'tab:orange'
    
    f, ax = plt.subplots(figsize=(6, 3))
    ax.plot(pr_x, pr_y, color=color_pr, marker='o', label='n=500')
    ax.plot(pr1000_x, pr1000_y, color=color_pr1000, marker='o', label='n=1000')
    
    ax.set(xlabel='x', ylabel='y', title='curve high vs low, gtype2')
    ax.legend()
    
    for x, y in zip(pr_x, pr_y):
        ax.annotate(
            f'{y:.3f}', (x, y),
            xytext=(-0.6, 0), textcoords='offset fontsize', #shift blue pts left
            horizontalalignment='right', verticalalignment='center',
            weight='bold', fontsize=8, color=color_pr,
        )
        
    for x, y in zip(pr1000_x, pr1000_y):
        ax.annotate(
            f'{y:.3f}', (x, y),
            xytext=(0.6, 0.2), textcoords='offset fontsize', #shift orange pts up, right
            horizontalalignment='left', verticalalignment='bottom',
            weight='bold', fontsize=8, color=color_pr1000,
            # rotation=20
        )
        
    ax.set_xlim(ax.get_xlim()[0] * 1.15, ax.get_xlim()[1]) #make x axis wider
    ax.spines[['right', 'top']].set_visible(False)
    

    This is an alternative arrangement that has less potential for overlap. There's a visual guide for each plot, but the numbers are split across two plots.

    I think seaborn's FacetGrid could do something similar and with less code if you were interested in exploring this sort of arrangement further.

    enter image description here

    f, axs = plt.subplots(ncols=2, nrows=1, figsize=(7, 3), sharey=True, layout='tight')
    
    pr_data = (pr_x, pr_y, 'n=500')
    pr1000_data = (pr1000_x, pr1000_y, 'n=1000')
    
    for ax in axs:
        fg_data = pr_data if ax==axs[0] else pr1000_data
        bg_data  = pr_data if ax==axs[1] else pr1000_data
        
        #Foreground data
        x, y, label = fg_data
        ax.plot(x, y, 'tab:purple', marker='o', markersize=7, linewidth=3, label=label)
        
        for x_i, y_i in zip(x, y):
            ax.annotate(
                f'{y_i:.3f}', (x_i, y_i), xytext=(1, 0.6), textcoords='offset fontsize',
                horizontalalignment='left', verticalalignment='top',
                color='midnightblue', weight='bold', size=8, rotation=0,
            )
        
        #Background data
        x, y, label = bg_data
        ax.plot(x, y, 'black', marker='o', markersize=6,
                linewidth=4.5, linestyle='-', alpha=0.12,
                label=label, zorder=0)
        
        ax.set_xlabel('x')
        ax.spines[['right', 'top']].set_visible(False)
        ax.set_title(label, color='rebeccapurple', weight='bold')
        # ax.legend()
    
        axs[0].set_ylabel('y')