Phlya / adjustText

A small library for automatically adjustment of text position in matplotlib plots to minimize overlaps.
https://adjusttext.readthedocs.io/
MIT License
1.5k stars 87 forks source link

Unable to avoid text overlap each other even avoid_self = True #125

Closed OuYaozhong closed 8 months ago

OuYaozhong commented 2 years ago

In many times, when we use matplotlib twinx to create two line in two axes but they share same x-axis, the first point and the last point may in the same position in figure coordinate.

If I want to annotate the start point and the end point, the annotation text will fully overlap. And the text should be repelled from the line to avoid overlap with the line too.

I found that in this situation, the adjust_text is unable to let the text separate.

Here is a simple demo. Very easy.

from adjustText import adjust_text
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from utils import get_line_repl_loc

fig = plt.figure(dpi=300)
ax = plt.subplot(111)

line1 = ax.plot([0, 1, 2], [1, 2, 3])[0]
line2 = ax.plot([0, 1, 2], [1, 2, 3])[0]

text_list = []
text_list.append(ax.text(x=0, y=1, s='line1', color=line1.get_color(), va='center', ha='center'))
text_list.append(ax.text(x=0, y=1, s='line2', color=line2.get_color(), va='center', ha='center'))

xy_repls = get_line_repl_loc(line1)
fig.savefig('./test0.png')
scatter = ax.scatter(x=xy_repls[:, 0], y=xy_repls[:, 1], color='r', alpha=0.1)
fig.savefig('./test1.png')

adjust_text(text_list, x=xy_repls[:, 0], y=xy_repls[:, 1], avoid_self=True, avoid_text=True, save_steps=True)
fig.savefig('./test2.png')

I have a repel function get_line_repl_loc to generate the interpolation point to repel text away from the lines.

Here, first, we see the original plot: test0

Then, I plot the xy_repls by function scatter, we can see the xy_repls are correct. test1

Finally, use the adjust_text to move the text objects, with the limitation to be away from the line with dense point, avoid text itself and each other avoid_self=True and avoid_text=True test2

It seem the adjust_text did nothing.

I visualise the operation of the adjust_text by save_steps=True. Here is the gif

steps_gif

Autoalign separate the text each other, but the algorithm moves them closer and closer. Why? Or how to solve it?

If disable point repelling, it work well but just will overlap with the line instead of text each other. adjust_text(text_list, avoid_self=True, avoid_text=True, save_steps=True)

no_repls

I have try expand_text or force_point, both of them is useless, except that I add the text itself into the add_objects, but this kind of solution is meaningless. adjust_text(text_list, avoid_self=True, avoid_text=True, save_steps=True, add_objects=text_list)

add_objects

This problem I have mention in #124 also.

So can owner gives some explanation or it just a bug?

Environment: numpy version 1.21.2 matplotlib version 3.5.1

$ conda list | grep adjust adjusttext 0.7.3.1 py_1 conda-forge

Phlya commented 2 years ago

Difficult problem... Have you tried increasing force_text? Strange that the line1 text isn't moving in the first animation...

OuYaozhong commented 2 years ago

I have tried some ablation experiments, force_text does not make sense. Only increase force_points makes a difference.

Noted: I modify add the kwargs to let the parameter be able to show in the picture:

force_text = [0.5, 0.5]
force_points = None
kwargs = {}
if force_text is not None:
    kwargs.update({'force_text': force_text})
if force_points is not None:
    kwargs.update({'force_points': force_points})

adjust_text(text_list, x=xy_repls[:, 0], y=xy_repls[:, 1], save_steps=True, avoid_self=True, avoid_text=True, **kwargs)

The whole test code is:

from adjustText import adjust_text
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from utils import get_line_repl_loc
import random

force_text = [0.5, 0.5]
force_points = None

fig = plt.figure(dpi=300)
ax = plt.subplot(111)

line1 = ax.plot([0, 1, 2], [1, 2, 3])[0]
line2 = ax.plot([0, 1, 2], [1, 2, 3])[0]

text_list = []
text_list.append(ax.text(x=0, y=1, s='line1', color=line1.get_color(), va='center', ha='center'))
text_list.append(ax.text(x=0, y=1, s='line2', color=line2.get_color(), va='center', ha='center'))

string = f'force_text = {force_text}, force_points = {force_points}'
fig.text(x=0.5, y=0, va='bottom', ha='center', s=string, color='k', transform=fig.transFigure)

# text_list.append(ax.annotate(xy=(0, 1), text='line1', color=line1.get_color(), va='center', ha='center', arrowprops=dict(arrowstyle='->', color='black')))
# text_list.append(ax.annotate(xy=(0, 1), text='line2', color=line2.get_color(), va='center', ha='center', arrowprops=dict(arrowstyle='->', color='black')))

xy_repls = get_line_repl_loc(line1)
fig.savefig('./test0.png')
scatter = ax.scatter(x=xy_repls[:, 0], y=xy_repls[:, 1], color='r', alpha=0.1)
fig.savefig('./test1.png')

kwargs = {}
if force_text is not None:
    kwargs.update({'force_text': force_text})
if force_points is not None:
    kwargs.update({'force_points': force_points})

adjust_text(text_list, x=xy_repls[:, 0], y=xy_repls[:, 1], save_steps=True, avoid_self=True, avoid_text=True, **kwargs)
fig.savefig('./test2.png')
  1. Use force_text 1.1 increase to force_text=[0.5, 0.5] adjust_text(text_list, x=xy_repls[:, 0], y=xy_repls[:, 1], save_steps=True, force_text=[0.5, 0.5], avoid_self=True, avoid_text=True)

1

1.2 increase to force_text = [2.0, 2.0] 3

  1. Use force_points=[0.5, 0.5], only this one make a difference 2

It seems strange that force_points makes sense instead of force_text

And when I add and arrow into the adjust_text, adjust_text(text_list, x=xy_repls[:, 0], y=xy_repls[:, 1], save_steps=True,avoid_self=True, avoid_text=True, arrowprops=dict(arrowstyle='->', color='black'), **kwargs), it shows that the arrowprops is conflict with the avoid_self and avoid_text

Traceback (most recent call last):
  File "***/test1.py", line 38, in <module>
    adjust_text(text_list, x=xy_repls[:, 0], y=xy_repls[:, 1], save_steps=True, avoid_self=True, avoid_text=True, arrowprops=dict(arrowstyle='->', color='black'), **kwargs)
  File "***/.conda/envs/py39torch110cu113/lib/python3.9/site-packages/adjustText/__init__.py", line 571, in adjust_text
    ax.annotate("", # Add an arrow from the text to the point
  File "***/.conda/envs/py39torch110cu113/lib/python3.9/site-packages/matplotlib/axes/_axes.py", line 666, in annotate
    a = mtext.Annotation(text, xy, *args, **kwargs)
  File "***/.conda/envs/py39torch110cu113/lib/python3.9/site-packages/matplotlib/text.py", line 1825, in __init__
    Text.__init__(self, x, y, text, **kwargs)
  File "***/.conda/envs/py39torch110cu113/lib/python3.9/site-packages/matplotlib/text.py", line 160, in __init__
    self.update(kwargs)
  File "***/.conda/envs/py39torch110cu113/lib/python3.9/site-packages/matplotlib/text.py", line 172, in update
    super().update(kwargs)
  File "***/.conda/envs/py39torch110cu113/lib/python3.9/site-packages/matplotlib/artist.py", line 1064, in update
    raise AttributeError(f"{type(self).__name__!r} object "
AttributeError: 'Annotation' object has no property 'avoid_self'

The reason is the adjust_text use an empty matplotlib.text.Annotation object to add an arrow into the text. And in the code of adjust_text, the avoid_self and avoid_text is incompatible with the arrow objects which is matplotlib.text.Annotation class. I don't know if relative to the problem.

Phlya commented 8 months ago

It's been so long everything has changed now and hopefully works now? Feel free to reopen.