Skip to content

Vector#22435

Draft
kmdalton wants to merge 9 commits intomatplotlib:mainfrom
kmdalton:vector
Draft

Vector#22435
kmdalton wants to merge 9 commits intomatplotlib:mainfrom
kmdalton:vector

Conversation

@kmdalton
Copy link
Copy Markdown

@kmdalton kmdalton commented Feb 9, 2022

PR Summary

This Draft PR is a first pass at implementing a vector method (see #22390) for the Axes class. Ideally someone can build on this to make a viable plt.arrow successor.

It uses FancyArrowPatch to draw the arrow. Right now the ArrowStyle is limited to "simple". It would be trivial to support "fancy". Other ArrowStyles will be more challenging as they diverge in parameter names.

This version supports legend labels out of the box through the Axes patch handler, and it automatically adjusts the axis limits. The arrow sizing can be adjusted in points. The scaling logic is based on the FancyArrowPatch code in plt.Annotate here.

Much of the credit goes to @ianhi for working out the details.

PR Checklist

Tests and Styling

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (install flake8-docstrings and run flake8 --docstring-convention=all).

Documentation

  • New features are documented, with examples if plot related.
  • New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).

Copy link
Copy Markdown

@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 while, please feel free to ping @matplotlib/developers or anyone who has commented on the PR. 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.

Comment on lines +6807 to +6812
ms = vect._mutation_scale
stylekw = {
"head_length": kwargs.get("head_length", 12) / ms,
"head_width": kwargs.get("head_width", 12) / ms,
"tail_width": kwargs.get("tail_width", 4) / ms,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For context this numbers are based on how annotate styles it's arrows.

@story645 story645 added status: needs workflow approval For PRs from new contributors, from which GitHub blocks workflows by default. topic: arrow New feature labels Feb 9, 2022
else "list[Polygon]")
return tops, bins, cbook.silent_list(patch_type, patches)

def vector(self, dx, dy, x=0., y=0., **kwargs):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
def vector(self, dx, dy, x=0., y=0., **kwargs):
def vector(self, x, y, dx, dy, **kwargs):
  • We want to keep the API similar to arrow()
  • While from a mathemetical point of view, you can have a direction vector only specifying dx, dy; but we need to plot it somewhere and there is no reasonable default. I don't think there is a real use case where you are not interested how the arrow is positioned relative to other plot elements.
  • If you have all four parameters x, y, dx, dy is the natural order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This may be an peculiarity of my field, but crystallographers frequently plot arrows in the complex plane that are by default rooted at zero. As evidence that I am not alone, have a look at an image search for Argand diagram. From my perspective, x=y=0 is a sensible default. I can understand how this might not be universal.

I find vector(tail_x, tail_y, head_x, head_y, ...) is also a natural four-parameter representation. Is that one the table?

Copy link
Copy Markdown
Member

@jklymak jklymak left a comment

Choose a reason for hiding this comment

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

This seems a good start, but does not, so far as I can tell, allow for arrays of x, y, dx, and dy to be passed, whereas that is a natural thing to do. In which case you probably want a PolyCollection?

You also have not taken into account our units system - x, dx etc need to be able to take datetime and other units, and needs tests for these. Maybe see errorbar (?) for an example of deltas being used with the unit system.

@kmdalton
Copy link
Copy Markdown
Author

@jklymak , I don't think plt.vector needs to support arrays for the following two reasons

  • plt.arrow does not
  • plt.quiver exists for this purpose

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Feb 10, 2022

Indeed plt.quiver provides this functionality, so why do we need vector? It's easy enough to pass a single x, y, u, v to quiver.

@ianhi
Copy link
Copy Markdown
Contributor

ianhi commented Feb 10, 2022

Indeed plt.quiver provides this functionality, so why do we need vector? It's easy enough to pass a single x, y, u, v to quiver.

I tried this out a bit and quiver is unfortunately not good substitute. Trying to replicate vector requires two extra kwargs and still doesn't replicate it. It also doesn't follow the color cycle

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()

x = .5
y = .5
dx = 1
dy=10
ax.vector(dx, dy, x ,y, label='vector')
shift = .05
ax.quiver(x+shift,y+shift, dx, dy, angles='xy', scale=np.linalg.norm([dx,dy]), label='quiver')
plt.legend(loc=2)
plt.show()

gives
image

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Feb 10, 2022

Ok then, there is probably a need for vector to accept more than one set of data

@timhoffm
Copy link
Copy Markdown
Member

We've discussed this method in the dev call: https://hackmd.io/jd_7FjxNQ4y7XgNknvmvGQ#vector-API

Basic ideas:

  • vector() is a replacement for arrow() and thus try to follow its API where possible.
  • vector() is a method wrapper for creating a FancyArrowPatch.

Summary:

  • we want vector(x, y, dx, dy, **kwargs); all kwargs are passed through to FancyArrowPatch. The non-kwarg parameters match arrow().
  • vector should only accept and draw one arrow. Accepting more causes some issues either way. You get either return type instability or always have to return a list of FancyArrowPatches, which is awkward for the common case that you only draw one. Note also that PatchCollection does not work with FancyArrowPatch (and this is not easily fixable).
  • There's been a hot discussion whether vector should follow the color cycle or not (and if so the line or the patch color cycle) (and if not should there be an additional `rcParams['vector.color'] to make it configurable.

Long story short, the color topic needs more investigation, we haven't reached a decision yet.

@kmdalton
Copy link
Copy Markdown
Author

As a user, I would be very frustrated if plt.vector did not follow the color cycle. I feel like this is a sensible default and can always be overridden through the "color", "ec", and/or "fc" kwargs. I guess I am missing the counterargument.

@ianhi
Copy link
Copy Markdown
Contributor

ianhi commented Feb 11, 2022

There's been a hot discussion whether vector should follow the color cycle

I second @kmdalton that it should follow a color cycle. I find methods that don't to be extremely frustrating.

or not (and if so the line or the patch color cycle)
Long story short, the color topic needs more investigation, we haven't reached a decision yet.

Probably not best discussed here but I've always wished that there was one global color cycle. It's disconcerting that scatter, plot and hist each have their own color cycle. That aside, I would advocate for the plot color cycle.

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Feb 11, 2022

If we are only making one arrow at a time, then I'm not convinced adding a new method is worth it just for the autolim and colorcycle behaviour. The colorcycle is dubious; I don't understand why we would want single arrows cycling their colors by default (groups of many arrows, sure). Similarly if I'm just adding one arrow at a time I typically have other data that I have plotted and that data set the limits for me.

@kmdalton
Copy link
Copy Markdown
Author

If we are only making one arrow at a time, then I'm not convinced adding a new method is worth it just for the autolim and colorcycle behaviour. The colorcycle is dubious; I don't understand why we would want single arrows cycling their colors by default (groups of many arrows, sure). Similarly if I'm just adding one arrow at a time I typically have other data that I have plotted and that data set the limits for me.

Here's a plot I made last week using plt.arrow to demonstrate complex addition for my students.

image

I had to manually specify colors, limits, and otherwise override the broken default kwargs in plt.arrow. The whole process was frustrating and time-consuming. As you've noted, @jklymak , plt.arrow doesn't use FancyArrowPatch so the result isn't very aesthetically appealing either.
With the version of vector in this PR, you can already do this sort of plot in just a few lines with mostly default params.
image

import matplotlib.pyplot as plt
import numpy as np

F1, F2 = np.random.random(2) + 1j*np.random.random(2)

fig, ax = plt.subplots()

dx,dy,x,y= np.real(F1),np.imag(F1),0., 0.
ax.vector(dx, dy, x, y, label='F1')

dx,dy,x,y= np.real(F2),np.imag(F2),np.real(F1),np.imag(F1)
ax.vector(dx, dy, x, y, label='F2')

dx,dy,x,y= np.real(F1+F2),np.imag(F1+F2),0., 0.
ax.vector(dx, dy, x, y, label='F1+F2')

plt.xlabel(r"$\mathbb{Re}$", size=16)
plt.ylabel(r"$\mathbb{Im}$", size=16)
plt.grid(ls='-.')
plt.legend()
plt.show()

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Feb 11, 2022

The relevant comparison is not plt.arrow which we all agree is broken, but plt.annotate.

I find your diagramming use case very specialized that you could easily write a little annotate wrapper for. I don't feel it justifies a whole new api element. But I'm just one vote. If others on the dev team think this is a good way to go, I'm not going to block it (unless it lacks unit support)

@timhoffm
Copy link
Copy Markdown
Member

I'm still not clear what the right way forward is. Let's maybe take one step back from what vector should do

Basic ideas:

  • vector() is a replacement for arrow() and thus try to follow its API where possible.
  • vector() is a method wrapper for creating a FancyArrowPatch.

to what is needed:

  1. Current users of arrow() need an alternative, that is reasonably simple to switch to.
  2. It should be possible to create a single FancyArrowPatch arrow using a function.
  3. There is a desire to be able to draw a set of arrows at once.

Discussion

RE 3: is out of scope here. Likely this wants a collection-type Artist, which we currently don't have for arrows (it was mentioned in the dev call that PatchCollection does not work with FancyArrowPatch, and that's not easily fixable). So let's save that for later.

RE 2: "To create an arrow, create an annotation without text." is too convoluted. Semantically, a data arrow and an annotation are different things (and they may have different behaviors, e.g. concerning color-cycling). We need a dedicated function. Whether that'd be vector or fancy_arrow.

RE 1: I'm pulling back a bit on the need to follow the arrow() API, as long as everything is still possible and it's reasonably simple to switch to the new API.

@timhoffm
Copy link
Copy Markdown
Member

We've discussed this extensively again in the dev call today: https://hackmd.io/jd_7FjxNQ4y7XgNknvmvGQ#Arrow

Conculsions:

  • Definitly not doing multiple here. This can be done with quiver in a reasonable way but definitly needs to be better documented.
  • Change signature to def vector(self, start, *, delta=None, end=None)
    • require exactly 1 of delta or * end*
  • should also add the start, end, delta setter/getters to FancyArrowPatch
  • color cycle: yes and line cycle
  • legend: yes
    we at least check and document what these do or do not get picked up by plt.legend(['a', 'b', 'c'])

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Feb 19, 2022

Sorry to miss the discussion - however, what was the argument for vector, other than to replace arrow? For multiple arrows we have quiver; for single arrows we have annotate. Our API problems are usually caused by too many similar ways to achieve the same thing (see pcolor,pcolorfast, pcolormesh), so I still don't understand why we want to add something that is basically just annotate with no option for accompanying text.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Feb 19, 2022

for single arrows we have annotate.

As discussed above "To create an arrow, create an annotation without text." is awkward and not intuitive/discoverable. Moreover, they are conceptually different. Annotations are decorations, while we came to the conclusion that vector arrows should be considered as data. This implies that vector arrows take part in color cycling and can be added to the legend.

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Feb 19, 2022

This will be the only place in the library where "data" is only allowed to be represented by one set of values per method call.

@timhoffm
Copy link
Copy Markdown
Member

We don't have a suitable artist for multiple FancyArrowPatch (it doesn't work with PatchCollection because it needs special draw-time logic: an positioned in data space but it's shape must be drawn in screen space. 🤷 There's always the option to extend vector() later, but let's start small for now.

@story645 story645 removed the status: needs workflow approval For PRs from new contributors, from which GitHub blocks workflows by default. label Jan 24, 2025
@melissawm
Copy link
Copy Markdown
Member

Hi folks- just coming around to see if there's a way forward for this. Thanks!

@melissawm melissawm added the status: needs comment/discussion needs consensus on next step label Mar 6, 2026
@ianhi
Copy link
Copy Markdown
Contributor

ianhi commented Mar 6, 2026

I think that there are good arguments for all the behaviors discussed in this thread. So as I see it, the next step needs to be matplotlib as a project (i.e. core devs) come to a concrete conclusion as to what the behavior/API should be. I'm happy to take over this PR and implement whatever that decision is, but cannot make it myself.

Reading back more carefully I see that it has been very hard to reach true consensus. And also to define what counts as consensus (as not everyone can make the same dev meetings). Not entirely sure how to resolve that, but I standby that I will implement whatever once there is a definitive decision.

My one request is that whatever the decision to e.g. default to black arrows, that there be a best effort made to preserve an easy way to opt-in to the other behavior such as color cycling/legend. Otherwise for our use case will just have to continue to use mpl-arrow

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 7, 2026

I asked AI to summarize the discussion. I hope it's not too spammy. It's not perfect, but I think this gives a good starting point into the discussion after we all lost track of the topic.

Edit / note: The summary is actually not that great. I believe it touches most of the relevant topics, but it's a bit unstructured, not precise and at times opinionated.

prompt

There a discussion on the API of a potential Vector class in matplotlib. This should draw one or multiple arrows. Read the discussion thread: #22435 (comment) Ignore the actual pull request content and only focus on the discussion. Summarize which API options have been considered. Then, list which aspects have seen consensus and which are still under discussion


Below is a structured summary of the API discussion in the GitHub thread you referenced (ignoring the PR implementation and focusing only on the discussion). The discussion centers on designing a new vector API in Matplotlib intended as a replacement for plt.arrow().


Detailed LLM summary

1. API Options That Were Considered

1.1 Parameter ordering and representation of a vector

Option A — Same API as arrow

vector(x, y, dx, dy, **kwargs)

Meaning:

  • (x, y) = tail/start point
  • (dx, dy) = displacement

Motivation:

  • Consistent with Axes.arrow
  • Familiar to users
  • Clear mapping to existing Matplotlib usage

This option was explicitly recommended by maintainers as the preferred API.


Option B — Direction-first vector

vector(dx, dy, x=0, y=0, **kwargs)

Meaning:

  • (dx, dy) define the vector
  • (x, y) optional origin (default (0,0))

Motivation:

  • Mathematical interpretation of a vector independent of position
  • Useful in contexts like Argand diagrams (complex numbers)

Criticism:

  • Plotting requires a position anyway
  • Default origin may be arbitrary for many plots

Option C — Two-point representation

vector(x0, y0, x1, y1, **kwargs)

Meaning:

  • Tail point (x0, y0)
  • Head point (x1, y1)

Motivation:

  • Intuitive geometric representation
  • Direct specification of arrow endpoints

Drawback:

  • Inconsistent with arrow()
  • Slightly less convenient when vector direction is naturally (dx, dy)

2. Number of Vectors per Call

Option A — Single arrow only

vector(x, y, dx, dy)

Arguments:

  • Simpler return type (always one FancyArrowPatch)
  • Matches arrow()
  • Avoids return type ambiguity

Concerns raised:

  • Some users naturally want arrays.

Maintainer conclusion:

  • Prefer single arrow to avoid API complexity.

Option B — Allow arrays

vector(x, y, dx, dy)  # each could be arrays

Motivation:

  • Natural vector plotting workflow
  • Similar to quiver

Counterarguments:

  • quiver already handles vector fields
  • Implementation complexity
  • FancyArrowPatch does not integrate well with PatchCollection

Thus arrays were considered but largely discouraged.


3. Relationship to Existing APIs

Several alternatives were discussed:

Using quiver

Pros:

  • Already supports vectors

Cons:

  • Hard to reproduce a simple single arrow
  • Requires additional parameters
  • Does not follow color cycle
  • Different scaling semantics

Conclusion: not a good replacement for a simple arrow/vector function.


Using annotate

Pros:

  • Already supports arrows via FancyArrowPatch

Cons:

  • Verbose API
  • Designed for annotations, not vector drawing

Thus the motivation for a dedicated function remained.


4. Keyword Handling

Proposed behavior:

vector(..., **kwargs)

All kwargs passed directly to:

FancyArrowPatch

Reasons:

  • Reuse existing arrow styling
  • Avoid designing a new arrow style API

5. Color Behavior Options

A major unresolved topic.

Option A — Follow the color cycle

Pros:

  • Consistent with plotting APIs
  • Convenient when drawing multiple vectors

Some developers strongly supported this.


Option B — Do not follow the color cycle

Arguments:

  • If only one arrow is drawn, cycling colors is odd
  • Users often explicitly specify arrow color

Option C — Dedicated rcParam

Example idea:

rcParams["vector.color"]

But this adds configuration complexity.

Outcome: no decision reached.


6. Implementation Basis

General agreement:

  • Implementation should use FancyArrowPatch.

Benefits:

  • Consistent arrow geometry
  • Better styling
  • Same backend used by annotate

7. Additional Concerns Raised

Units support

x, dx, etc. must work with:

  • datetime
  • other Matplotlib unit systems

Example mentioned: similar to how errorbar handles deltas.


Axis limits behavior

Desired behavior:

  • Automatically update axis limits when arrows are added.

8. Consensus vs Open Questions

Areas with Clear Consensus

1. A replacement for arrow() is needed

arrow() has known problems (e.g., axis-scale distortion). ([GitHub]2)


2. Use FancyArrowPatch internally


3. Prefer API compatible with arrow

Final preferred signature:

vector(x, y, dx, dy, **kwargs)

4. Pass styling kwargs to FancyArrowPatch


5. Purpose of vector

A simple way to draw a single arrow/vector without the complexity of annotate.


Still Under Discussion

1. Color-cycle behavior

Questions:

  • Should arrows follow the default color cycle?
  • Which cycle (line vs patch)?
  • Should there be a new rcParam?

2. Whether the function should support arrays

Options debated:

  • single arrow only
  • multiple arrows
  • rely on quiver

3. Whether the new API is necessary at all

Some developers argued:

  • annotate could suffice
  • a wrapper could be written by users

4. Exact argument order defaults

Especially around:

  • default (x, y) = (0,0)
  • alternative representations (x0,y0,x1,y1)

One-Sentence Summary

The discussion largely converges on vector(x, y, dx, dy) as a simple FancyArrowPatch wrapper replacing arrow(), but debates remain about color cycling, multi-arrow support, and whether the API is justified vs existing tools like annotate or quiver.


@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 8, 2026

Premise

I think we basically agreed on this, so I take this for granted:

  • We want a dedicated method to draw arrows; semantically a replacement for arrow();
    exisiting methods like annotate or quiver have a different scope and are not sufficient
  • FancyArrowPatch is the suitable underlying artist because of its flexibility

Open topics

From my point of view, these are the open topics:

  1. Naming
  2. Parameters / how do we encode the arrow
  3. Single or multiple arrows
  4. Should arrow take part in the color cycle

My opinion on the open topics

  1. Naming vector() is a working assumption. Not urgent, but t some point, we should think whether there's a better name. Ideally, we'd want arrow(), but that is taken by the old implementation and cannot be reused. Is vector() the second best name?

  2. Parameter ordering and representation of a vector: The overall trend was going towards vector(x, y, dx, dy) to be API-similar to arrow() - I call this "similar" not compatible, because it only works for these parameters. If you have kwargs, they will change anyway.
    I'd like to propose vector(from, to) with each being a tuple; exact names t.b.d. Motivation:

    • This structure is the same as FancyArrowPatch (which calls it posA, posB). There is value in keeping the logic in line with the underlying Artist.
    • Draw an arrow from A to B seems like the most common and simple specification of an arrow. Draw an arrow from A with size (dx, dy) feels not that intuitive (my personal opinion). Two points is symmetry. With x, y, dx, dy why do we specify the start point. It could alternatively be the end point (which may even be the more relevant point of the two because this is the point the arrow is pointing to).
    • Leaving out the direction makes units easier. dx, dy would have delta-units which we tend to not support that well.

    I believe these points outweigh the API-similarity arguments.

  3. Single or multiple arrows Let's do a single one. Arguments:

    • Keep it simple. - This makes the API simpler, as we are mostly wrapping FancyArrowPatch as is and can forward all parameters.
    • FancyArrowPatch itself is single.
    • I expect this not to be used for a lot of arrows. If the need is there, users can call this in a loop, or write a small helper.
    • If many arrows are needed, probably a new "ArrowCollection" should be created. We can always do that later and create a plural method, e.g. vectors().
  4. Should arrow take part in the color cycle? That depends on the interpretation: If they are considered "data" they should take part. If they are rather "decoration", they shouldn't. There's no universal answer, however I suppose the tendency is more towards "decoration".
    In that sense, I propose they should not take part in the color cycle.
    One argument for the color cycle was that if you are in by default it's easy to opt-out by specifying a color, but if you are not in by default, it's cumbersome to assign multiple arrows with color cycle colors. The solution here is to make opting into the color cycle easier, e.g. by developing "special color names". We have "C0", "C1", ... We could create something like "Cnext". But that should be a separate / follow-up activity. For a start, users should explicitly set the colors. They can either use "C1", "C2" ... or get the color cycle colors in a loop.

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Mar 8, 2026

If we are making it be a single arrow, I'd agree with vector-ish(start, stop) in analogy to annotate(..., xy, xytext, ...). I think since this is obviously a subset of annotate's functionality, it should follow the annotate public API as closely as possible for everything, so that ideally annotate would just use vector-ish.

As for the name, I'd actually do annotate_arrow so that it is clear it is the same thing as annotate, without the text.

I'm not a fan of vector as that has a lot of connotations beyond just drawing an arrow.

I strongly feel this should not be part of the color cycle.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

, it should follow the annotate public API as closely as possible for everything, so that ideally annotate would just use vector-ish.

I think that's the whole problem though, far as I can tell what @ianhi and @kmdalton are after here is explicitly not annotate w/o text, which I think @timhoffm basically proposed at #29826

Also a lot of the power of the annotation arrow is the transforms, which is handled by ConnectionPatch.

  1. Is vector() the second best name?

I think that's fundamentally the sticking point, is this function supposed to have vector(data) or connect (annotate) semantics?

  1. vector(from, to)/ vector(x, y, dx, dy)

Maybe the API difference of connect(src, dest) and vector(x, y, dx, dy) is part of the underlying semantic difference/difference in use cases between the two functions?

  1. If many arrows are needed, probably a new "ArrowCollection" should be created.

Or we write a better tutorial for quiver (or maybe a wrapper over quiver?)

  1. That depends on the interpretation: If they are considered "data" they should take part. If they are rather "decoration", they shouldn't.

Which is why I think vector should if it's the replacement for the arrow that was a data method. ETA: B/c I think if you want an arrow for decoration, that's annotate w/o text.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 9, 2026

I think the main underlying difficulty is the "data" vs. "annotation" topic. If we limit us to one, most of the properties have obvious choices.

Both are legitimate use case. We need to decide:
a. Do we want to limit us to one perspective.
b. Do we want to create two separate functions, e.g. data_arrow(), annotation_arrow().
c. Do we want to have cover both with a single method (which will need to make some decisions on defaults, but can fundamentally cover both aspects.

I think we should do either b. or c.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

think we should do either b. or c.

I strongly think we should do b as they're two distinct use cases and the attempts to do c. is:
i) why we can't get to consensus here
ii) how we land at a function (no participating in color cycle, doesn't match arrow API) that doesn't fit the needs of the original ask

@ianhi
Copy link
Copy Markdown
Contributor

ianhi commented Mar 9, 2026

b also seems the easiest to me. a few ideas for names of either of the potentially two new functions:

data like
vector
ray
delta
arrow_data
data_arrow

annotation like
arrow_annotation (has the advantage that shfit-tab will lead you. to consider this over arrow
arrow_guide
pointer
callout

I think the main underlying difficulty is the "data" vs. "annotation" topic. If we limit us to one,

I think the other was teh API for a data arrow. two points vs dx/dy. is it worth considering 3 functions?
data(x1, y1, x2, y2)
data(x,y, dx, dy)
annotation(x1,y1, x2, y2)

that would be the easiest to resolve, but adds the emost API surface to maintain.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 9, 2026

I think the other was teh API for a data arrow. two points vs dx/dy. is it worth considering 3 functions?

I would go with tuples, not flat coordinates. Four values in a row are harder to interpret than two tuples. With that, you can reasonably overload the signature. https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.axline.html#matplotlib-axes-axes-axline is a precedent in that you can specify either a second point or a slope.

Here you could do

def data_arrow(from, to=None, *, direction=None):
# allowing
data_arrow((1,0), (1, 1))
data_arrow((1,0), to=(1, 1))
data_arrow(from=(1,0), to=(1, 1))
data_arrow((1,0), direction=(0, 1))

or

def data_arrow(*, from=None, to=None, direction=None):
# allowing
data_arrow(from=(1,0), to=(1, 1))
data_arrow(from=(1,0), direction=(0, 1))
data_arrow(to=(1, 1), direction=(0, 1))

Not saying that we have to do this, but I would prefer that over two data functions. The trade off between the two is that you can use positional parameters in the first case (may be more convenient than always needing to give the parameter names), whereas in the second case, you can freely choose two out of (from, to, direction).

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 9, 2026

I’m still not clear whether we need the two variants (b) or whether one variant can also work (c).

What differences would we make?

  • Participation in the color circle
  • Maybe different position signature (end vs direction) - though that is in principle solvable with one function as shown above.

Anything else?

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

Anything else

I think participation in the color cycle has been a major sticking point, but practically the big one is probably that an annotation arrow should support the annotation coordinate system while that's a lot of overhead for the data arrow.

ETA: basically, data arrows should always have all points in the same transform, defaulting to data. Annotation arrows benefit greatly from the freedom of specifying head and tail in different coordinate systems, especially the relative positioning logic.

ETA2: b/c GitHub just brought up this comment, data should participate in legends while annotations shouldn't.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

Poking even more on the transforms being a huge part of the story, if IndirectTransform goes in:

  • data arrow should position the whole arrow relative to an artist -> end/dx+dy share start's transform
  • annotation arrow should still explicitly treat start and end as independent, so setting IndirectTransform on start should not effect end's positioning.

I'm not even sure how dx/dy gets computed if the start and end have different transforms.

ETA: used IndirectTransform b/c it's a proposed GSOC, but this problem arises just out of supporting annotation coordinates.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 9, 2026

So we are at the following differences between data and annotation arrows:

data annotation
use color_cycle yes no
support individual transforms for start/end not needed yes
position encoding start, direction or start, end start, end

Notes:

  • Legend is not a limiting factor. The universal legend rule is sufficient: If a label is given, add the artist to the legend, if not, leave it out.
  • the current individual transfrom support of annotate() is encoded in matplotlib.text.Annotation and therefore tightly coupled to text annoations. FancyArrowPatch itself does not support this. We'd need to investigate how to make this available best for an arrow annotation. - also to be determined: Do we need to support this right from the start, or can we safely design this in such a way that we can add it later?

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Mar 9, 2026

So we are at the following differences between data and annotation arrows:

Except for the color cycle, the "data" in the above is just a basic subset of the "annotation" column.

Edit: I guess except for the trivial math in start/end versus start/dx.

matplotlib.text.Annotation and therefore tightly coupled to text annoations.

That is currently the way that Annotation is written, but I don't think the coupling is very complicated.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

Do we need to support this right from the start, or can we safely design this in such a way that we can add it later?

To me that's the entire utility of annotation arrow/what I use it for.
ETA: also the easiest path here is build on connection patch, which I think currently supports everything but the relative artist positioning (which admittedly is also about 95% of my use case).

Also strictly speaking, I think data should not support different transforms for start and end - I think it's a hard constraint that data is specified in the same units. ETA: kinda like how scatter and line can't do per point transforms.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

but I don't think the coupling is very complicated.

Decoupling is basically the first step of the proposed IndirectTransform GSoC, and the IndirectTransform solves the problem of patching in relative positioning into ConnectionPatch.

This PR of course doesn't have to wait on GSOC, but also this architectural stuff is part of why I think data and annotation have fundementally different scope/technical solutions.

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Mar 9, 2026

but also this architectural stuff is part of why I think data and annotation have fundementally different scope/technical solutions.

One is a strict subset of the other, isn't it? I would guess 75% or so of uses of annotate are from data-data space?

I'm not familiar with the GSoC plan or what you are calling IndirectTransform, but Annotation already allows both ends of the arrow to be different transforms, so the problem is solved, it just needs a bit of refactoring.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

Also I'm a little frustrated here b/c I don't follow how a new arrow function that doesn't follow the color cycle and follows annotation semantics isn't mostly just:

def ann_arrow(src, dest, src_coords=None, dest_coords='data', arrowprops=None, annotation_clip=None):
       annotation = self.ax.annotate("", dest, src, dest_coords, src_coords, arrowprops, annotation_clip)
       return annotation.arrow_patch

The other annotate kwargs are forwarded to text, and unwinding arrowprops is kind of a mess b/c it's based on arrow type.

And sure that's (maybe) solving a problem, but I don't think that's solving the drawing vectors problem that motivated this PR.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

One is a strict subset of the other, isn't it?

venn diagram, as demonstrated by the differences in Tim's table. data and annotate differ on the parameters specific to their use cases, which is kind of similar to imshow and matshow being different parameterizations of AxesImage. Or ConnectionPatch being FancyArrowPatch + coordinate transforms

I would guess 75% or so of uses of annotate are from data-data space?

Maybe, but almost none of mine. I'm pretty sure I just used arrow when I made arrows in data-data space, and then quickly realized I was using the wrong function b/c what I really needed was the positioning flexibility of annotate.

Annotation already allows both ends of the arrow to be different transforms, so the problem is solved,

Except I think we should strictly not support this in the data arrow case b/c the expectation in the data case is that everything is in the same transform.

not familiar with the GSoC plan or what you are calling IndirectTransform

One of the proposals for this years GSoC1 is to:

  1. refactor all the positioning logic out of annotation into a standalone function/utility/API
  2. pull out the position relative to artists logic into a new transform called IndirectTransform
  3. weave this into places where it'd be appropriate to support, like ConnectionPatch.

Footnotes

  1. https://github.com/matplotlib/matplotlib/wiki/Matplotlib-GSoC-2026-Ideas#indirect-transforms

@jklymak
Copy link
Copy Markdown
Member

jklymak commented Mar 9, 2026

A new annotate arrow function would indeed only remove the text part of annotate.

Again to summarize my position: I do not understand why you would want to have a special data->data arrow where the only extra feature is that it participates in color cycling, and it is much more restricted otherwise. It should either

  1. behave like annotate, or
  2. allow multiple vectors in the same call, perhaps as a simplification of quiver.

But everyone should do what they want here. I don't think it's the end of the world to have one-off data->data arrows where each data point is plotted via a separate call. I just think it's crufty when I can already do the same thing with annotate.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 9, 2026

Also I'm a little frustrated here b/c I don't follow how a new arrow function that doesn't follow the color cycle and follows annotation semantics isn't mostly just

That may well be

The broader topic here is that this all runs in the wake of substituting plt.arrow(). IMHO it's a major realization that people use arrows either as data or as annoations and that both are valid use cases that must be considered. While annotate / Annotation can draw an annotation arrow, it is not the right semantic API, because it's text-focussed. That justifies looking also at an annotate_arrow API.

On the data arrow API, I just note that the existing plt,arrow() is far from usable, not only because of its distortions, but also because the end point is the head base, not the tip and it does not take part in color cycling.

example plot
plt.arrow(0, 0, 1, 1, width=0.03)
plt.arrow(1, 1, 1, -1, width=0.03)
plt.arrow(2, 0, -2, 0, width=0.03)
plt.grid()
image

So both parties have desires on an improved API. I would like to discuss both in proximity, because I still see a chance that one function can suffice. Please do not prematurely argue this cannot work because of ... Let's put the desired properties side by side, as I've started above.

On a side note, I have been thinking about a more formal separation between data and decoration artists, but I'm now of the opinion that it's a spectrum. There are clearly data-only and decoration only cases, but there's also middle ground. This has to do with defining "data" is difficult. Is it that is purely (or partially) in data coordinates, is it that it can appear in a legend. Also the same Artist, e.g. a Line2D, can be used for data or decoration. So thinking about data or decoration use cases is good, but it's not necessarily helpful to encode this in the API.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Mar 9, 2026

I just think it's crufty when I can already do the same thing with annotate.

We have discussed this before. It's the wrong semantic model: "To draw an arrow, do a text annotation that uses an arrow style and leave out the text."
It may be that the solution is just the wrapper #22435 (comment), however from the architecture perspective, we want the arrow without the text overhead attached, which is a possible refactoring.

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 9, 2026

Is it that is purely (or partially) in data coordinates, is it that it can appear in a legend.

I define it based on the semantic assumptions: The data artist $Artist: data-> graphic$ must preserve the structure of the data in the graphic s.t roughly $Artist( change (data)) -> change(graphic)$ The data arrow plotting function has to preserve the structure of the vector when scaled or translated - which is why I think the transforms are so key - for it to be a valid visualization. It's more of a nice to have for annotation - users will be frustrated if it doesn't, sure, but we've so far not guaranteed that data can be encoded in annotation the way it's encoded in say scatter.

On the data arrow API, I just note that the existing plt,arrow() is far from usable, not only because of its distortions, but also because the end point is the head base, not the tip and it does not take part in color cycling.

That's why I support @ianhi and @kmdalton writing a data oriented replacement. I think the annotation side is just less urgent.

Let's put the desired properties side by side, as I've started #22435 (comment).

I think the differences are the sane defaults for each API, kinda like how we got to matshow/imshow:

data annotation
use color_cycle yes no
support individual transforms for start/end no yes
position encoding start, direction or start, end start, end
arrow properties fancy? simple or fancy?
base artist FancyArrowPatch ConnectionPatch

@story645
Copy link
Copy Markdown
Member

story645 commented Mar 10, 2026

Another way to think about this might be that the annotate "arrow" isn't really semantically an arrow. It's a line connecting the annotation to the thing it's annotating & that's why the start and end points are completely independent with respect to positioning/coordinate. The data arrow semantically is an arrow/vector - it's encoding a piece of data (often traditionaly represented as an arrow), which is akin to how a scatter mark encodes a point. And, like scatter, why the x/y axis values of the arrow head and tail are supposed to be semantically meaningful.

Where I think this discussion is getting stuck now is in what I see as a proposal to use the data arrow as the annotation itself. If Annotation didn't provide an arrow, you'd (in theory) use AnnotationBbox to annotate the point with the arrow artist.

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

Projects

Status: Needs decision

Development

Successfully merging this pull request may close these issues.

10 participants