46 votes

Annotations de chevauchement Matplotlib

Je veux annoter les barres dans un graphe avec un peu de texte, mais si les barres sont rapprochées et sont comparables hauteur, les annotations sont au-dessus de l'ea. d'autres, et donc difficile à lire (les coordonnées pour les annotations ont été prises à partir de la barre de position et la hauteur).

Est-il un moyen de modifier l'un d'eux s'il s'agit d'une collision?

Edit: Les barres sont très minces et très proche parfois, afin d'aligner verticalement ne résout pas le problème...

Une image peut clarifier les choses: bar pattern

54voto

fraxel Points 13844

J'ai écrit une solution rapide, qui vérifie chaque annotation position contre le défaut de paiement boîtes englobantes pour tous les autres annotations. Si il y a une collision, elle change de position pour la prochaine collision place de libre. Il met aussi à nice flèches.

Pour une assez exemple extrême, il va produire ce (aucun des numéros de chevauchement): enter image description here

Au lieu de cela: enter image description here

Voici le code:

import numpy as np
import matplotlib.pyplot as plt
from numpy.random import *

def get_text_positions(x_data, y_data, txt_width, txt_height):
    a = zip(y_data, x_data)
    text_positions = y_data.copy()
    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
                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_positions

def text_plotter(x_data, y_data, text_positions, axis,txt_width,txt_height):
    for x,y,t in zip(x_data, y_data, text_positions):
        axis.text(x - txt_width, 1.01*t, '%d'%int(y),rotation=0, color='blue')
        if y != t:
            axis.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)

Voici le code de la production de ces parcelles, montrant l'utilisation:

#random test data:
x_data = random_sample(100)
y_data = random_integers(10,50,(100))

#GOOD PLOT:
fig2 = plt.figure()
ax2 = fig2.add_subplot(111)
ax2.bar(x_data, y_data,width=0.00001)
#set the bbox for the text. Increase txt_width for wider text.
txt_height = 0.04*(plt.ylim()[1] - plt.ylim()[0])
txt_width = 0.02*(plt.xlim()[1] - plt.xlim()[0])
#Get the corrected text positions, then write the text.
text_positions = get_text_positions(x_data, y_data, txt_width, txt_height)
text_plotter(x_data, y_data, text_positions, ax2, txt_width, txt_height)

plt.ylim(0,max(text_positions)+2*txt_height)
plt.xlim(-0.1,1.1)

#BAD PLOT:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(x_data, y_data, width=0.0001)
#write the text:
for x,y in zip(x_data, y_data):
    ax.text(x - txt_width, 1.01*y, '%d'%int(y),rotation=0)
plt.ylim(0,max(text_positions)+2*txt_height)
plt.xlim(-0.1,1.1)

plt.show()

9voto

Yann Points 6909

Une option est de faire pivoter le texte/l'annotation, qui est défini par l' rotation mot-clé ou de la propriété. Dans l'exemple suivant, je tourne le texte de 90 degrés afin de garantir qu'il n'y entrent en collision avec les voisins de texte. J'ai aussi mis l' va (ou verticalalignment) mot-clé, de sorte que le texte est présenté au-dessus de la barre (au-dessus du point que j'utilise pour définir le texte):

import matplotlib.pyplot as plt

data = [10, 8, 8, 5]

fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(range(4),data)
ax.set_ylim(0,12)
# extra .4 is because it's half the default width (.8):
ax.text(1.4,8,"2nd bar",rotation=90,va='bottom')
ax.text(2.4,8,"3nd bar",rotation=90,va='bottom')

plt.show()

Le résultat est la figure suivante:

enter image description here

Déterminer par programmation si il y a des collisions entre les diverses annotations est un plus délicat processus. Cela pourrait être en vaut une autre question: Matplotlib dimensions du texte.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X