Skip to content

make FancyArrow animatable #18807

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

Merged
merged 4 commits into from
Apr 23, 2021
Merged

Conversation

ianhi
Copy link
Contributor

@ianhi ianhi commented Oct 24, 2020

PR Summary

Addresses point 7 of #5699

Makes the FancyArrow Patch animatable. While FancyArrowPatch is somewhat animatable with set_positions there is no easy way to animate the Polygon that is returned from plt.arrow. This PR addresses that gap allowing things like this:

%matplotlib ipympl
import ipywidgets as widgets
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
arrow = ax.arrow(0, 0, 0.5, 0.5)
ax.set_xlim([0, 1])
ax.set_ylim([0, 1])
width_slider = widgets.FloatSlider(min=0, max=0.5, step=0.02, description="width")
x_slider = widgets.FloatSlider(min=0, max=0.5, step=0.02, description="x")

def f(change):
    arrow.set_data(x=x_slider.value, width=width_slider.value)

width_slider.observe(f, names="value")
x_slider.observe(f, names="value")
display(widgets.VBox([x_slider, width_slider]))

fancy-arrow-anim

PR Checklist

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (run flake8 on changed files to check).
  • New features are documented, with examples if plot related.
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • Conforms to Matplotlib style conventions (install flake8-docstrings and run flake8 --docstring-convention=all).
  • 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).

@jklymak
Copy link
Member

jklymak commented Oct 25, 2020

This approach looks good to me, I don't see why anyone would object to it.

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.

FancyArrow is also a bit special: It's basically only a wrapper around Polygon that transforms the given arrow parameters into a Polygon. I agree that it's reasonable to store all attributes need to construct the arrow shape, so that single parameters can be changed and the arrow be regenerated.

I'm not yet clear how to approach this consistently. While not directly comparable, the update_arrow() method is not in line with other parts of the Matplotlib API (see note below). Proposals welcome.

Side-note: FancyArrow is the only Polygon subclass. There's no directly comparable case we can learn from, but at least also no other class we'd have to adapt to make such an update mechanism consistent.

Additional Remark: How useful is this FancyArrow anyway? It's generated in data coordinates, which means that the arrow will be distorted by the scaling ratio of the y- and x-axis. IMHO that's broken by design, and I thought basically nobody is using it. Isn't annotate() with suitable arrowprops the way to go? (c.f. https://matplotlib.org/devdocs/gallery/text_labels_and_annotations/fancyarrow_demo.html).

if head_width is None:
head_width = 3 * width
if head_length is None:
self.x = x
Copy link
Member

Choose a reason for hiding this comment

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

Let's keep all the attributes private for now. Assigning to one of these would not update the underlying polygon. If you want to expose them, we'd probably need to make them properties, like Polygon.xy.

Copy link
Member

Choose a reason for hiding this comment

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

Attributes still need to be made private.

self._make_verts()
super().__init__(self.verts, closed=True, **kwargs)

def update_arrow(self, *, x=None, y=None, dx=None, dy=None, width=None,
Copy link
Member

Choose a reason for hiding this comment

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

This is not how we manage updates in Artists within Matplotlib.

While it's not 100% consistent:

  • We don't have update_*() methods taking keyword arguments.
  • Usually we set individual properties via set_*() and that marks the Artist as _stale.
  • There's Artist.update(props), but that's just a convenience wrapper calling set_*() methods internally, and (for some reason) the single argument is a dict.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So my understanding is that when an artist has _stale=True then on the next fig.canvas.draw event it will be asked to re-render itself, is that correct?

I was trying to avoid having to call _make_verts multiple times if someone wanted to update the width and the xy positions all at once. Two ideas:

  1. call _make_verts in whatever gets called on account of _stale. Like this:
def method_I_dont_know_the_name_of(self):
    self._make_verts()
    super().method_I_dont_know_the_name_of()
  1. There are both styling (e.g. color) args and args that are analogous to to data. So maybe we can take inspiration from the set_data methods for lines? i.e. have a set_data(x,y,dx,dy,width) and set_color, set_... methods?

Copy link
Member

Choose a reason for hiding this comment

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

  1. I've never dug into the exact stale mechanism. Maybe someone else can help here. (Also just found out, that you should use the property Artist.stale instead of the private attribute Artist._stale.

  2. set_data(x, y, dx, dy, width) seems a good choice. Possibly with all parameters defaulting to None. I think in that case, it's also feasible to leave out the stale-mechanism and run _make_verts() in set_data().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about properties like length_includes_head or shape? They aren't as exactly analogous to data, but there doesn't seem to be a good reason to exclude them. Should they get their own methods or be included in a set_data method?

Copy link
Member

Choose a reason for hiding this comment

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

One could take the "pure" standpoint at data is only the position and length and everything else is styling. But then you'd also need to handle width separately. For simplicity, let's put all parameters that affect the polygon shape in set_data, so that we can compute the polygon right there.

Copy link
Member

Choose a reason for hiding this comment

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

Thinking again, the styling-related parameters are typically not modified in animations. So we can leave them as separate setters and explicitly let them call _make_verts as well. That's a bit more clean API-wise and not a significant performance limitation. (One could later still introduce stale if one wants to delay generation of the polygon). What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To my eye the break down of data vs style is this:
data:

  • x
  • y
  • dx
  • dy
  • width
  • head_width
  • head_length

style:

  • length_includes_head
  • shape
  • head_starts_at_zero
  • overhang (arguably data)

Copy link
Member

Choose a reason for hiding this comment

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

Ok by me. I'm not sure about width, head_width, and head_length. But then again, it's not too invasive having them in set_data because they will be optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should the style setters have a single setter. i.e. set_style(overhang=None...) or each get their own? Each their own can lead to clunky names like set_head_starts_at_zero

Copy link
Member

Choose a reason for hiding this comment

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

I'm somewhat opposed to set_style(). We're not using it currently and "style" is a heavily overloaded name.

If you don't need to set styles for your use case, I'm fine with not adding any setters for them for now. It's probably very rare that one wants to change them.

@ianhi
Copy link
Contributor Author

ianhi commented Oct 26, 2020

I'm not yet clear how to approach this consistently. While not directly comparable, the update_arrow() method is not in line with other parts of the Matplotlib API (see note below). Proposals welcome.

Is there a path to a more general set_data method for every artist? It would be awesome if there was a consistent interface accross maptlotlib for this. For instance it took me an embarssingly long time to figure out that to animate scatter plots I needed set_offsets not set_data.

How useful is this FancyArrow anyway? It's generated in data coordinates, which means that the arrow will be distorted by the scaling ratio of the y- and x-axis. IMHO that's broken by design, and I thought basically nobody is using it.

It's currently the object returned by plt.arrow which is how I ended up using it. I've also used FancyArrowPatch to make 3D arrows based on https://stackoverflow.com/questions/22867620/putting-arrowheads-on-vectors-in-matplotlibs-3d-plot As for usage: per a not very scientific github search (https://github.com/search?l=Python&q=plt.arrow&type=Code) the answer is "lots".

@timhoffm
Copy link
Member

Is there a path to a more general set_data method for every artist? It would be awesome if there was a consistent interface accross maptlotlib for this. For instance it took me an embarssingly long time to figure out that to animate scatter plots I needed set_offsets not set_data.

A more uniform API would be desirable. However that'll be a major effort. I suppose it's not going to happen before Matplotlib 4, which has the idea of a more clean separation/interaction between data and Artists. Scatter returns a PathCollection, and Collections have - partly historically - their own terminology.

@timhoffm
Copy link
Member

timhoffm commented Nov 3, 2020

Needs revision: Change from update_arrow() to set_data() with parameters as discussed above.

@ianhi
Copy link
Contributor Author

ianhi commented Nov 3, 2020

# Not testing Arrow, FancyArrow here
# because they seem to exist only for historical reasons.

I'm not sure I fully agree that they are only historical as plt.arrow returns a FancyArrow, but should I continue in this tradition and not add tests?

@ianhi ianhi force-pushed the FancyArrow-set-positions branch 2 times, most recently from 09d3d5f to 3026f92 Compare November 3, 2020 23:59
@ianhi
Copy link
Contributor Author

ianhi commented Nov 4, 2020

Does this need an example? I can see not needing one as tt's pretty consistent with other set_data methods and probably a bit niche thus excluding would reduce clutter. But also more examples is maybe always good?

@timhoffm
Copy link
Member

timhoffm commented Nov 4, 2020

If there is not anything particularly special (which I don't see), we don't need an example. More is not always better with examples. You can hide relevant information among repetition an clutter.

@ianhi ianhi marked this pull request as ready for review November 12, 2020 03:18
@ianhi
Copy link
Contributor Author

ianhi commented Nov 12, 2020

Needs revision: Change from update_arrow() to set_data() with parameters as discussed above.

Done.

previously the FancyArrow docstring was missing x,y,dx, and dy
@tacaswell tacaswell added this to the v3.5.0 milestone Feb 16, 2021
@ianhi ianhi force-pushed the FancyArrow-set-positions branch from 3026f92 to bbe2c90 Compare February 16, 2021 00:56
@ianhi
Copy link
Contributor Author

ianhi commented Feb 16, 2021

Needs revision: Change from update_arrow() to set_data() with parameters as discussed above.

Done.

Now done for real 😄

Copy link
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 looks good to me!

@timhoffm timhoffm merged commit 5120b19 into matplotlib:master Apr 23, 2021
@ianhi ianhi deleted the FancyArrow-set-positions branch February 5, 2022 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants