Skip to content

[ENH]: Add fancyarrow() as a function to draw single arrows #29826

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

Closed
timhoffm opened this issue Mar 29, 2025 · 10 comments
Closed

[ENH]: Add fancyarrow() as a function to draw single arrows #29826

timhoffm opened this issue Mar 29, 2025 · 10 comments

Comments

@timhoffm
Copy link
Member

timhoffm commented Mar 29, 2025

Problem

Drawing arrows is currently a mess.

  1. We have ax.arrow() which uses a fundamentally broken transformation, typically resulting in skewed arrows.
  2. There has been ample discussion on replacing arrow() with a more general vector() method Vector #22435
  3. We have the recommendation to ax.annotate() with an empty text.

I feel (3.) is quite complicated if you just want to draw an arrow between two points. I therefore suggest to add a method ax.fancyarrow() to replace (3.). It's basically a wrapper for creating a FancyArrowPatch

def fancyarrow(self, ...):
    patch = FancyArrowPatch(...)
    self.add_patch(patch)
    return patch

Rationale:

  • The FancyArrowPatch API is capable and clear.
  • a wrapper function is justified since user code typically does not explicitly create and add Artists
  • The scope is reasonably different from what is discussed for vector() so that it does not overlap with a potential introduction of vector() later
  • the name fancyarrow is a bit ... fancy, but we cannot use arrow and since this returns a FancyArrowPatch, fancyarrow seems the best viable name.
@JosephBARBIERDARNAL
Copy link
Contributor

JosephBARBIERDARNAL commented Mar 30, 2025

Hi there,

FYI I've created a package dedicated to this purpose: https://github.com/JosephBARBIERDARNAL/drawarrow

You can find the most relevant function doc here: https://josephbarbierdarnal.github.io/drawarrow/reference/ax_arrow/

In my opinion, as a heavy matplotlib user and someone who uses arrows a lot, I really feel that my API is close to what I would expect in "native" matplotlib (fig_arrow() for an arrow on a Figure and ax_arrow() for an arrow on an Axes).

I'm not sure my code will help you in practice but TLDR: I've created the FancyArrowPatch wrapper and I have the impression that the user can make any type of arrow he wants intuitively.

And since you're planning to work on this, here are a few things I particularly like about my package that I think would be nice to have by default in matplotlib, which might give you some ideas:

  • the double_headed argument: a boolean that defines whether the arrow should have 2 heads or not
  • the fill_head argument: a boolean that defines the head style (-> or -|>)
  • the ability to add an inflection point by specifying its position rather than the angles -> it's far too difficult to think about angles when you want the inflection point to be in a specific place. In practice, my package doesn't correct the problem perfectly because the position sometimes remains odd, I'm not sure why (see this issue if you're interested: inflection point are not well located JosephBARBIERDARNAL/drawarrow#15).

Please let me know if you have any question, I'm particularly interested in this topic.

@story645
Copy link
Member

story645 commented Mar 30, 2025

Something like 90% of the arrows I draw using annotation are using the relative positioning to object support from annotation, and those transforms are currently computed in annotation rather than FancyArrowPatch. There's a proposal to factor out the annotation coordinate handling and I think it'd have to be implemented for the fancyarrow function to be really useful : #22223 (comment)

For simple data coordinate arrows, I think the vector proposal should be revived and we should come to some hard decisions there.

the ability to add an inflection point by specifying its position rather than the angles

I also really wanted to be able to set the midpoint on the bezier and the coordinate system 'cause this is messy when spanning 3 coordinate systems.

@timhoffm
Copy link
Member Author

timhoffm commented Mar 31, 2025

@JosephBARBIERDARNAL thanks for the input. With fancyarrow() I plan to stick more closely to the FancyArrowPatch API than you do in drawarrow. It should be a lightweight wrapper without additional logic. I also want to support all arrowstyles and connectionsstyles, so I think I cannot make the opinionated choices you've done to provide the high-level interface double_headed/fill_head/inflection_point.

For arrowstyle, I believe "->" / "-|>" / "<->" is as good as double_headed/fill_head. For the inflection point, one would have to see whether one can have an alternative parametrization of the "angle3" connection style. But that would have to be solved in that class.

@story645 while reasonable, I believe we can add Artist support later. I believe this is already an improvement even if we don't support artists as reference points right away:

@story645
Copy link
Member

which is an awkward workaround - and arrow() currently also only supports coordinates.

That's sorta my point though - what's the advantage of adding a a fancyarrow method vs sorting out the vector API. Down the line, do we actually expect/want the examples to differentiate?

@timhoffm
Copy link
Member Author

timhoffm commented Mar 31, 2025

I believe they are separate use cases:

  • fancyarrow(): "a directed visual connection as decoration" - only a single arrow, very detailed configuration (arrowstyle, connectionstyle), possibly using Artists as reference points
  • vector(): "a directed connection as data" - a collection of arrows - possibly many arrows with one call, possibly different colors etc, reference points are data coordinates (not Artists), only straight connections (possible, but to be discussed: participation in color cycle, possibility to add this to legend)

@story645
Copy link
Member

story645 commented Mar 31, 2025

fancyarrow(): a directed visual connection as decoration"

I think of ConnectionPatch as the "connect things" artist, and I think it's already got the API for comprehensive coordinate support. (xref #26440)

@timhoffm
Copy link
Member Author

Ok so what is your proposal? Should fancyarrow() (or other name t.b.d.) rather return a ConnectionPatch?

@story645
Copy link
Member

story645 commented Mar 31, 2025

Should fancyarrow() (or other name t.b.d.) rather return a ConnectionPatch?

I'm not sure it should exist, but if it does then yeah I think wrapping ConnectionPatch makes more sense if the semantics are connection oriented. Make fig.connect and ax.connect and that means all the examples of manually adding the artist can be swapped out w/ function calls.

@timhoffm
Copy link
Member Author

timhoffm commented Apr 1, 2025

Ok I yield for now. My thought was basically "let's make FancyArrowPath available as a function, so that we don't have to recommend the awkward use annotate() without text to create an arrow".

That in itself is reasonable. The question arises whether additional functionality should be included. #22435 suggests a bunch of these to better support data-like arrows. OTOH we have Artist references points (as defined in annotate()) and reference points in different coordinates and Axes as defined in ConnectionPatch.

I still think the distinction between data-like and annotation/decoration-like is reasonable and we should have two separate functions on that because we cannot squeeze all the above "arrow" extensions into one API. I consider vector() the potential data-like arrow API. Whereas this here is intended for the decoration-like arrow API.

However, I don't have the capacity to think about the extensions in that direction right now. It might be that connect() is the better approach rather than fancyarrow(), but it doesn't feel good. "arrow" is nicely concrete whereas "connect" is more abstract due to its generality. I'm also unsure whether ConnectionPatch and FancyArrowPatch are really separate concepts or whether they should actually be the same class.

@timhoffm timhoffm closed this as completed Apr 1, 2025
@story645
Copy link
Member

story645 commented Apr 1, 2025

I'm also unsure whether ConnectionPatch and FancyArrowPatch are really separate concepts or whether they should actually be the same class

ConnectionPatch is basically a wrapper on FancyArrowPatch that handles the coordinate transforms. Which, probably would be best to keep separate if FancyArrowPatch is also gonna act as the base artist for vector (I'm not sure if Arrow or FancyArrow was decided on as the base).

class ConnectionPatch(FancyArrowPatch):

I still think the distinction between data-like and annotation/decoration-like is reasonable and we should have two separate functions on that because we cannot squeeze all the above "arrow" extensions into one API

I don't totally disagree, I just think ax.annotate("", ...) is not the worst API and there's a lot of functionality there (ax.connect would be ax.annotate w/o the text options) and that having a clean definition for the data arrow API will help in defining the functionality for the annotation arrows.

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

No branches or pull requests

3 participants