Skip to content

Fix errobar order #17231

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
wants to merge 12 commits into from
Closed

Fix errobar order #17231

wants to merge 12 commits into from

Conversation

cosama
Copy link

@cosama cosama commented Apr 23, 2020

PR Summary

Closes #1622 #17139

This pull request fixes inconsistent behaviors of errorbar and the order they are drawn with respect to other elements.

They way this is done, is to change how the vertical and horizontal columns of errorbar are plot. So far a LineCollection was used. Unfortunately, the only way to have a collection being drawn before Lines2D is to change the zorder, which let to weird behavior as well, when other objects are supposed to be plot above the error bars. The proper way to fix this would be to fix the internals of how collections and lines are stored and ordered as mentioned by @tacaswell in #1622 (comment).

As the proper fix is a major rework of matplotlib and probably not possible in the foreseeable future, I suggest an easier fix here: The vertical and horizontal bars are drawn with Line2Ds. The only issue this causes it that the call to errobar now returns a list of Line2D while so far a LineCollection was returned. It would be even possible to add the errorbar caps directly to these new Line2D making errorbars more flexible and less argument parsing necessary, but that would have created even more changes, so I kept it simple for now (let me know if that is something you think is worth it).

The implementation should pass all tests, I had to change a few tests, where the test checked inconsistent behavior/wrong images caused by the old implementation.

Don't think this is a API change that can be made easily backward-compatible, or where a deprecation warning could be used, I'm open to suggestions how to resolve this. For sure might cause some issues of packages that depend on the correct return type. However, unless someone else has a good idea how this can be fixed, I think the benefits outweigh the drawbacks for now.

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@tacaswell tacaswell added this to the v3.4.0 milestone Apr 23, 2020
@tacaswell
Copy link
Member

Thank you for working on this!

However, I have several serious concerns about this.

As you note, this is a un-mitigatable API change so anyone who is currently working with the return values will have their code broken with no warning. This will also break any code that is expecting to be able to look at the line or collections attributes on the Axes.

LineCollectionwith many lines is significantly more preformant than the same number of Line2D objects, I am worried that switching to individual Line2D objects will cause significant performance regressions.


A middle ground, which accepts some API breakage of not being able to find the artists in the expected lists is:

  • change from using self.vlines and self.hlines to creating the LineCollection artists directly (like we already do with Line2D instead of using self.plot)
  • write a very thin Artist sub-class (or duck-type?) that basically only knows enough for forward it's draw call onto its children
  • add all of the created artists to the proxy / bucket artist described above (rather than calling self.add_line)
  • add the proxy artist to the axes with self.add_artist.
  • make sure the new artist participates it autoscaling as we expect etc

In this case we do get to fix the rendering order issues but still preserve the return type and the performance gains.


Sorting out how to do a major overhaul is part of what @story645 is funded to work on under CZI grant so hopefully the major overhaul is much closer than it used to be!

@cosama
Copy link
Author

cosama commented Apr 24, 2020

@tacaswell Thanks for getting back to me. I suspected that the change in function return would be a deal breaker, but I already did most of the changes to figure out what is going on with errorbars, so I thought I might as well put in a pull request and see what you guys think about it. I think performance is not the main issue here, if you try to plot thousands of errorbars you probably do something wrong in the first place.

I'm not very familiar with the artists, etc, so the changes you suggested most likely will take quite a bit of time for me to figure out and not sure if I will find that, but I will give it a try. Any pointers are welcome too. It certainly sounds like a much more elegant solution

Any ideas on how to force the svg tests to be run, it somehow skips them locally, but they seem required on travis?

@tacaswell
Copy link
Member

You need inkscape locally to run the svg tests (we do comparisons after rasterization rather than text comparison and use inkscape to do that rasterization).

The very very fast summary of Artists is everything you see in the output is represented by an Airtist internally (see https://matplotlib.org/tutorials/introductory/usage.html#parts-of-a-figure). When we render a figure we create a renderer object and then pass it into Figure.draw(renderer) which in turn recursively calls the draw method of its children and so on (aka visitor pattern). In each draw method the artist has the ability to leave marks on the canvas. At the end those aggregated marks are serialized out as the svg/png/...

https://aosabook.org/en/matplotlib.html is a good reference for the overall architecture.

@cosama
Copy link
Author

cosama commented Apr 24, 2020

@tacaswell. Thank you so much for taking time to explain that. Your references have been very valuable so far.

The way I would think the objects should appear is:

  1. Any previously plotted line, etc.
  2. Error bar lines and error bar bars depending on barsabove
  3. Any line, etc. plotted later

I think I figured out how the different artists are sorted for Axes through

artists = sorted(artists, key=attrgetter('zorder'))
and
def get_children(self):
# docstring inherited.
return [
*self.collections,
*self.patches,
*self.lines,
*self.texts,
*self.artists,
*self.spines.values(),
*self._get_axis_list(),
self.title, self._left_title, self._right_title,
*self.tables,
*self.images,
*self.child_axes,
*([self.legend_] if self.legend_ is not None else []),
self.patch,
]
. If my understanding of this is correct, the envisioned order only is possible if the new artist sub-class is a line2D itself and added through Axes.add_line, so that it appears in AxesBase.lines at the correct position.

I think using Line2D as a base class would be quite natural for something like Line2DWithErrorbars. Most code currently being implemented in Axes.errorbar could be moved to that new class, or left where it is for now, depending on how big these changes should be (opinions are welcome).

If you agree with me on this, I think I could manage the required changes.

@anntzer
Copy link
Contributor

anntzer commented Apr 25, 2020

The fact that all child artists are not stored in a single list is unfortunate, making it difficult to break ties by insertion order (which is more or less what we need here, I guess). I guess one option would be to complement zorder with a hidden _insertion_order which would use a global counter, and use that to break ties in zorder instead.

@cosama
Copy link
Author

cosama commented Apr 25, 2020

I pushed another implementation of this, now as @tacaswell descriped using a single Artist holding all error bar objects, in this case a class inheriting directly from Line2D. As it is mostly a line, it can be sorted in correctly with the other Line2D objects in Axes.lines. I think a lot of the complexity currently present in Axes.errorbar could be moved to that new class, but I held off doing so to see what you guys are thinking about it first.

There is something wrong with the documentation, will fix that later, but other than that it seems to pass all tests.

@anntzer: I think this would be part of the major overhaul @tacaswell mentioned to be done by @story645 over summer.

@cosama
Copy link
Author

cosama commented Apr 28, 2020

I will leave this as it is for now. With @anntzer's suggestion in #17247, these changes would be unnecessary.

I think however, putting errorbars into a single Artist container is a good idea, however, if it doesn't have to be sorted in as a Line2D the implementation probably could be optimized. I'm more than happy to work on this some more if it is considered to be useful. The errorbar function seems a bit bloated and moving that to a external class looks like a good idea to clean the code up some.

@QuLogic QuLogic modified the milestones: v3.4.0, v3.5.0 Jan 22, 2021
@QuLogic
Copy link
Member

QuLogic commented Apr 5, 2021

#18216 is now in, so I think we can now close this. Thank you for working on this, @cosama, and sorry it did not get merged in. If you want to work on the other things, feel free to open a PR.

@QuLogic QuLogic closed this Apr 5, 2021
@cosama
Copy link
Author

cosama commented Apr 5, 2021

@QuLogic Thanks for working on this. Awesome to see this finally getting into matplotlib. Was happy to help.

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

Successfully merging this pull request may close these issues.

zorder is not respected by all parts of errorbar
4 participants