Skip to content

Implement head resizing (and reversal) for larrow/rarrow/darrow #29998

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

Open
wants to merge 29 commits into
base: main
Choose a base branch
from

Conversation

CharlieThornton33
Copy link

@CharlieThornton33 CharlieThornton33 commented May 1, 2025

PR summary

When creating an annotation using boxed text in an arrow shape (patches.BoxStyle.LArrow/RArrow/DArrow), head_width and head_angle arguments may now be passed to adjust the size of the arrow head(s), and (via the use of negative angles) generate 'reversed' arrow heads. Addresses #24618 and is a direct continuation of @Abitamim's work in #24744.

This solves both the issue initially mentioned in #24618 (creating a pentagonal 'road-sign' annotation box), as well as allowing for many other customisation options for arrows; for example, a reversed-head DArrow could act as a way to bring attention to a certain region on an axis.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(4, 3))

t1 = ax.text(0.5, 0.75, "Road-sign",
            ha="center", va="center", size=15,
            bbox=dict(boxstyle="rarrow,pad=0.3,head_width=1,head_angle=45",
                      fc="lightblue", ec="steelblue", lw=2))

t2 = ax.text(0.5, 0.25, "  Reversed heads   ",
            ha="center", va="center", size=15,
            bbox=dict(boxstyle="darrow,pad=0.3,head_width=1.5,head_angle=-45",
                      fc="lightblue", ec="steelblue", lw=2))

plt.show()

pr_example

A new test, test_boxarrow_adjustment, has been added to test these options.

PR checklist

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@CharlieThornton33
Copy link
Author

Hi everyone,

I was just wondering whether someone would be willing to have a look at this PR? I feel like it's in a complete or nearly-complete state but need some guidance - the two failing CI jobs seem to be:

  • PR cleanliness:
    The added unit test was written first by @Abitamim, and then expanded by myself. When I expanded the test to check the newly-added reversed arrow heads, I changed both the test name (temp_test_boxarrowtest_boxarrow_adjustment) and the name of the output image (roadsign_test_image.pngboxarrow_adjustment_test_image.png) to reflect this. The 'file both added and deleted' error given seems to be a result of adding then deleting roadsign_test_image.png.
  • Python 3.11 on macos-13:
    The failing test here (test_blitting_events) is marked as flaky, with the comment # subprocesses can struggle to get the display, so rerun a few times. I feel it may be that this was just a random instance of the test failing multiple times in a row; unfortunately I don't have access to macos-supported hardware for rerunning this locally, so I was wondering if someone would be able to take a look.

Any help would be greatly appreciated. Thanks!

@timhoffm
Copy link
Member

timhoffm commented May 9, 2025

PR-cleanliness: This is a check so that we don't merge commits with unneeded images, which would increase repo size. You can either squash and force-push. Or ignore this and we'll squash-merge at the end.

Ignore the macos test failure. It's unrelated.

@CharlieThornton33
Copy link
Author

Thank you for getting back so quickly! Would it be okay if you handled the squashing - I'm relatively new to git, and I'm not sure of the best method to use for squashing to keep a relatively clean but traceable commit history?

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the arrows in the test image, https://github.com/matplotlib/matplotlib/pull/29998/files#diff-ff8adb592a399d60ee5c93fbff7552182f53b74be4573ca21db404ef3c3a6673, the outline sometimes cuts through the text, which must not happen.

From plotting

plt.text(0.5, 0.5, 'text', size=24, bbox=dict(boxstyle="rarrow", fc="w", ec="k"),)
plt.text(0.5, 0.5, 'text', size=24, bbox=dict(boxstyle="square", pad=0, fc="C0", ec='none'))

we can see that the original calculation had a shaft with the length of the text, i.e. the arrow head starts at -pad from the end of the text.

image

This setting was ok for the hard-coded parameters, but when head_size and head_angle can vary, we have to invest more into a good positioning of arrow head.

Comment on lines 65 to 69
angle = -80
angle_step = 120 / repetitions
for i in range(repetitions):
for j, stylename in enumerate(sorted(styles)):
if stylename in ("larrow", "rarrow", "darrow"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explain what you are trying to achieve with this rather wild parameter juggling. I cannot follow the logic and the associated reference image looks complicated. I would have expected that you have a set of characteristic cases such as

{'head_width'=1.5, 'head_angle'=90},
{'head_width'=1, 'head_angle'=60},
{'head_witdh'=0.5, 'head_angle'=30},
{'head_witdh'=1.5, 'head_angle'=-90},

for each of 'larrow', 'darrow', 'rarrow'. You could coveniently arrange them on a regular grid.

I don't think you need to test the rotation. By design that's covered by the transform system and is thus orthogonal to the shaping of the arrow. But if you want, you could add an extra entry with rotation in the above matrix.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware that the transform system itself was tested elsewhere; I'll remove the rotations, as well as lay the arrows out more regularly. I was rushing quite a bit when writing the test, and I felt the positioning needed to somewhat resemble that in test_boxarrow - the insane double loop is a result of me trying to test multiple cases for each type of arrow while still using the positioning code from that test.

Before I get on to rewriting the test, though, I'll work on the arrow head positioning/padding issue.

@CharlieThornton33
Copy link
Author

Since I'm going to need to make some relatively major changes to the contents of this PR (a redo of the arrow drawing code and a rewrite of its unit test), would it be better for me to amend my last commit (d9b0b32) as suggested in the contributing guide, or make a few new commits? I'm not quite sure how to add more commits to an open PR - do I just push the changes to my development branch?

@rcomer
Copy link
Member

rcomer commented May 10, 2025

@CharlieThornton33 yes you can just push more commits to your current branch and they will appear in this PR. Since you already have several commits they will ultimately need squashing one way or the other (and I see you already asked that we handle that), so I do not think you gain anything from the —amend option.

@CharlieThornton33
Copy link
Author

I've just redone the padding (only for LArrow and RArrow for the moment), and I think the issue with the text escaping the outline has been fixed. I set the arrow body to dynamically increase in length to make sure there's always a gap of at least pad between the edge of the text closest to the arrow head and the position where that edge would first intercept the outline if moved towards the tip:

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(3, 8))

t0 = ax.text(0.5, 0.9, "Text",
            ha="center", va="center", rotation=0, size=30,
            bbox=dict(boxstyle="rarrow,pad=0.3,head_width=1.5,head_angle=-90",
                      fc="lightblue", ec="steelblue", lw=2))

t1 = ax.text(0.5, 0.7, "Text",
            ha="center", va="center", rotation=0, size=30,
            bbox=dict(boxstyle="rarrow,pad=0.3,head_width=1.5,head_angle=70",
                      fc="lightblue", ec="steelblue", lw=2))

t2 = ax.text(0.5, 0.5, "Text",
            ha="center", va="center", rotation=0, size=30,
            bbox=dict(boxstyle="rarrow,pad=0.3,head_width=1.5,head_angle=100",
                      fc="lightblue", ec="steelblue", lw=2))

t3 = ax.text(0.5, 0.3, "Text",
            ha="center", va="center", rotation=0, size=30,
            bbox=dict(boxstyle="rarrow,pad=0.3,head_width=1.5,head_angle=130",
                      fc="lightblue", ec="steelblue", lw=2))

t4 = ax.text(0.5, 0.1, "Text",
            ha="center", va="center", rotation=0, size=30,
            bbox=dict(boxstyle="rarrow,pad=0.3,head_width=1.5,head_angle=170",
                      fc="lightblue", ec="steelblue", lw=2))

plt.show()

new_padding

It's also a bit concerning that the CI workflow seems to run on every commit to this PR - am I okay to disable tests in my commit messages until the one where I write the new test to save on CI resources?

@CharlieThornton33
Copy link
Author

I've just noticed - all of the pytest test runs are about to fail because I forgot to remove the flawed test_boxarrow_adjustment test. Is there any way to cancel a CI test run?

@rcomer
Copy link
Member

rcomer commented May 14, 2025

@CharlieThornton33 I wouldn't worry about the CI failing, but if they are still running when you push another commit they will be interrupted and re-started.

@melissawm melissawm moved this from Needs review to Waiting for author in First Time Contributors May 26, 2025
@CharlieThornton33
Copy link
Author

Hi,

I'm afraid I won't be able to finish this PR for a few weeks - I'm currently in exam season at university and I'm having to focus on them at the moment. As soon as I've finished exams (mid June), I'll get back on it and finish the rest of the implementation.

@timhoffm
Copy link
Member

That's ok.

@CharlieThornton33
Copy link
Author

I'm going to try to write a new unit test tomorrow, then hopefully this should be okay to merge.

@CharlieThornton33
Copy link
Author

Hi,

I'm unsure about why the two Windows tests on Azure failed, and I don't think it's likely to be a direct result of my addition of test_boxarrow_adjustment.

The python 3.12 job failed test_fontcache_thread_safe, seemingly by some sort of timeout, and a child process appears to have lingered a bit after bash was stopped.

The python 3.13 job seemed to be slightly confused with versions - it was trying to link a free-threaded version of python at build time during C/C++ compilation (python313t.lib), but the only installed version in this CI job is the non-free-threaded python313.lib.

I'd really appreciate any help/guidance; apart from these failing jobs, I'm quite happy with the feature implementation as it is now and would be really grateful if someone could have a look and see whether it's okay.

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the margin for inverse arrows is a bit tight. Best be seen for the “square arrows”

IMG_6361

IMHO all three should be identical.

@CharlieThornton33
Copy link
Author

I think this should be all of the remaining issues fixed - I rectified the padding for reversed arrow heads (including a missing + dxx in the trapezoidal case), and updated test_boxarrow_adjustment accordingly.

@timhoffm
Copy link
Member

timhoffm commented Jul 7, 2025

Sorry, I'm quite scrace with time right now. Proper review will have to wait for some more days.

From a quick look: Do we get into trouble with these if the heads become even smaller?

image

There's no margin, so for very mall heads, the box line will touch the text. Can we add a margin here as well? Argument: Concerning space for text, these are basically like the flat versions
image
because the tip is generally only a narrow extension, into which text does not fit.

Also please check what the edge case
image
is / should be doing. If we scale the overshoot in these cases to 0
image, the second one (full margin) is still ok, the first one (negative) may be a bit tight - OTOH we may not need the full margin?

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not verified the math here, but just going by the test images, it looks good.

CharlieThornton33 and others added 2 commits July 24, 2025 16:27
Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
@CharlieThornton33
Copy link
Author

Hi everyone,

I was just wanting to get some input on how I should handle padding; should the margins around text form a complete rectangle? Currently, only a rectangle of width pad between the text and the arrow-head is guaranteed clear, and this can lead to some really close calls where text almost (and in some circumstances possibly could) leaves the arrow boundary. I feel like the most logical solution is to make sure the area shown dashed below lies fully inside the arrow, but I'd like to see if anyone has any better ideas.

padding_margins

@timhoffm
Copy link
Member

timhoffm commented Jul 27, 2025

Here are the options for regular arrows with different angles

  1. tip base is at text + pad
  2. tip base is at text
  3. tip point is at text + pad
  4. tip is moved so that the text box with horizontal pad only is inside
  5. tip is moved so that the text box with horizontal and vertical pad is inside
grafik

I've roughly encoded my preferece, though that's somewhat subjective - maybe the helper lines are also distracting: green = good, yellow = ok, red = problematic

Overall, if we want a simple criterion 1) is the best solution.
If we want to be more refined, use 2) if "the tip is large enough", otherwise 1). - We have to define "large enough": A simple heuristic solution would be some multiple of the pad, e.g. if the tip has a length of at least 2x pad. In theory, the crition of 5 (padded text is within the tip) could be taken as "large enough" but im inclined to think that a suitable chose heuristic is good enough.

@timhoffm
Copy link
Member

Additional images for arrows with headwidth=1 (would also hold for headwidth < 1).

grafik

Here 1) is clearly the preferred solution. Note that the above criterion "head is large enough" realized by "padded text is within the tip" is always false for 2) so the more refined version above would also cover this case. (But it's still probably easier to short-cut on headwidth <= 1).

@CharlieThornton33
Copy link
Author

@timhoffm I'm hesitant to implement the above; while the padding code becomes much simpler, for example for LArrow:

# Apply padding to text on head side
if self.head_width > 1 and width_adjustment / (pad * tan_half_angle) > 1:
    # Pad text into head by amount pad
     x0 += pad

a, b = 1.214, 0.250  # Empirical factors to align text
padding_offset = (a * pad) + (b * mutation_size)
x0 -= padding_offset

this breaks public API, as it doesn't match the preestablished text positioning.

@timhoffm
Copy link
Member

I'm not following, which is probably because I'm not deep enough in the topic:

  • Is the code snippet you show is a hypothetical implementation of my examples? What are these empirical factors?
  • What exactly breaks wrt. to the existing solution; i.e. in which cases and what is different?

We may consider breaking preestablished behavior if the solution is overall more consistent and much simpler to implement. But that requires a full overview on on the impact it would have.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Waiting for author
Development

Successfully merging this pull request may close these issues.

[ENH]: "Road sign" boxstyle/annotation
6 participants