2012-01-13 18 views
35

Voglio annotare le barre di un grafico con del testo ma se le barre sono ravvicinate e hanno un'altezza comparabile, le annotazioni sono sopra l'ea. altro e quindi difficile da leggere (le coordinate per le annotazioni sono state prese dalla posizione e dall'altezza della barra).Annotazioni sovrapposte Matlotlib

C'è un modo per spostarne uno in caso di collisione?

Edit: Le barre sono molto sottili e molto vicino a volte in modo solo l'allineamento verticale non risolve il problema ...

Un'immagine potrebbe chiarire le cose: bar pattern

risposta

43

Ho scritto una soluzione rapida, che controlla ogni posizione di annotazione rispetto alle caselle di delimitazione predefinite per tutte le altre annotazioni. Se c'è una collisione cambia la sua posizione al prossimo posto libero da collisioni disponibile. Mette anche belle frecce.

Per un esempio piuttosto estremo, produrrà questo (nessuno dei numeri sovrappongono): enter image description here

Invece di questo: enter image description here

Ecco il codice:

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) 

Ecco il codice che produce questi grafici, che mostra l'utilizzo:

#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() 
+0

piuttosto bello. C'è anche un modo per generalizzare questo su grafici non bar? Sto cercando di annotare un grafico a dispersione, e naturalmente sarebbe bello se anche la distanza delle frecce fosse ridotta al minimo. Inoltre è possibile minimizzare la quantità di frecce che attraversano i numeri? – tarrasch

+0

@tarrasch - Questo principio dovrebbe funzionare bene per qualsiasi tipo di trama. Spero di avere il tempo di battere il codice in una forma più attraente nei prossimi giorni (deve essere generalizzato, come ho detto). La distanza delle frecce può essere ridotta un po '(cambiare '2 * L' a' L'), ma le frecce tipo devono passare attraverso i numeri qualche volta (inizierà a diventare molto più complesso per evitarlo), tuttavia se si modificano le frecce 'alpha' su' alpha = 0.3' e il testo 'color' su blu, la trama inizia a sembrare ancora migliore. – fraxel

+0

bello! lo proverò questo pomeriggio :) – tarrasch

7

Una possibilità è quella di ruotare il testo/annotazione, che è impostato dalla parola chiave/proprietà rotation. Nell'esempio seguente, ruoto il testo di 90 gradi per garantire che non entri in collisione con il testo adiacente. Ho anche impostato il va (abbreviazione di verticalalignment) parola chiave, in modo che il testo si presenta sopra la barra (superiore al punto che uso per definire il testo):

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() 

Il risultato è il seguente dato:

enter image description here

Determinare a livello di codice se ci sono collisioni tra varie annotazioni è un processo più complicato. Questo potrebbe valere una domanda a parte: Matplotlib text dimensions.

+0

Ciò risponde alla domanda per i bar un po 'più ampi, ma nel mio caso sono molto sottili e molto stretto allineamento quindi, anche in verticale non lo farebbe Ho anche pensato ad alcuni test di collisione di bounding box ma questo aumenterà la complessità ben oltre il tempo che sono disposto a spendere :) – BandGap

+0

@BandGap, quindi lo farei manualmente, impostando la posizione di annotazione nella parte superiore di ogni barra e regolando la posizione del testo fino a quando non si scontrano (regolando solo il componente y), e definirei uno stile di freccia, come fanno nella sezione degli assi di annotazione della guida per l'utente: http://matplotlib.sourceforge.net/ users/annotations_guide.html # annotating-axes Ciò consente a una freccia di puntare dall'etichetta alla barra e le linee di testo da separare l'una dall'altra. Fammi sapere che questo suggerimento non è chiaro. – Yann

+3

Se dovessi farlo manualmente, potrei semplicemente stamparlo e aggiungere del testo a mano. Dato che ho bisogno di diversi grafici con la modifica della posizione e dell'altezza della barra, questo non è fattibile (a parte il fatto che ci sono diverse decine di barre) – BandGap

6

Un'altra opzione che utilizza la mia libreria adjustText, scritta appositamente per questo scopo (https://github.com/Phlya/adjustText). Penso che sia probabilmente molto più lento della risposta accettata (rallenta notevolmente con molte barre), ma molto più generale e configurabile.

from adjustText import adjust_text 
np.random.seed(2017) 
x_data = np.random.random_sample(100) 
y_data = np.random.random_integers(10,50,(100)) 

f, ax = plt.subplots(dpi=300) 
bars = ax.bar(x_data, y_data, width=0.001, facecolor='k') 
texts = [] 
for x, y in zip(x_data, y_data): 
    texts.append(plt.text(x, y, y, horizontalalignment='center', color='b')) 
adjust_text(texts, add_objects=bars, autoalign='y', expand_objects=(0.1, 1), 
      only_move={'points':'', 'text':'y', 'objects':'y'}, force_text=0.75, force_objects=0.1, 
      arrowprops=dict(arrowstyle="simple, head_width=0.25, tail_width=0.05", color='r', lw=0.5, alpha=0.5)) 
plt.show() 

enter image description here

Se permettiamo autoallineamento lungo l'asse x, è ancora meglio (ho solo bisogno di risolvere un piccolo problema che non gli piace mettere le etichette sopra i punti e non un po 'alla lato...).

np.random.seed(2017) 
x_data = np.random.random_sample(100) 
y_data = np.random.random_integers(10,50,(100)) 

f, ax = plt.subplots(dpi=300) 
bars = ax.bar(x_data, y_data, width=0.001, facecolor='k') 
texts = [] 
for x, y in zip(x_data, y_data): 
    texts.append(plt.text(x, y, y, horizontalalignment='center', size=7, color='b')) 
adjust_text(texts, add_objects=bars, autoalign='xy', expand_objects=(0.1, 1), 
      only_move={'points':'', 'text':'y', 'objects':'y'}, force_text=0.75, force_objects=0.1, 
      arrowprops=dict(arrowstyle="simple, head_width=0.25, tail_width=0.05", color='r', lw=0.5, alpha=0.5)) 
plt.show() 

enter image description here

(ho dovuto regolare alcuni parametri qui, naturalmente)

+0

Il readme dice che trasforma i testi in annotazioni. Significa che non funzionerà se le annotazioni che si sovrappongono sono ciò che devo iniziare? –

+0

@JosephGarvin no, attualmente non supporta annotazioni, deve iniziare con oggetti di testo. – Phlya