2012-01-13 4 views
35

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

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

Edit: Полоски очень тонкие и очень близко, иногда так просто выравнивая по вертикали не решает проблему ...

Изображение может прояснить вещи: bar pattern

ответ

43

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

Для довольно крайний пример, он не будет производить это (ни одно из чисел перекрытия): 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() 
+0

довольно приятно. Есть ли способ обобщить это на не-bar grafics? Я пытаюсь аннотировать диаграмму рассеяния, и, естественно, было бы неплохо, если бы расстояние стрел было также минимизировано. Также можно минимизировать количество стрелок, проходящих через цифры? – tarrasch

+0

@tarrasch - Этот принцип должен работать нормально для любого сюжета. Надеюсь, у меня будет время сбить код в более привлекательную форму в ближайшие пару дней (его нужно обобщить, как я уже упоминал). Расстояние между стрелками можно немного уменьшить (измените '2 * L' на' L'), но стрелки вроде бы должны иногда проходить через числа (это начнет становиться намного сложнее, чтобы этого избежать), однако если вы измените настройку «альфа» стрелок на «альфа = 0,3», а текст «цвет» - синий, сюжет начнет выглядеть еще лучше. – fraxel

+0

приятно! я попробую это во второй половине дня :) – tarrasch

7

Одним из вариантов является повернуть текст/аннотацию, которая задается ключевым словом/свойством 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'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() 

В результате на следующем рисунке:

enter image description here

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

+0

Это отвечает на вопрос, для несколько более широких баров, но в моем случае они очень тонкие и очень близко, так что даже вертикальное выравнивание не сделал бы. Я также подумал о некоторых испытаниях на столкновение с ограничивающей коробкой, но это увеличит сложность далеко за время, когда я захочу потратить на это :) – BandGap

+0

@BandGap, тогда я бы сделал это вручную, установив позицию аннотации в верхней части каждого бара и отрегулируйте положение текста до тех пор, пока они не будут сталкиваться (путем настройки только компонента y), и я бы определил стиль стрелки, как это делается в разделе аннотирующих осей руководства пользователя: http://matplotlib.sourceforge.net/ users/annotations_guide.html # аннотирующие оси. Это позволяет стрелке указывать с вашей метки на вашу панель и линии текста, которые должны быть отделены друг от друга. Дайте мне знать, что это предложение неясно. – Yann

+3

Если бы я должен был сделать это вручную, я мог бы просто распечатать его и добавить текст вручную. Поскольку мне нужно несколько участков с изменяющимся положением и высотой полотна, это невозможно (кроме того, что существует несколько десятков баров) – BandGap

6

Другой вариант использования моей библиотеки 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() 

enter image description here

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

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

(мне пришлось изменить некоторые параметры здесь, конечно)

+0

В readme говорится, что он превращает тексты в аннотации. Означает ли это, что это не сработает, если аннотации, которые перекрываются, - это то, с чем я должен начать? –

+0

@JosephGarvin no, в настоящее время он не поддерживает аннотации, он должен начинаться с текстовых объектов. – Phlya

Смежные вопросы