Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implementing ticklabels keyword in plt.colorbar() #18884

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 13 commits into from

Conversation

danuo
Copy link
Contributor

@danuo danuo commented Nov 3, 2020

PR Summary

This adds the ticklabels keyword to the plt.colorbar() function. Tests and documentation has been added. Please review the documentation, as I am unsure if this has been done correctly.

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).

@timhoffm
Copy link
Member

timhoffm commented Nov 4, 2020

What is the use case for explicitly setting labels? I mainly see categoricals, but that does not make sense for a colorbar.

@danuo
Copy link
Contributor Author

danuo commented Nov 5, 2020

What is the use case for explicitly setting labels? I mainly see categoricals, but that does not make sense for a colorbar.

@timhoffm From my view, it is not us to decide if something makes sense or not. I will still provide some examples for you: May it be that someone wants to visualize the certainty of a neural network that something is part of a category. Or a user is looking for an easy/hacky way of adding units behind the tick numbers. There even is a demo for this, with the following approach:
https://matplotlib.org/3.1.1/gallery/ticks_and_spines/colorbar_tick_labelling_demo.html

cbar = fig.colorbar(cax, ticks=[-1, 0, 1])
cbar.ax.set_yticklabels(['< -1', '0', '> 1'])

Not only is this complicated, but also frustrating for users. Remember: Most of your users are no experts and, for example, set ticks with plt.xticks(ticks=.., labels=...). Therefore, I see this as an improvement of api consistency.

@jklymak
Copy link
Member

jklymak commented Nov 5, 2020

If we are adding API then we need to decide if it makes sense or not.

OTOH this seems pretty useful to me. Imagine a pcolor of phase and wanting to label \pi/2, \pi etc.

@timhoffm
Copy link
Member

timhoffm commented Nov 6, 2020

I'm a bit hesitant with adding functionality for explicit labels, because it tends to be a can of worms: The ticks positions and labels must be synchronized. If you just pass labels they will end up on the automatically chosen ticks, hence their position is dependent on the data(range) - That e.g. is explicitly forbidden in plt.xticks(), but not in the current state of this PR. Additionally, the attributes are basically public, so users could expect to be able to e.g. cb = plt.colorbar(); cb.ticklabels =['foo', 'bar'], which again can lead inconsistent labeling. Much of this is inherited from a non-ideal design, but that's where we start from and we must be really careful if extensions based on that design will consistently work. Otherwise, the API consistency gets worse overall.

IMHO colorbar_tick_labelling_demo.html should be changed because the <1 / >1 don't actually make sense. But @jklymak's phase example seems like a fair use case, which is why one can consider a labels argument. But if we want to provide this functionality, it needs more consideration per the above.

Concerning frustration: I see your point that it's more cumbersome to work on the axes afterwards as in the example. If one can provide a consistent simplification, that's a benefit for this use case. However the frustration is even larger if a claimed functionality does not work consistently. With a labels parameter we claim that setting colorbar labels is a first-class functionality. If we cannot live up to that, I'd rather not support it and defer to the example (which implies: You can change the labels by working on the internals, but it's also your responsibility to do that consistently).

@jklymak
Copy link
Member

jklymak commented Nov 6, 2020

Just to be clear, the last couple of draft PRs I have put in are attempting to make colorbar axes the same as normal axes as much as is practical. Its a bit far off, maybe even after 3.4, but it is a goal. Then most of the tricks that work for normal axes should carry over.

@danuo
Copy link
Contributor Author

danuo commented Nov 6, 2020

@timhoffm
If colorbar() is called with ticklabels, but without ticks, there actually is a userwarning in the state of this PR:

plt.colorbar(ticks=None, ticklabels=['foo', 'bar'])
**output:** UserWarning: set_ticks() must have been called.

I believe that addresses your main concern. Regarding your second argument: Would it be possible to call self.set_ticklabels(self.ticklabels) when the figure is finalized, i.e. when plt.show() or something similar is executed?

@jklymak
Copy link
Member

jklymak commented Nov 6, 2020

I think as long as ticklabels is forced to be called with ticks and they are forced to be the same, we should be fine, though you may not want to use the lower level warning.

Regarding your second argument: Would it be possible to call self.set_ticklabels(self.ticklabels) when the figure is finalized

I am not sure what you are suggesting here. The way that ticks and tick labels get decided at draw time is via Locators and Formatters. All that should happen when you call set_ticklabels(labels) is the axis's Formatter is changed to a FixedFormatter. I've not actually looked at what you have done.

@danuo
Copy link
Contributor Author

danuo commented Nov 6, 2020

@jklymak My second thought was related to Tim's concern, that directly setting cb = plt.colorbar(); cb.ticklabels =['foo', 'bar'] would have no effect on the colorbar. After reading your response and giving it a second thought, I believe my suggestion is not feasible.

I am not sure what you are suggesting here. The way that ticks and tick labels get decided at draw time is via Locators and Formatters. All that should happen when you call set_ticklabels(labels) is the axis's Formatter is changed to a FixedFormatter. I've not actually looked at what you have done.

I guess that is exactly what my PR is doing. I tried to utilize as much of existing code as possible, and indeed the kwarg ticklabels is handed over to set_ticklabels(ticklabels) which then creates an FixedFormatter.

@timhoffm
Copy link
Member

timhoffm commented Nov 6, 2020

To clarify: I'm conceptually ok with adding a ticklabels keyword arguement to colorbar(). However, I consider the proposed implementation too ad-hoc. At least the follow issues arise:

  • passing labels without an explicit list of ticks must raise in colorbar() with an appropriate message. The warning from the lower level is too weak and too imprecise.
  • passing labels and format are incompatible and must raise.
  • The existing public attributes are an API nightmare, I prefer not to add more of those, in particular if they can be overlap.
    Speaking of which, it may be reasonable to store the tick labels in format using a FixedFormatter instad of creating a new attribute. That'd only slighly worsen the situation (FixedFormatter and FixedLocator should be used together and there's still the possibility of overwriting each via the existing public attributes).

I've not completely thought this through, so there may be more.

@jklymak Will there still be public attributes and a draw_all() in your Colorbar rewrite?

@jklymak
Copy link
Member

jklymak commented Nov 6, 2020

I agree with all the above. For sure ticklabels should not be a public attribute, though we could add a set_ticklabels.

@jklymak Will there still be public attributes and a draw_all() in your Colorbar rewrite?

For sure. Nothing I'm working on will change the API (much, I hope). Its low level with how we draw the solids and the extend triangles. OTOH, I'm all for cleaning up the API. The changes I am making may make more API easier. ie. set_scale or set_lims will actually be possible, and could do what you expect without changing the underlying norm. But none of that is necessary..

@danuo
Copy link
Contributor Author

danuo commented Nov 10, 2020

@timhoffm @jklymak Please have a look at the latest commit. Now, ticks are only accepted with they are np.iterable or isinstance(ticker.Locator). This applies for ticks keyword argument in plt.colorbar() and for colorbar.set_ticks(). If they are not, ticks are not used and a clear warning warning is given. Ticklabels set through keyword argument in plt.colorbar() or through colorbar.set_ticklabels() are only accepted, if valid and explicit ticks have been supplied earlier. Anything going wrong will throw a clear warning. The attributes are both private now.

@timhoffm
Copy link
Member

Something is broken here. Tests are failing with AttributeError: 'Colorbar' object has no attribute 'vmin'.

@danuo
Copy link
Contributor Author

danuo commented Nov 13, 2020

@timhoffm
github matplotlib

I have investigated the issue and found the bug. All existing tests are now passing again. I have also implemented additional tests for the new ticklables keyword. Can you explain, why the last test of mine fails? Why doesn't the error match?

Failed: DID NOT WARN. No warnings of type (<class 'UserWarning'>,) matching (
'To set explicit ticklabels, call colorbar() with ticks keyword or use set_ticks() method first.') was emitted. The list of emitted warnings is: [UserWarning(
'To set explicit ticklabels, call colorbar() with ticks keyword or use set_ticks() method first.')].

(I have aligned the warnings so one can see that the strings are exactly the same)

@timhoffm
Copy link
Member

Why doesn't the error match?

The match parameter is interpreted as a regular expression, so you have to escape the brackets.

@danuo
Copy link
Contributor Author

danuo commented Nov 24, 2020

Why doesn't the error match?

The match parameter is interpreted as a regular expression, so you have to escape the brackets.

@timhoffm Thank you, that worked. Can you also point me in the right direction regarding the failed build of the documentation? I do not find a meaningful error message.

@timhoffm
Copy link
Member

From CI log:

/home/circleci/project/lib/mpl_toolkits/axes_grid1/colorbar.py:docstring of mpl_toolkits.axes_grid1.colorbar.colorbar:68: WARNING: Malformed table.
Text in column margin in table line 15.

===========   ====================================================
Property      Description
===========   ====================================================
*extend*      [ 'neither' | 'both' | 'min' | 'max' ]
              If not 'neither', make pointed end(s) for out-of-
              range values.  These are set for a given colormap
              using the colormap set_under and set_over methods.
*spacing*     [ 'uniform' | 'proportional' ]
              Uniform spacing gives each discrete color the same
              space; proportional makes the space proportional to
              the data interval.
*ticks*       [ None | list of ticks | Locator object ]
              If None, ticks are determined automatically from the
              input.
*ticklabels*  sequence of str
              List of texts for tick labels; must include values
              for non-visible labels.
*format*      [ None | format string | Formatter object ]
              If None, the
              :class:`~matplotlib.ticker.ScalarFormatter` is used.
              If a format string is given, e.g., '%.3f', that is
              used. An alternative
              :class:`~matplotlib.ticker.Formatter` object may be
              given instead.
*drawedges*   bool
              Whether to draw lines at color boundaries.
===========   ====================================================

*ticklabels* is one char wider than the column (specified by ===========).

@danuo
Copy link
Contributor Author

danuo commented Nov 25, 2020

@timhoffm Thank you tim, it's all fixed now.

@danuo
Copy link
Contributor Author

danuo commented Dec 3, 2020

@jklymak please review

'length as the ticks.')
# check if objects in list have valid type
elif not all(type(item) in [str, float, int] for item in ticklabels):
cbook._warn_external('ticklabels need to be a list of str')
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to check these warnings and the next one if you have time for that....

Copy link
Member

Choose a reason for hiding this comment

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

Actually, w/o reading the discussion, do we need all these warning checks? I don't think we usually check for mismatched type, for instance...

@timhoffm
Copy link
Member

Given #19016 (comment), I'm wondering if we should accept a dict/list of tuples for the ticks parameter instead of a new ticklabels parameter. Unfortunately, the above idea is not yet completely thought through.

@jklymak
Copy link
Member

jklymak commented Dec 20, 2020

OK, sorry for letting this languish.

This should just pass through to cax._long_axis.set_ticks and cax._long_axis.set_ticklabels. If set_ticks ends up with more features, or a different API, this should just inherit that. That is a slight behaviour change, but that is constant with the changes in #18900 which is trying to make colorbars more like normal axes.

@jklymak
Copy link
Member

jklymak commented Jun 3, 2021

@danuo, I'm going to close this, because how colorbar axes are handled is pretty different now that #20054 is in. If you wanted to refactor based on master, that would be great. However, you should also simply be able to do cb.ax.set_yticklabels like you can for any axes, so perhaps this is not necessary and/or much easier to implement (you just need to determine the long axes and call the set_xticklabels or set_yticklabels.

Thanks for your work on this!

@jklymak jklymak closed this Jun 3, 2021
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.

3 participants