I have code that handles coloring and plotting multiple plots automatically easily (for me). I want to make annotation easier:
goal: If an annotatio
This is my solution, one I was trying to avoid. I saw in the old question linked in the question comments someone mentions this is an np-complete problem, but I want to point out that's doesn't really matter. I tested up to 26 annotations, and it takes a few seconds but no more. Any practical plot won't have 1000 annotations.
Caveats:
Background:
Iplot
is a helper class I have to handle multiplot figures, handling coloring, sizing and now annotating.plt
is matplotlib.pyplot
Iplot.axes
holds my axes figure.EDIT I removed my class code to make this more copy pasteable. Axes should be given to the function, and kwargs accept an existing boxes keyword to take into account previous annotations, which is edited in place. Note I use a class to encapsulate this. The function has to return the boxes for use as well, in case this is a first call.
EDIT 2
After a while sped this up - no need to draw so much, better to loop twice and then update the renderer in between.
The code:
def annotate(axes,boxes,labels,data,**kwargs):
#slide should be relevant edge of bbox - e.g. (0,0) for left, (0,1) for bottom ...
try: slide = kwargs.pop("slide")
except KeyError: slide = None
try:
xytexts = kwargs.pop("xytexts")
xytext = xytexts
except KeyError:
xytext = (0,2)
xytexts = None
try: boxes = kwargs.pop("boxes")
except KeyError: boxes = list()
pixel_diff = 1
newlabs = []
for i in range(len(labels)):
try:
len(xytexts[i])
xytext = xytexts[i]
except TypeError: pass
a = axes.annotate(labels[i],xy=data[i],textcoords='offset pixels',
xytext=xytext,**kwargs)
newlabs.append(a)
plt.draw()
for i in range(len(labels)):
cbox = a.get_window_extent()
if slide is not None:
direct = int((slide[0] - 0.5)*2)
current = -direct*float("inf")
arrow = False
while True:
overlaps = False
count = 0
for box in boxes:
if cbox.overlaps(box):
if direct*box.get_points()[slide] > direct*current:
overlaps = True
current = box.get_points()[slide]
shift = direct*(current - cbox.get_points()[1-slide[0],slide[1]])
if not overlaps: break
arrow = True
position = array(a.get_position())
position[slide[1]] += shift * direct * pixel_diff
a.set_position(position)
plt.draw()
cbox = a.get_window_extent()
x,y = axes.transData.inverted().transform(cbox)[0]
if arrow:
axes.arrow(x,y,data[i][0]-x,data[i][1]-y,head_length=0,head_width=0)
boxes.append(cbox)
plt.draw()
return boxes
Any suggestions to improve will be warmly welcomed. Many thanks!