python - how - matplotlib title position
Anotaciones superpuestas Matplotlib (3)
Quiero anotar las barras en un gráfico con texto, pero si las barras están muy juntas y tienen una altura comparable, las anotaciones están por encima de ea. otro y, por lo tanto, difícil de leer (las coordenadas para las anotaciones se tomaron de la posición y la altura del compás).
¿Hay alguna manera de cambiar uno de ellos si hay una colisión?
Editar: Las barras son muy finas y muy cerradas, por lo que alinearlas verticalmente no resuelve el problema ...
Una imagen puede aclarar cosas:
Otra opción que usa mi biblioteca adjustText
, escrita especialmente para este propósito ( https://github.com/Phlya/adjustText ). Creo que es probablemente mucho más lenta que la respuesta aceptada (se ralentiza considerablemente con muchas barras), pero es mucho más general y configurable.
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()
Si permitimos la autoalineación a lo largo del eje x, se vuelve aún mejor (solo tengo que resolver un pequeño problema de que no le gusta poner etiquetas por encima de los puntos y no un poco a un lado ...).
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()
(Tuve que ajustar algunos parámetros aquí, por supuesto)
Una opción es rotar el texto / anotación, que se establece mediante la palabra clave / propiedad de rotation
. En el siguiente ejemplo, giro el texto 90 grados para garantizar que no colisione con el texto vecino. También establecí la palabra clave va
(abreviatura de verticalalignment
), de modo que el texto se presente sobre la barra (sobre el punto que uso para definir el texto):
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()
El resultado es la siguiente figura:
Determinar mediante programación si hay colisiones entre varias anotaciones es un proceso más complicado. Esto podría valer una pregunta por separado: dimensiones de texto de Matplotlib .
He escrito una solución rápida, que comprueba cada posición de anotación contra cuadros delimitadores predeterminados para todas las otras anotaciones. Si hay una colisión, cambia su posición al siguiente lugar libre de colisión disponible. También pone bonitas flechas.
Para un ejemplo bastante extremo, producirá esto (ninguno de los números se superponen):
En lugar de esto:
Aquí está el código:
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)
Aquí está el código que produce estas representaciones, mostrando el uso:
#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()