Матпотлиб перекрывает аннотации

Я хочу, чтобы аннотировать бары на графике с некоторым текстом, но если полосы близки друг к другу и имеют сопоставимую высоту, аннотации находятся выше ea. другие и, следовательно, их трудно читать (координаты аннотаций были взяты из позиции и высоты стержня).

Есть ли способ сдвинуть один из них, если есть столкновение?

Изменить: Бары очень тонкие и очень близкие, поэтому просто выравнивание по вертикали не решает проблему...

Изображение может прояснить ситуацию: bar pattern

Ответы

Ответ 1

Я написал быстрое решение, которое проверяет каждую позицию аннотации по умолчанию для всех остальных аннотаций. При столкновении он меняет свое положение на следующее свободное место без конфликтов. Он также добавляет хорошие стрелки.

Для довольно экстремального примера он будет производить это (ни одно из чисел не перекрывается): enter image description here

Вместо этого: enter image description here

Вот код:

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)

Вот код, создающий эти графики, показывающий использование:

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

Ответ 2

Один из вариантов заключается в том, чтобы повернуть текст/аннотацию, которая задается ключевым словом/свойством rotation. В следующем примере я поворачиваю текст на 90 градусов, чтобы гарантировать, что он не столкнется с соседним текстом. Я также установил ключевое слово va (сокращение от verticalalignment), чтобы текст был представлен над панелью (над точкой, которую я использую для определения текста):

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

Результат следующий:

enter image description here

Определение программно, если есть столкновения между различными аннотациями, - более сложный процесс. Это может стоить отдельного вопроса: Текстовые размеры Matplotlib.

Ответ 3

Другой вариант, использующий мою библиотеку adjustText, написанную специально для этой цели (https://github.com/Phlya/adjustText). Я думаю, что это, вероятно, значительно медленнее, чем принятый ответ (он значительно замедляется с большим количеством баров), но гораздо более общий и настраиваемый.

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

введите описание изображения здесь

Если мы разрешаем автоматическое выравнивание вдоль оси x, оно становится еще лучше (мне просто нужно решить небольшую проблему, которая не похожа на нанесение надстроек над точками, а не немного в сторону...).

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

введите описание изображения здесь

(я должен был, конечно, настроить некоторые параметры)