Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avoid crossing of the connector lines #182

Open
aminihamed opened this issue Aug 8, 2024 · 17 comments
Open

avoid crossing of the connector lines #182

aminihamed opened this issue Aug 8, 2024 · 17 comments

Comments

@aminihamed
Copy link

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.
image

ggrepel appears to have such a constraint?
https://ggrepel.slowkow.com/articles/examples#align-labels-on-the-left-or-right-edge

@Phlya
Copy link
Owner

Phlya commented Aug 8, 2024

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.

@aminihamed
Copy link
Author

Thanks for your message.

  • I read the positions of the adjusted texts and used Line2D to link the original positions to the adjusted ones (in a for loop)
  • I'm not sure about the ggrepel setup either. I just saw the picture and thought it might be relevant. Perhaps there is no argument to control this behavior, but maybe it is (?) built in the optimization algorithm.
  • I agree that there is an optimal set of parameters that minimizes the crossings, I can confirm that explode settings above did not fix the issue.

@Phlya
Copy link
Owner

Phlya commented Aug 8, 2024

Can you share your data and code?

@aminihamed
Copy link
Author

aminihamed commented Aug 8, 2024

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)

image

The indentation does not look correct in the code that I pasted above.
image

@Phlya
Copy link
Owner

Phlya commented Aug 8, 2024

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)

image

@aminihamed
Copy link
Author

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.

@Phlya
Copy link
Owner

Phlya commented Aug 8, 2024

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.

@Phlya
Copy link
Owner

Phlya commented Aug 8, 2024

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.

@aminihamed
Copy link
Author

aminihamed commented Aug 8, 2024

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 ....

image

@Phlya
Copy link
Owner

Phlya commented Aug 8, 2024

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...

@Phlya
Copy link
Owner

Phlya commented Aug 15, 2024

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.

@ManavalanG
Copy link

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:

  • Adding rotation=45 to texts.append section in line 22 of @Phlya script results in connector lines crossing:
image
  • In addition to the above modification, if size of the label strings in line 10 labels is increased, labels get heavily disordered in the plot:
image

@Phlya
Copy link
Owner

Phlya commented Sep 10, 2024

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.

@Phlya
Copy link
Owner

Phlya commented Sep 10, 2024

(It's also possible something about rotated texts causes issues in general, it's not something I have tested...)

@ManavalanG
Copy link

Thanks for the response!

  1. I used the most recent release (v1.2.0) available from conda.
  2. So if I make more room, for example, changing xaxis max to 4.0 instead of 1.5, it will likely work? Sorry, I haven't had a chance to put it to test.

@Phlya
Copy link
Owner

Phlya commented Sep 10, 2024

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.

@ManavalanG
Copy link

Ah, that makes sense! Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants