Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Matplotlib overlapping annotations

I want to annotate the bars in a graph with some text but if the bars are close together and have comparable height, the annotations are above ea. other and thus hard to read (the coordinates for the annotations were taken from the bar position and height).

Is there a way to shift one of them if there is a collision?

Edit: The bars are very thin and very close sometimes so just aligning vertically doesn't solve the problem...

A picture might clarify things: bar pattern

like image 443
BandGap Avatar asked Jan 13 '12 11:01

BandGap


People also ask

How do I stop Matplotlib overlapping?

Use legend() method to avoid overlapping of labels and autopct. To display the figure, use show() method.

What is BBOX in Matplotlib?

BboxTransformTo is a transformation that linearly transforms points from the unit bounding box to a given Bbox. In your case, the transform itself is based upon a TransformedBBox which again has a Bbox upon which it is based and a transform - for this nested instance an Affine2D transform.

How do I annotate in Matplotlib?

The annotate() function in pyplot module of matplotlib library is used to annotate the point xy with text s. Parameters: This method accept the following parameters that are described below: s: This parameter is the text of the annotation. xy: This parameter is the point (x, y) to annotate.


2 Answers

I've written a quick solution, which checks each annotation position against default bounding boxes for all the other annotations. If there is a collision it changes its position to the next available collision free place. It also puts in nice arrows.

For a fairly extreme example, it will produce this (none of the numbers overlap): enter image description here

Instead of this: enter image description here

Here is the 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) 

Here is the code producing these plots, showing the usage:

#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() 
like image 66
fraxel Avatar answered Oct 14 '22 10:10

fraxel


Another option using my library adjustText, written specially for this purpose (https://github.com/Phlya/adjustText). I think it's probably significantly slower that the accepted answer (it slows down considerably with a lot of bars), but much more general and 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() 

enter image description here

If we allow autoalignment along x axis, it gets even better (I just need to resolve a small issue that it doesn't like putting labels above the points and not a bit to the side...).

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

(I had to adjust some parameters here, of course)

like image 36
Phlya Avatar answered Oct 14 '22 10:10

Phlya