Skip to content

Make Spines accessable by the attributes. #17099

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 4 commits into from
Closed

Conversation

y9c
Copy link
Contributor

@y9c y9c commented Apr 11, 2020

PR Summary

Make Spines accessable by the attributes.

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

@y9c
Copy link
Contributor Author

y9c commented Apr 11, 2020

#17100

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.

I‘m +0.5 on this. Slightly concerned about the redundancy (there should only be one way...), but I think attribute access is easier and more natural than item access.

You should override __dir__ as well to include the names.

@tacaswell tacaswell added this to the v3.3.0 milestone Apr 12, 2020
@tacaswell
Copy link
Member

I'm only -0.5 on this, but if we do take this we should make sure that we populate __dir__ so that tab-completion in IPython works as expected.

On the pro-side I see the argument for the slightly terser access (4 less chars on 3-5 char names) and it does make the access match the rcparams.

On the con-side, in my day-job I have ended up with dictionary sub-classes, in part to support dot-access like this, and ended up regretting it. We ran into a bunch edge cases where the we subtly changed the behavior of the base-class and our sub-class (which was doing more than adding dot-access). There is also the slightly increased complexity of explaining "this is mostly an OrederedDict, but does some extra stuff" to users. This will likely lead to questions about why other parts of the API which are currently dict like don't have this feature.

A bigger concern is that we have documented over-riding _gen_axes_spines as a way to inject custom logic so there is likely code out in the wild which is doing this, this code does not automatically "up convert" those cases so the attribute access will only work sometimes.

sharon@01:53  ➤  ack 'def _gen_axes_spines' --python
examples/specialty_plots/radar_chart.py
87:        def _gen_axes_spines(self):

examples/specialty_plots/skewt.py
95:    def _gen_axes_spines(self):

examples/misc/custom_projection.py
246:    def _gen_axes_spines(self):

lib/matplotlib/tests/test_skew.py
85:    def _gen_axes_spines(self):

lib/matplotlib/axes/_base.py
930:    def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'):

lib/matplotlib/projections/polar.py
1003:    def _gen_axes_spines(self):

lib/matplotlib/projections/geo.py
142:    def _gen_axes_spines(self):

- change `__dir__` of OrderedDict
@y9c
Copy link
Contributor Author

y9c commented Apr 12, 2020

  • Spine names is now included in _Spines object by overwriting the __dir__ method. But I haven't figured out a best solution to automatically "up convert" dict input into wrapped class. I am not sure adding a setter method for spines in _AxesBase class is an acceptable change.
    Meanwhile, this PR is just a quick and dirty demo. I think a dataclass is better than OrderedDict for storing spines.

  • .spines as dict gain more flexibility, but will lead ambiguity. One can store irrelevant data into .spines without a visible exception. Storing spines in a internal class, we can get extra inspection capabilities. Another benefit from _Spines class is that we can simplify many function within matplotlib package.

  • I am also +0.6 only for this PR. (0.1 is for those confusion occurred when I tried to modify spines.)
    Any change should be natural for the users, not only when running some legacy code, but also when accessing other object in the plot. This is the most difficult thing. For backward compatibility, less change, but for consistency some other attribute in axes class also need to be changed.
    I don’t know whether such a change is suitable for the project, but it is appropriate for users. I believe someone will figure out a better solution for this.

@ImportanceOfBeingErnest
Copy link
Member

I want to mention that the fact the ax.spines is a dictionary is pretty useful, because often enough you may want to set some property to all of them, like

plt.setp(ax.spines.values(), visible=False)

Not having to remember the name of each spine is a great advantage. (Similar for a loop, of course!)
This is not to say that this change here is bad, but it bears the problem that this option might be even less obvious to users when they see the attributes being accessed in the docs.

That being said, I think the main problem is that the .spines attribute isn't even present in the matplotlib.axes documentation.

@y9c
Copy link
Contributor Author

y9c commented Apr 12, 2020

Hi @ImportanceOfBeingErnest,

If my understanding is correct, the functional programming scheme for plotting is a 'old' way, but now it is more inclined to adopt an object-oriented approach.

Like other of the plt.xxx function in the package, plt.setp() is very powerful, but I have heard more than once that someone complains that these functions (the MATLAB style) are not easy to remember. Objected oriented scheme is more suitable for graphic manipulate.

It's not difficult to imagine that scheme like

ax.spines.set_visible(False) 

or

ax.spines.visible = False 

would be easier to be accept by new users, than

plt.setp(ax.spines.values(), visible=False)

Also, a spines in dataclass will not break those capabilities of a dictionary. Or even better

plt.setp(ax.spines, visible=False)

BTW, Controlling the spines is one of the few features that matplotlib surpasses ggplot, while it is not well documented...😂
 

@timhoffm
Copy link
Member

timhoffm commented Apr 12, 2020

A bigger concern is that we have documented over-riding _gen_axes_spines as a way to inject custom logic

Recommending to override a private method seems to a good choice, but could gracefully be handled with:

I am not sure adding a setter method for spines in _AxesBase class is an acceptable change.

I think that's ok as long as that handles all reasonable input well.

It's not difficult to imagine that scheme like

ax.spines.set_visible(False) 

It should be viable to make Spines an intelligent object that can defer method calls to all it's members in a dynamic way. Sort of:

    def __getattr__(self, key):
        spine = self.get(key)
        if spine:
            return spine
        # broadcast to all spines
        broadcast_targets = [spine for spine in self.values() if hasattr(spine, key)]
        if broadcast_targets:
             return self.create_broadcast_proxy(broadcast_targets, key)
        else:
            raise AttributeError(...)

where create_broadcast_proxy() generates a method that has the correct docstring and handles the distribution of the method call.

IMHO that would be a better broadcasting API than setp(ax.spines, visible=False), even though it's doing a bit of magic under the hood. Things to think about:

  • What about name conflicts between the spine names 'left'/'right'/'top'/'bottom' (or known custom ones) and Spine attributes or method names. AFAICS there are currently no conflicts, but we should define what will take precedence: item access or broadcasting.
  • Do we want to make it more explicit via spines.all.set_visible() instead of spines.set_visible()?
  • Do we have to support broadcasting attributes or are methods enough?
  • The item access (spines['top']) should stay for backward compatibility. However, we should investigate if that should be implemented via a MutableMapping subclass rather than subclassing OrderedDict.

@ImportanceOfBeingErnest
Copy link
Member

Are we overcomplicating things? (Given every 2h-python-crash course teaches you how to use a dictionary)

@timhoffm
Copy link
Member

Are we overcomplicating things? (Given every 2h-python-crash course teaches you how to use a dictionary)

IMHO not. The dictionary API is a somewhat unusual way of accessing a rather fixed set of subcomponents. And the method for the very common case of modifying all spines is not quite obvious.

@tacaswell
Copy link
Member

If my understanding is correct, the functional programming scheme for plotting is a 'old' way, but now it is more inclined to adopt an object-oriented approach.

The thing we really want to move away from is the implicit global state of pyplot (plt.plot "just knows" where to plot, etc) not necessarily a functional or OO flavor. plt.setp does not rely on any of the global state, it is a helper to broadcast over to the methods on the objects it was passed.

The dictionary API is a somewhat unusual way of accessing a rather fixed set of subcomponents.

The names are customized by the individual axes so while we ship a relatively constrained set, I don't think we should put a technical limitation on what names can come in. This code also likely dates back to the early days of the library when we were worrying about things like py24 compatibility.

We definitely should not break the API of 'spines as a dict' (even if we stop using it internally and un-document it as much as possible) so we don't break user code. While we do change the API, we try to only do that in cases where the existing behavior was problematic.

  • What about name conflicts between the spine names 'left'/'right'/'top'/'bottom' (or known custom ones) and Spine attributes or method names.

This an important point. At my day-job we did the attribute access in way that also made key access fallback to attribute access so for k, v in obj['items'](): worked and we ended up with a some users confused by other dictionaries were broken....


@yech1990 Thank you for working on this, that you have gotten this much of a response (even if it leans negative) is a sign you are pushing on an interesting and useful feature ! Please don't be discouraged.

@tacaswell
Copy link
Member

@timhoffm @ImportanceOfBeingErnest Can one of you please volunteer to champion this PR through the process (either getting it merged or deciding this is not something we want to do)?

@ImportanceOfBeingErnest
Copy link
Member

I'm sorry, I don't see championing this one occur anywhere towards the top of my priority list.

@y9c
Copy link
Contributor Author

y9c commented Apr 12, 2020

Thank you @tacaswell, @ImportanceOfBeingErnest and @timhoffm .I learned a lot from your comments.

This PR is just a dirty demo, #17107 provide a much a brilliant design.
I prefer to close this PR, but leave Issue #17100 and #17107 open.

The reason that I ask for a .spines class is that I think it is more acceptable from the perspective of a user. Another humble view is that this will make matplotlib get more powerful scalability.
Meanwhile, as mentioned in #17107 (comment), .axis can become more consistent through a similar approach.

I used to have such questions.
Since I can access x axis by ax.xaxis, how do I access secondary axis in this way?
Can I add any number of axis through a build in .axis class for .whaterveraxis?
Can I draw a figure with broken axis quickly through modifying the .spine attribute?

I don't think I have the ability to provide useful help, but I look forward to using these functions in matplotlib more conveniently in a future version.

@y9c y9c closed this Apr 12, 2020
@tacaswell
Copy link
Member

Thank you @yech1990 Hopefully we will hear from you again!

@QuLogic QuLogic modified the milestones: v3.3.0, v3.4.0 Jul 9, 2020
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.

Is it a better solution to access one of the spines by class attribute?
5 participants