Open aminihamed opened 1 month ago
How did you generate the picture above?
At the moment there is no explicit way to have this constraint (and I don't see where in the ggrepel example it's explicitly set to avoid crossings? I think it just worked that way with the chosen arguments), but usually in practice there is a way to minimize the crossings... For example, if you switch off explode (explode_radius=0
and/or force_explode=(0,0)
) and reduce forces it should help.
Thanks for your message.
Can you share your data and code?
import matplotlib.pyplot as plt
import matplotlib.colors as cm
from adjustText import adjust_text
from matplotlib.lines import Line2D
fig, ax = plt.subplots(figsize=(5,10))
ys = [1,1.1,1.2,2,2.5,3, 15,16,17, 50,52,53,54, 60,61,62,63,64,65,66, 100,101,102,103,104]
xs = len(ys) * [0]
labels = [str(i) for i in range(1, len(x)+1)]
ax.set_xlim(-1.5,1.5)
cm_pastel2 = 10 * [cm.to_hex(plt.cm.tab10(i)) for i in range(10)]
ii = -1
#an empty list for text objects
texts = []
for label,y in zip(labels, ys):
ii = ii+ 1
ax.plot(xs[ii], ys[ii], "+", c=cm_pastel2[ii])
texts.append(ax.text(x=1, y=y, s=label, c=cm_pastel2[ii], fontdict={'fontweight':10}, va='center'))
#adjust text labels
adjtexts, _ = adjust_text(
texts,
avoid_self=False,
only_move="y", # Only allow movement vertically
max_move=None,
ensure_inside_axes=True,
explode_radius=0,
force_explode=(0.0, 0.0),
)
#plot the connector lines
ii = -1
for text in adjtexts:
ii = ii + 1
_, y_adjusted = text.get_position()
line = Line2D(
[0.1, 0.3, 0.7, 0.9],
[ys[ii], ys[ii], y_adjusted, y_adjusted],
clip_on=False,
color=cm_pastel2[ii],
linewidth=0.75,
)
ax.add_line(line)
The indentation does not look correct in the code that I pasted above.
OK, the main solution is actually switching off the pull force in this case (the force that tries to pull the labels back to their original positions, often useful to make sure they don't end up too far from them), and finding the right value for force_text. This works well for me:
import matplotlib.pyplot as plt
import matplotlib.colors as cm
from adjustText import adjust_text
from matplotlib.lines import Line2D
fig, ax = plt.subplots(figsize=(5,10))
ys = [1,1.1,1.2,2,2.5,3, 15,16,17, 50,52,53,54, 60,61,62,63,64,65,66, 100,101,102,103,104]
xs = len(ys) * [0]
labels = [str(i) for i in range(1, len(xs)+1)]
ax.set_xlim(-1.5,1.5)
cm_pastel2 = 10 * [cm.to_hex(plt.cm.tab10(i)) for i in range(10)]
ii = -1
#an empty list for text objects
texts = []
for label,y in zip(labels, ys):
ii = ii+ 1
ax.plot(xs[ii], ys[ii], "+", c=cm_pastel2[ii])
texts.append(ax.text(x=1, y=y, s=label, c=cm_pastel2[ii], fontdict={'fontweight':10}, va='center'))
#adjust text labels
adjtexts, _ = adjust_text(
texts,
avoid_self=False,
only_move="y", # Only allow movement vertically
max_move=None,
ensure_inside_axes=True,
force_pull=(0, 0),
force_text=(0, 0.05),
)
#plot the connector lines
ii = -1
for text in adjtexts:
ii = ii + 1
_, y_adjusted = text.get_position()
line = Line2D(
[0.1, 0.3, 0.7, 0.9],
[ys[ii], ys[ii], y_adjusted, y_adjusted],
clip_on=False,
color=cm_pastel2[ii],
linewidth=0.75,
)
ax.add_line(line)
nice, however it seems that if the y axis is limited to the extend of the data i.e. 'ax.set_ylim(0, 105)' the crossing happens again.
Yeah I guess that's because of ensure_inside_axes=True - it will force the labels outside axes limits inside the Axes, and then the crossing can occur.
Or maybe there something else going on... Idk there is not specific mechanism to ensure this (yet - I have thought about adding something, but it's not trivial), so there is always tweaking involved, unfortunately.
sure, no problem. thanks for your support. I think I'll relax the y-axis limits on my plots for now :-)
ps: I'm not sure about the optimisation algorithm, but I was thinking in 1D application the constraint is to keep the order of distance from origin the same between the original and adjusted labels, and in 2D keep the order of distance (r) from origin and azimuth (alpha) the same. For example, in the figure below, the adjusted labels should satisfy the following conditions
pps: maybe the midpoint (center of gravity) of labels is a better choice than origin ....
Generally wider axes limits give more space and usually the result of label placement looks better.
Seems like a nice idea, I'm not sure how to implement it with the current algorithm though... I was thinking something more naive like directly checking whether there are any overlaps between the lines connecting labels to their targets and if so, swapping the labels. It can get tricky since it's a lot of comparisons for all pairwise combinations, and there might be weird cases when more than two lines all overlap each other or smth... not sure how to deal with that, maybe a smarter approach like you suggest would work better...
So I actually gave it a go, and I think it works... In the master branch the arrows shouldn't cross anymore, but it's not tested with complicated cases - i.e. not sure what will happen if multiple arrows all cross each.
Thanks for both the question and script! This was super helpful.
Quick question. Changing the angle of text breaks it though. Is there a way to prevent this? Couple examples:
rotation=45
to texts.append
section in line 22 of @Phlya script results in connector lines crossing:labels
is increased, labels get heavily disordered in the plot:
Sorry for the late reply, I was on vacation. Did you install the version from github master branch to check the crossings? It's not perfect, but seems to generally work... Anyway, I am working on some improvements to that code too.
For the second point - there is just not enough space in the plot to accommodate such tall labels! So it breaks down trying to push them out of the Axes, while also ensuring they are inside the Axes limits... The result could be better, but I think it's simply impossible to expect a good result from such a setup.
(It's also possible something about rotated texts causes issues in general, it's not something I have tested...)
Thanks for the response!
The code that explicitly removes overlaps is only available in the master branch so far, because it's very experimental.
In this case since you limit movement to only Y axis, you need to increase the Y axis limits, not X.
Ah, that makes sense! Thanks!
is it possible to constrain the optimization further to avoid crossing of the connector lines? its practical application is shown below where the current setup (allowing to move along y only) dose not preserve the order of the original labels along the y direction.
ggrepel appears to have such a constraint? https://ggrepel.slowkow.com/articles/examples#align-labels-on-the-left-or-right-edge