-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Vector #22435
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
base: main
Are you sure you want to change the base?
Vector #22435
Conversation
There was a problem hiding this 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.
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, | ||
} |
There was a problem hiding this comment.
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.
lib/matplotlib/axes/_axes.py
Outdated
@@ -6777,6 +6777,45 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, | |||
else "list[Polygon]") | |||
return tops, bins, cbook.silent_list(patch_type, patches) | |||
|
|||
def vector(self, dx, dy, x=0., y=0., **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this 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.
@jklymak , I don't think
|
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 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() |
Ok then, there is probably a need for vector to accept more than one set of data |
We've discussed this method in the dev call: https://hackmd.io/jd_7FjxNQ4y7XgNknvmvGQ#vector-API Basic ideas:
Summary:
Long story short, the color topic needs more investigation, we haven't reached a decision yet. |
As a user, I would be very frustrated if |
I second @kmdalton that it should follow a color cycle. I find methods that don't to be extremely frustrating.
Probably not best discussed here but I've always wished that there was one global color cycle. It's disconcerting that |
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 I had to manually specify colors, limits, and otherwise override the broken default 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() |
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) |
I'm still not clear what the right way forward is. Let's maybe take one step back from what
to what is needed:
DiscussionRE 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 RE 1: I'm pulling back a bit on the need to follow the |
We've discussed this extensively again in the dev call today: https://hackmd.io/jd_7FjxNQ4y7XgNknvmvGQ#Arrow Conculsions:
|
Sorry to miss the discussion - however, what was the argument for |
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. |
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. |
We don't have a suitable artist for multiple |
A common complaint about Matplotlib is that there are too many ways to do the same thing, and the proposal here is to add another way to do something already accessible via |
Um so I don't think they're two ways to do the same thing, I think they're two different things (annotating and encoding1) that may generate the same visual element (an arrow). Kinda like how plot and vlines can both create vertical lines, or vlines+scatter = stem, or matshow is shifted imshow. And I think a lot of the confusion for users is also not so much in the multiple ways to do things, it's that we could be much clearer about which way matches their use case and therefore as part of the implementation has the defaults (eta: baked in semantic assumptions) they expect for their visualization task. For example, this new vector method would get an entry in plot types and maybe we should add a new section there and move all the annotation type plotting methods - spans, fills, annotate-there. Footnotes |
@story645 has clearly laid out the arguments why
This is generally a good idea. We‘ve started from there in the API discussion rounds. However we realized that a) the current arrow API has deficits and b) changed behavior is desirable (e.g. color cycling). Because of b) |
To my understanding this discussion happened at the weekly meeting and a consensus was reached as presented in #22435 (comment). Is this not the case? As a broader point: It's difficult to work on PRs like this if there isn't clarity about what constitutes a consensus. As an experienced contributor I find that I can stick with it, but were I newer then the uncertainty as to whether or not there has been a conclusion of if this feature will make it in in any form would be very difficult to deal with. I've just pushed an update that I think matches the spec as laid out in #22435 (comment) with the small exception that I only added a Happy to add tests once there is clarity that this is direction this PR should take. |
With current implementation this example code: 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((x,y), delta=(dx, dy), label='F1')
dx,dy,x,y= np.real(F2),np.imag(F2),np.real(F1),np.imag(F1)
ax.vector((x,y), delta = (dx, dy), label='F2')
dx,dy,x,y= np.real(F1+F2),np.imag(F1+F2),0., 0.
ax.vector((x,y), delta=(dx, dy), label='F3')
plt.xlabel(r"$\mathbb{Re}$", size=16)
plt.ylabel(r"$\mathbb{Im}$", size=16)
plt.grid(ls='-.')
plt.legend()
plt.show() gives: |
Historic note: We had at least two weekly meetings on the topic and positions have changed throughout and between the meetings (the topic is compicated and we have to balance different aspects). #22435 (comment) was the consensus of the last meeting. I don't remember exactly, but I think @jklymak has not been part of that meeting.
I definitively agree. This topic turned out to be more complex than we initially thought. We had a first decision on the API, which then was labelled "good first issue". Only after the PR came in and all details were mapped out, we partially realized that the initial decision wasn't quite right, which resulted in the following controversial discussion. |
Those arguments are based on "encoding" of the method for use case, which is completely subjective, and boil down to "I don't think of adding arrows that join data as annotating". I think we should guard against the API being expanded just to fit different mental models of how to add information to a plot. Expanding the API should add new non-niche actual functionality that is not otherwise available, or it should be significantly more convenient for a significant proportion of our users. What's more, I don't agree that adding arrows, one at a time, is not annotating the plot, regardless of the intention. By this logic we should also allow What I am arguing against is having the same object put on the plot by different APIs. If you need the autolim behaviour (which is the most useful in my opinion), that could be added to However, plotting one vector at a time and cycling through a colorcycle and adding the vectors to a plot is extremely niche. I have never seen a plot like that outside of a physics or math text book, and even then the vectors are typically the same color, and labeled in-situ rather in a legend. I don't mind, at all, Matplotlib being used for diagraming vectors this way, but I don't think it merits a new API when it can relatively easily be achieved via annotate. If you cannot remember that annotate is a great way to draw individual arrows, adding a helper method to your helper library is surely a nice way to work around awkwardness in the API if you don't like the defaults and you do something often enough. |
This is essentially a suggestion to grow the ETA: The point @timhoffm and I are trying to make, which you seem to support here, is that visualizing data as arrows has different requirements than drawing arrows for labeling; therefore one way to make this easier on the user (ETA: and maintainer) is to encode these different needs and expectations in different functions. |
I think this need‘s a live discussion to resolve. Unfortunately, I currently cannot commit to a dev call. You may discuss this without me. I support @story645‘s position. (Or we’ll have to defer this a bit- 3.8 is still somewhat away.) |
Again, I don't think this meets my criteria of what should be added to the API. If no one else is concerned about the cruft, you should feel free to move forward. |
I apologize for not making it to the calls these days (a bit overwhelmed with work), but I generally with @jklymak's points:
I just performed the following poll to n=1 matplotlib user: "In matplotlib there are functions which auto-advance the color (e.g. plot()) and functions which don't (e.g. text()). There is currently a proposal to add a function for drawing arrows (which is currently possible but a bit complicated); do you expect that this function should auto-advance the color or not?" (This question is specifically formulated so that the pollee doesn't need to know advanced concepts like "color cycle" other than as "what plot() does", and also assumes that the pollee doesn't know about the already existing arrow() method -- I believe both assumptions are true for the vast majority of matplotlib users. I also don't think the formulation is particularly biased.) I got the response "no the arrows should obviously all be black, why would you want them of various colors?" (followed by a rather salient "Usually if you have two line plots which are the same color it's impossible to tell which is which, whereas usually arrows are self-evident given where they point to and thus don't need different colors". Naturally I realize that there are exceptions, but that was the intuition of the user I polled.). Naturally that's n=1 but I could easily poll more people and would suggest that others do the same. Perhaps my intuition is completely off, but I would bet that the vast majority of users would respond "the arrows should all be black". Note that my point is not really "we should not add vector()"; it's more "it should be more like annotate". Edit: Current polling status: 3 users against color-cycling, 0 in favor of it. As a side point, if we are going to make the "it can be added to legends" feature a selling point for vector(), then it should get a proper legend handler so that the legend entry is also an arrow; having the legend entry be a rectangle of solid color looks quite sloppy to me. |
We didn't discuss it on the call so nothing missed. I'm currently too burnt out from stuff happening at home to intentionally add a contentious discussion.
💯 and also I think this is another reason for factoring all this stuff into vector, b/c labeling-annotations generally serve the same "identify/add context to" role as legends and therefore probably don't need this logic either. (ETA: With the assumption that vector returns a FancyArrowPatch while annotate returns an text.Annotatation object, so the handler would I think be for FancyArrowPatch) I think it should participate in the color cycle for the sake of consistency, but also a "help us design new API" banner at the top of the docs/as poll in the sidebar could be a fun little user engagement project. |
Text doesn't participate in the color cycle, so consistency isn't everything. Yes, I realize that arrows can be "data" rather than "annotations", but (in agreement @jklymak's point above, I believe) I believe that if you poll a few users and ask them a method that draws arrows should participate in the color cycle or not (which is essentially asking "is it more like text() or more like plot()", without getting into abstract concepts like data vs. annotations) they would say "no" (i.e. they see it as annotations). |
Re: color cycle participation (semi-OT): Aside from fundamental considerations, part of the argument for color-cycling was "it's easy to opt out by giving an explicit color; you cannot opt-in". Possibly we should make it easier to opt in; e.g. supporting |
Looked it up and a major reason for opting into color_cycle is that's what arrow did #22390 (comment) and this is supposed to be the replacement 😓 but also @kmdalton and @ianhi what do you all think since you use these in your work? ETA: on the legend patch - I think a |
It would be nice to see prior work - a published example of this type of plot and/or another library that does this. |
There are countless examples in Bernhard Rupp's textbook, but I will not post them here because I do not have the copyright. Here are some fairly typical examples online. Most often authors use color coded arrows which are annotated with text. When I make these sorts of plots for lectures, I prefer to use legends to cut down on clutter. Frequently we use circles to represent a set of complex numbers with a particular amplitude and offset. |
An example from Rupp is at https://www.ruppweb.org/Xray/tutorial/vectordiag.htm. Here is a journal article https://journals.iucr.org/d/issues/2003/11/00/ba5050/ba5050.pdf that makes liberal use of these. Note that these are called vector diagrams or Argand diagrams and they all, almost uniformly, have a bunch of labelling, arc circles etc. They also tend to key two or three colors with semantic meaning. They also never seem to have quantitative information on the axes. |
Hello, friends! What are the action items here? Should we add this for discussion at the community meeting? |
I think the last thing I considered blocking was #22435 (comment) and #22435 (comment) which imply that a live discussion is needed to finalize the api. After that there will need to be (1) tests, (2) a custom legend patch as a follow up PR FWIW I think Kevin and I got a bit disheartened/overwhelmed by the level of back and forth. So in the mean time we have made and are using mpl-arrow. Though I still believe that long term this has a place in the core library (e.g. we had to use some private methods to get it work correctly). n.b. I appreciate that it's difficult to get API "right" and thus understand the necessity of the back and forth, but nonetheless found it disheartening. |
The arrow() design is broken beyond repair. Even though we currently only have to offer `annotate` as a workaround, we should make the limitations more prominent then just a note and we should steer users away. This is a a step towards deprecation (matplotlib#20387). Unfortunately the discussion of a replacement has stalled (matplotlib#22435) and we have to revive it later. But that shound not hold us from pointing out the flaws of arrow() more prominently.
The arrow() design is broken beyond repair. Even though we currently only have to offer `annotate` as a workaround, we should make the limitations more prominent then just a note and we should steer users away. This is a a step towards deprecation (matplotlib#20387). Unfortunately the discussion of a replacement has stalled (matplotlib#22435) and we have to revive it later. But that shound not hold us from pointing out the flaws of arrow() more prominently.
PR Summary
This Draft PR is a first pass at implementing a
vector
method (see #22390) for theAxes
class. Ideally someone can build on this to make a viableplt.arrow
successor.It uses
FancyArrowPatch
to draw the arrow. Right now theArrowStyle
is limited to "simple". It would be trivial to support "fancy". OtherArrowStyles
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 theFancyArrowPatch
code inplt.Annotate
here.Much of the credit goes to @ianhi for working out the details.
PR Checklist
Tests and Styling
pytest
passes).flake8-docstrings
and runflake8 --docstring-convention=all
).Documentation
doc/users/next_whats_new/
(follow instructions in README.rst there).doc/api/next_api_changes/
(follow instructions in README.rst there).