Skip to content

Add Spines class as a container for all Axes spines #17107

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 2 commits into from
Dec 28, 2020

Conversation

timhoffm
Copy link
Member

@timhoffm timhoffm commented Apr 12, 2020

PR Summary

Closes #17099. Alternative approach to #17100, as proposed in #17099 (comment).

With this, you can do (updated based on discussion below):

spines['top'].set_visible(False)              # as before
spines.top.set_visible(False)                 # NEW: access single spines by attibute
spines[:].set_visible(False)                 # NEW: batch-set properties for all spines
spines[['top', 'right']].set_visible(False)  # NEW: batch-set properties for some spines

and it should be mostly backward-compatible (with the exception that I do not support OrderedDict-specific methods) for spines.

Note: This PR consists of two patches for easier review: The first patch introduces the new API, the second patch applies it throughout the library (i.e. you can see the API in action here).

Open issues:

  • needs discussion
    • is it generally a good idea to have a collection object instead of OrderedDict?
    • do we want access by attribute (spines.top.set_visible)? - Access by item spines['top'] will stay anyway for backward-compatibility
    • do we want a proxy for changing all spines.all.set_visible(False)?
      This is now implemented as spines[:].set_visible(False).
    • is it ok to be not fully backward compatible by just deriving from MutableMapping? - I.e. the OrderedDict methods spines.popitem() and spines.move_to_end() are dropped without deprecation. - Could add deprecation dance for these as well if really needed.
    • Currently not implemented: Do we want to support subgroups spines['left', 'right'] or (personally preferred) spines[['left', 'right']]?
      Yes. Implemented.
    • Currently not implemented: Do we want to deprecate modifying the available spines (i.e. deprecate towards using a Mapping instead of a MutableMapping.
  • compare with AxisDict approach (see Add Spines class as a container for all Axes spines #17107 (comment))
  • pickle support
  • use spines.all throught library and examples where suitable
  • what's new entry

@anntzer
Copy link
Contributor

anntzer commented Apr 12, 2020

Haven't reviewed carefully, but you may want to compare with the AxisDict approach in mpl_toolkits, which similarly overrides getitem so that one can write ax.axis["left", "top"].toggle(...) or ax.axis[:].toggle(...) (.axis is "similar" to the spines dict).

@timhoffm timhoffm force-pushed the spines branch 2 times, most recently from 98d122a to c54a2e0 Compare April 12, 2020 19:04
@timhoffm
Copy link
Member Author

Comparison with AxisDict approach

  • AxesDict only supports item access (single item axis['top'] and all items via slice axis[:]).
    It's an intended feature that Spines makes the Elements available via attribute access (spines.top). It also has item access, but rather for backward compatibility. It currently does not support accessing all elments via a full slice, because I rather see item access as backward compatibility and we currently don't have hat. But it would be easy to add if desired.
  • The multi-element proxy object returned by AxisDict is SimpleChainedObjects, which has a simpler implementation than the SpinesProxy. However, that means it misses some features of SpinesProxy:
    • SpinesProxy limits the available methods to set_* methods. Other methods are most likely not reasonable to be batch-called. In particular all methods mainly returning a value are not supported (SimpleChainedObjects still allows to call these methods but does not return anything).
      With the limitation I'd rather err on the side of being too restrictive than to allow calling nonsensical methods. It's always possible to whitelist additional methods later if needed.
    • SpinesProxy has a somewhat reasonable docstring. SimpleChainedObjects does not and it's more hard to describe, because it's recursive: In axis[:].set_visible(False), axis[:] is a SimpleChainedObjects containing spines, and axis[:].set_visible is a SimpleChainedObject containing bound Spine.set_visible methods.
      In contrast spines.all.set_visible is a dynamically created function with the correct docstring.
  • AxisDict is a dict subclass, which may expose undesired dict functionality: Should the user be able do modify an AxisDict by adding elements, removing elements or replacing elements? I say rather not. While Spines currently allows that for backward compatibility, I'm considering deprecating these and finally making Spines read-only by deriving from Mapping instead of MutableMapping.

Summary: Spines + SpineProxy intentionally chooses a slightly different design compared to AxesDict + SimpleChainedObjects so that it provides a more well tailored interface due to:

  • proper docstrings
  • support for tab-completion
  • limiting to broadcasting setter methods

@anntzer
Copy link
Contributor

anntzer commented Apr 12, 2020

We don't necessarily have to copy the AxesDict approach here. However, I think we're also trying to bring axes_grid towards inclusion in the main lib (#17095), so I guess the converse question could be, would it make sense to change AxesDict to use this machinery (with the proper deprecation period, yada yada)? (You don't have to write the adaptor API for this PR, but it would be better to at least check something similar can be used for axes_grid.)

@timhoffm
Copy link
Member Author

Without having written the prove of concept: From what I've seen in the comparison to AxisDict (#17107 (comment)), I assume that it's quite easy to replace AxisDict with this approach.

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

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

I don't have any changes to request, but I expect this would have to be checked against Cartopy, WCSAxes, and maybe yt, so blocking just in case.

@timhoffm
Copy link
Member Author

timhoffm commented Apr 16, 2020

Checked for possible downstream incompatibilities.

Cartopy:

  • uses spines['geo'] (read/write) and spines.values()
  • overrides _gen_axes_spines()

all of these usages are supported by this PR. - I've run the Cartopy tests: the 4 same tests failed with and without this PR.

WCSA Axes:
I didn't find any spines usage. All WCSA tests pass with this PR.

yt:
I didn't find any spines usage. (I did not bother to install a dev version to run the tests.)

@jklymak
Copy link
Member

jklymak commented Apr 16, 2020

This seems pretty nice to me. I assume users can add more spines as before. How do we get the list of spines? ax.spines.keys()? I don't find the abc documentation to be super clear.

@timhoffm
Copy link
Member Author

The list of spines is still ax.spines.values(). axes.spines.keys() would give you "top, "bottom", ...

By subclassing MutableMapping and implementing its abstract methods, we get a class with essentially the same API as a dict. Therefore Spines is almost a fully compatible replacement for the original OrderedDict (the slight incompatibility coming from that I did not bother to implement the special OrderedDict methods popitem() and move_to_end()).

@timhoffm
Copy link
Member Author

Ping @QuLogic #17107 (comment) addresses your change request.

@QuLogic QuLogic dismissed their stale review October 15, 2020 03:46

Downstreams were checked.

@anntzer
Copy link
Contributor

anntzer commented Oct 16, 2020

On thing I had missed is that in #17107 (comment) @timhoffm stated

AxesDict only supports item access (single item axis['top'] and all items via slice axis[:]).

but that is actually missing an important point about AxesDict which is not provided by SpinesProxy: you can actually also select a subset of Axes using AxesDict, using e.g. axis["top", "right"] (which returns a proxy object on which you can call normal axes methods).

I'm not saying we absolutely need this feature, but would at least like to point out that this is something much easier to retrofit when using indexing notation rather than attribute access.

@timhoffm
Copy link
Member Author

timhoffm commented Oct 16, 2020

I agree that we should support subsets, but viaspines[['left', 'right']].

Thinking a bit more, I take the standpoint that we should support

# Single spine
spines['left'],   spines.left

# All spines
spines[:],        spines.all

# Subset of spines
spines[['left', 'right']]

This is an API similar to pandas and thus hopefully easy to remember. Differences are: 1) We have .all for convenience. 2) We don't have arbitrary slices because we don't have oder.

@anntzer
Copy link
Contributor

anntzer commented Oct 17, 2020

Do we want spines.all, or is spines[:] good enough?
Also note that if you support spines[["left", "right"]], then you'll likely end up supporting spines["left", "right"] as well unless you're being extra careful (the former calls spines.__getitem__(["left", "right"]) and the latter spines.__getitem__(("left", "right")), the difference is just list/tuple).

@timhoffm
Copy link
Member Author

One of the main reasons for this was to be able to access the spines via attributes (spines.left instead of spines['left']). In that way, it makes sense to have spines.all. The item access was originally intended only for backward-compatibility. However, when supporting subsets of spines, it's the only way to define them, which makes item-access first-class citizen again. In that sense, I agree, we don't need all. Btw. by removing .all this makes the API more pandas like, which IMHO is a good thing.

@timhoffm timhoffm force-pushed the spines branch 3 times, most recently from d5e9f0b to 1682307 Compare October 17, 2020 22:17
@QuLogic
Copy link
Member

QuLogic commented Oct 19, 2020

I'm a bit confused whether you do or do not want to keep .all now. At the very least, the doc note still mentions it and doesn't mention [:] that I can see.

@timhoffm
Copy link
Member Author

Oh, will fix that. .all is gone.

Copy link
Member

@efiring efiring left a comment

Choose a reason for hiding this comment

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

This is very nice.
You could use the ax.spines[['top', 'bottom']].set_visible(False) type of construct many more places in the examples, but I certainly wouldn't insist on it. It aids readability and reduces LOC whenever a setting is applied to more than one spine.

@timhoffm
Copy link
Member Author

This has two positive reviews. IMHO it could be merged, or is there still anything to be discussed?

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