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

Skip to content

Enh better colorbar axes #20054

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 1 commit into from
May 25, 2021
Merged

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Apr 23, 2021

PR Summary

Redo of #18900 because GitHub was mad at me. PR is exactly the same...

closes #19543 and many other annoyances

PR Summary

This is a relatively major re-arrangement of how we create colorbars. This will simplify making colorbars for other norms if the norms are created with a scale. It also allows more normal manipulation of the colorbar axes. There are some negative consequences as well.

Issue

Currently most colorbars are drawn as a pcolormesh on a hidden axes. When extend='both'/'upper'/'lower' it makes the axes longer by a proportion extendlen and draws the extend regions by distorting the upper and/or lower cell of the pcolormesh. This is great, except it means that all the usual tick locators need to have special cases that do not continue drawing the ticks into the region of the axis that has the extend triangles. We do that now with a few piecemeal wrapper classes: _ColorbarAutoLocator(ticker.MaxNLocator), _ColorbarAutoMinorLocator(ticker.AutoMinorLocator), etc. Needless to say that is clunky.

The scale used for the colorbar also has to nicely invert past vmin and vmax for the triangles to be drawn properly, despite the fact these regions are only meant for over/under colors that you wouldn't necessarily expect an arbitrary norm to be able to handle.

Proposal

The proposed solution here is to draw the main pcolor ("solid") on a full axes that goes from vmin to vmax, and draw the extend triangles ("extends") as patches appended to the end of that axes. They are drawn in axes space, so the scale of the axes and the limits of the axes no longer matter.

The problem with this approach is that all the sizing and placement of axes is with an axes that is the size of the solid and the extends. i.e. if shrink=1 the whole axes, including the extends, is the height (or width) of the parent axes.

The solution proposed here is to draw the solid on an inset_axes that is shrunk from the parent axes if there are extends.

Pros:

  • The visible axis labels are all on the "inner" axes and no longer require weird wrappers around the tick Locators.
  • Any scale can be used, and the user can set the scale manually as they like. The initial scale is chosen from the norm, if it has one, or chosen to be linear.
  • the user can change the min and max of the axes if they like. We sometimes get requests for this if the user wants to zoom in on some part of the colormap.
  • I think the code is simpler, but I wrote it so YMMV

Cons:

manual axes placement is not the main colorbar axes object any more:

The biggest con is that for

cax = fig.add_axes([0.1, 0.1, 0.8, 0.07])
cb = fig.colorbar(im, cax=cax)

the axes object we create is new so that cb.ax is no longer cax. In fact cax == cb.ax.parent_axes. So this breaks cases where the user hoped to do things to cax after it was created. Of course they can access cb.ax. This failed one test where we did cax.tick_params. We can pass these through as we find them (I did already for tick_params). However, this also means that cax.xaxis points to an axis we make invisible now, and won't do anything (again cb.ax.xaxis points to the right thing.)

I will argue this breakage is worth it. Doing random things to cax failed much of the time anyway, whereas the new object stored on cb.ax should be much more robust. Unfortunately, however, I cannot think of a reasonable way to deprecate this.

Slight change in visibility of lines with extends

This is an obscure one, but lines are now partly cut off if they are drawn on the edge of the colorbar and there is an extend. The test that broke this didn't appear to have a reasonable reason to have an extend in that case anyhow (see contour_colorbar.png, and contour_manual_colors_and_levels.png)

Other changes:

  1. There seemed to have been a bug in colorbar widths with extends before, where they ended up a bit narrower than the colorbars without extends. This can be seen in double_cbar.png and a few other tests.
  2. If there is only one extend, the new axes is offset from the center. This means that the label (which is attached to the inner_axes) is now offset as well. We could move it (by placing the label logic all on the parent), but I'm not convinced that it doesn't look better to let it move down and be centred on the spine. (Also double_cbar.png)
  3. I ditched the private _internal.classic_mode for the colorbars. It doesn't change that many tests, and it needlessly complicates the Locator code to keep it around.

Test:

fig, ax = plt.subplots(1, 2, constrained_layout=True)
pc = ax[0].pcolormesh(np.arange(100).reshape(10, 10)+1)
cb = fig.colorbar(pc, ax=ax[0], extend='both')
cb.ax.set_yscale('log')

pc = ax[1].pcolormesh(np.arange(100).reshape(10, 10)+1)
cb = fig.colorbar(pc, ax=ax[1], extend='both')
cb.ax.set_ylim([20, 90])
plt.show()

Before

before

After

after

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 jklymak added this to the v3.5.0 milestone Apr 23, 2021
@jklymak jklymak mentioned this pull request Apr 23, 2021
7 tasks
@timhoffm
Copy link
Member

The biggest con is that [...] the axes object we create is new so that cb.ax? is no longer cax. In fact cax == cb.ax.parent_axes.

Can we turn this around: make a new parent axes and inject cax as the inset?

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2021

Can we turn this around: make a new parent axes and inject cax as the inset?

I don't know how we would make it so cax.set_position and cax.set_xticks would both work, because in this implementation they are different axes.

@timhoffm
Copy link
Member

I see, but this is mostly set_position vs. Everything else? In which case it might still be better to have everything else work?

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2021

My thinking is that the usual pattern is to not specify cax

cb = fig.colorbar(im, ax=axs[0, 1])  # Note cb.ax is type ColorAxes that we control, cb.parent_axes is the usual outline axes
cb.ax.set_yticks()

cb.ax.set_position()  # note this passes through to the parent axes, so work as before.

So cb.ax behaves exactly as before (if I haven't missed something).

Currently we do not support most tick methods on colorbar axes: e.g.

cbax = fig.add_axes([0.9, 0.1, 0.07, 0.8])
cb = fig.colorbar(im, cax=cbax)
cbax.set_yticks(np.arange(-2, 2, 0.2))

returns a UserWarning: UserWarning: Use the colorbar set_ticks() method instead. We can however, access the xaxis and yaxis properties of the parent, so what we are breaking is the relatively low-level:

cbax.yaxis.set_major_locator(mticker.FixedLocator(np.arange(-2, 2, 0.1)))

which will be a no-op.

Of course the alternate works fine:

cb.ax.yaxis.set_major_locator(mticker.FixedLocator(np.arange(-2, 2, 0.1)))

Maybe we could make an Axis subclass that just warned when the y or xaxis is accessed and no-ops for everything.

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2021

One thing we could do to be super clear is destroy cax so no methods work on it and it is removed from the axes stack. EDIT: no we can't, because it is a kwarg and hence immutable.

@jklymak
Copy link
Member Author

jklymak commented May 2, 2021

I don't see a clean way to cripple the parent axes (cax in cb = colorbar(im, cax=cax)) or point it to the compound axes that we store at cb.ax. We can't just change the pointer because it is an immutable argument to a kwarg. So, if the user uses a pattern as above, cax is not a good thing for the user to operate on.

While that is awkward, the benefit is that it allows a lot of things to work for cb.ax that previously did not work at all (and of course did not work for cax either).

  • we can set all the locators
  • we can change the scale.
  • we can set the ticks with the usual methods and not lock them out.
  • we can have both axes be labeled if the user wants.
    This will all allow other non-linear norms other than logarithmic, and more user-customizable locators.

Personally I feel that the slight API annoyance (you must use cb.ax and not cax if you want this thing to work as a colorbar) is a significant improvement over the existing situation, particularly as I expect most users are not positioning colorbars using cax=cax. (well, probably many are, but they probably are only doing so based on out-of-date stackoverflow recipes).

@jklymak jklymak force-pushed the enh-better-colorbar-axes branch from 4999aa7 to 6b61028 Compare May 2, 2021 19:37
@jklymak
Copy link
Member Author

jklymak commented May 2, 2021

... about 30 minutes after the above, I came up with a reasonable way to do this in the latest commit:

cb = fig.colorbar(im, cax=cax)
assert cb.ax == cax

will return False, but they are functionally the same because assert cb.ax.__dict__ == cax.__dict__ returns True. So now anything the user may want to do to cb.ax should also work on their terribly modified cax.

This is acceptable - cax is only used to position the colorbar. In the current PR, if it is passed, we copy its position and its _axes_locator.

Copy link
Contributor

@greglucas greglucas left a comment

Choose a reason for hiding this comment

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

Your last commit looks like a lot of extra code and keeping track of things to make it "functionally" the same without being exactly the same. I'm wondering if you can put something in the docstring to make it clear that you're overwriting the Axes passed in with a ColorbarAxes instead of adding a bunch of code to try and maintain half of the status quo.

Another option here is whether you need to create an entirely new ColorbarAxes or not? Could you just add an attribute to the Colorbar itself to store the inset axes instead (cb.inner_ax)? I think that might simplify some areas to not have to access nested Axes attributes (cb.ax.inner_ax). That might allow you to keep cb.ax == cax == cb.ax.parent_axes (now removed). That is just a quick thought I had, so I might be missing something fundamental too.

@@ -458,6 +458,7 @@ def _get_pos_and_bbox(ax, renderer):
bbox = pos
else:
bbox = tightbbox.transformed(fig.transFigure.inverted())
print('bbox', bbox)
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove

@@ -0,0 +1,26 @@
Axes used to make colorbar now wrapped
Copy link
Contributor

Choose a reason for hiding this comment

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

Change this API change note to the new PR 20054

else:
parent_ax = parent

inner_ax = parent_ax.inset_axes([0, 0, 1, 1])
Copy link
Contributor

Choose a reason for hiding this comment

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

You use inner and parent throughout, I think it would flow better as one of the pairs (inner, outer) or (child, parent) instead. I would vote in favor of inner/outer as that seems to signify this inset axes relationship a little better, but don't have a real strong preference at all.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, did inner/outer....

self.parent_ax.set_yticks = self.inner_ax.set_yticks
for attr in ["get_position", "set_position", "set_aspect"]:
setattr(self, attr, getattr(self.parent_ax, attr))
if userax:
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this if statement is necessary? The other branch of this would just put this dict on a newly created axes.

Copy link
Member Author

@jklymak jklymak May 4, 2021

Choose a reason for hiding this comment

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

This is necessary if we want the user's parent axes to be as much like the new axes as possible (short of being the same object). This is what locks out all the methods on the parent axes (not outer!) and points them at the inner axes. That way if the user does parent.set_xscale('log') it is the same as cb.ax.set_xscale('log').

Copy link
Contributor

Choose a reason for hiding this comment

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

I still think you could do that to outer_ax regardless of whether a userax was passed in or not? In the other case it is just a new axis you've created so I don't think it hurts anything to assign the dict in that case.

I also think you missed the argument parent change here too.

@jklymak
Copy link
Member Author

jklymak commented May 4, 2021

Your last commit looks like a lot of extra code and keeping track of things to make it "functionally" the same without being exactly the same. I'm wondering if you can put something in the docstring to make it clear that you're overwriting the Axes passed in with a ColorbarAxes instead of adding a bunch of code to try and maintain half of the status quo.

I guess I'd argue the new code is pretty straight forward though - basically, if the user passed the axes we need to do some extra bookkeeping, otherwise don't bother.

I suppose its possible that the user could manually pass us a gridspec colorbar, but at that point they are tying then selves in knots.

Another option here is whether you need to create an entirely new ColorbarAxes or not? Could you just add an attribute to the Colorbar itself to store the inset axes instead (cb.inner_ax)? I think that might simplify some areas to not have to access nested Axes attributes (cb.ax.inner_ax). That might allow you to keep cb.ax == cax == cb.ax.parent_axes (now removed). That is just a quick thought I had, so I might be missing something fundamental too.

The whole point of all this is to make cb.ax behave like a normal axes, rather than the mostly-broken thing it is now. So the methods we would normally call on an axes shouldn't be buried in cb.ax.inner_ax. (i.e. set_xscale...) Worse, we need to lock out those methods on cb.ax.parent because we don't want the user mucking with the outer axes by accident.

@greglucas
Copy link
Contributor

Where you have:

        inner_ax = outer_ax.inset_axes([0, 0, 1, 1])
        self.__dict__ = inner_ax.__dict__

        self.outer_ax = outer_ax
        self.inner_ax = inner_ax
        self.outer_ax.xaxis.set_visible(False)
        self.outer_ax.yaxis.set_visible(False)
        self.outer_ax.set_facecolor('none')
        self.outer_ax.tick_params = self.inner_ax.tick_params
        self.outer_ax.set_xticks = self.inner_ax.set_xticks
        self.outer_ax.set_yticks = self.inner_ax.set_yticks
        for attr in ["get_position", "set_position", "set_aspect"]:
            setattr(self, attr, getattr(self.outer_ax, attr))

I was wondering if you could create a new method instead that gets called on initialization to create your inner_ax? I think that you could just put your dispatching methods onto the new attribute axes, rather than needing a ColorbarAxes to do that work for you. Something like this:

    def _add_insest_axes(self):
         # self.ax is outer_ax

        inner_ax = self.ax.inset_axes([0, 0, 1, 1])
        self.__dict__ = inner_ax.__dict__
        self.inner_ax = inner_ax
        self.ax.xaxis.set_visible(False)
        self.ax.yaxis.set_visible(False)
        self.ax.set_facecolor('none')
        self.ax.tick_params = self.inner_ax.tick_params
        self.ax.set_xticks = self.inner_ax.set_xticks
        self.ax.set_yticks = self.inner_ax.set_yticks
        for attr in ["get_position", "set_position", "set_aspect"]:
            setattr(self, attr, getattr(self.ax, attr))

@jklymak
Copy link
Member Author

jklymak commented May 4, 2021

I was wondering if you could create a new method instead that gets called on initialization to create your inner_ax? I think that you could just put your dispatching methods onto the new attribute axes, rather than needing a ColorbarAxes to do that work for you. Something like this:

I guess stylistically I think that delineating it as a different type of axes would be helpful, and it may sprout its own methods.

Technically, your suggestions above won't work as written because we will have lost the outer-axes altogether, and we need to keep it and its methods, just make them basically private. i.e. we need something to continue to hold those methods.

Copy link
Contributor

@greglucas greglucas left a comment

Choose a reason for hiding this comment

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

I do think the new Axes you created that is used for mapping between two other axes is a nice way to do it 👍.

I'm wondering if you want to default userax to True instead of False... Let the make_axes routines steal the space and set userax to False there. I think that would keep it consistent if someone is calling colorbar.Colorbar(ax) directly themselves rather than figure.colorbar().

Comment on lines 1167 to 1168
else:
userax = True
Copy link
Contributor

Choose a reason for hiding this comment

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

You can get rid of some of the branches below if you populate the keywords here.

Suggested change
else:
userax = True
else:
kw['userax'] = True
current_ax = cax

cb = cbar.Colorbar(cax, mappable, **cb_kw)

self.sca(current_ax)
if not userax:
Copy link
Contributor

Choose a reason for hiding this comment

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

If you take the above suggestion, then this can become:
if current_ax is not cax or if not kw.get('userax', False)

Now ``cb.ax`` returns the parent ``ColorbarAxes``, and to get
``cax`` back, you can do::

cb.ax.parent_axes
Copy link
Contributor

Choose a reason for hiding this comment

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

outer/inner in all of this documentation as well.

@jklymak
Copy link
Member Author

jklymak commented May 6, 2021

As discussed, this needs tests of the new monkeying around with the parent....

@anntzer
Copy link
Contributor

anntzer commented May 6, 2021

Having spent a bit of time looking at the side idea (discussed during the call) of using an axis wrapper to fix the locator clipping issue (and solely that -- I'm not claiming it fixes anything else), it turns out one of the issues with that is that we don't use locator return values "as is": there's an additional clip-to-viewlims step which is performed by Axis._update_ticks (that's why Axis.get_majorticklocs can sometimes (for normal axes) return an additional tick out of bound on either side of the viewlims, which is another issue which has been discussed elsewhere...), so that would also need to know about the axis wrapper faked lims, and various tests based on cbar.ax.yaxis.get_majorticklocs() need to be rewritten as the ticklocs are not the actually drawn one anymore...

I may revisit this later, but probably will stash the idea for now. (See also polar._AxisWrapper, from where I stole the design.)

@jklymak jklymak force-pushed the enh-better-colorbar-axes branch from cef9099 to cf066f3 Compare May 7, 2021 04:48
@jklymak
Copy link
Member Author

jklymak commented May 7, 2021

I'm wondering if you want to default userax to True instead of False..

I'm a little torn about this. Users really not really supposed to do this, and if they do, they should expect the axes to get subsumed. It was a problem because axes_grid calls Colorbar directly and when I made this default to True, it of course didn't work. I've made it False but I think its questionable what the default should be.

@jklymak jklymak force-pushed the enh-better-colorbar-axes branch from cf066f3 to 95e58d7 Compare May 7, 2021 04:52
@greglucas
Copy link
Contributor

I somewhat agree with you on the default choice being questionable, but since you've gone through the whole process of trying to make the original cax passed in point to the new ColorbarAxes it seems to me like the default should be to try and monkey-patch that original axis to keep the original object alive. My guess is that if you pass cax into axes_grid, you lose that reference now, although axes_grid is a little bit different and I'm not sure that makes sense since it is really trying to create cax for you in the first place...

Would it be worth trying to deprecate the cax object being kept alive? You could put in the docstring that cax is only used for placement of a new ColorbarAxes, but should not be kept as a reference due to a new object being created.
If you want to have a reference to the axes you should do this instead:

cb = fig.colorbar(cax=cax)
cax = cb.ax

(FYI I can't resolve any of my previous comments, so feel free to resolve them yourself)

@jklymak
Copy link
Member Author

jklymak commented May 7, 2021

We discussed this on the dev call this week. Deprecating cax was definitely possible, and maybe what we will want to do. However in the meantime we would still need to do something with it, and having it do the wrong thing would be confusing. So something like the current setup is still necessary. Or poisoning the original cax.

The subtlety here is that cax can have a _locatable_axes method on it, and that should still work. Actually I should probably add a test that this still works.

@jklymak
Copy link
Member Author

jklymak commented May 7, 2021

Interestingly this example doesn't properly take into account the colorbar in the constrained layout. This will take a bit of investigation:

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(constrained_layout=True)
pc = ax.imshow(np.arange(100).reshape(10, 10))
cax = ax.inset_axes([1.02, 0.1, 0.03, 0.8])
# cax.set_in_layout(True)
fig.colorbar(pc, cax=cax)

plt.show()

@jklymak jklymak marked this pull request as draft May 7, 2021 15:12
@jklymak jklymak force-pushed the enh-better-colorbar-axes branch 2 times, most recently from 800b32c to d144f43 Compare May 7, 2021 17:27
@jklymak jklymak marked this pull request as ready for review May 7, 2021 20:41
hatches = mappable.hatches
else:
hatches = [None]
if self._extend_lower:
Copy link
Member

Choose a reason for hiding this comment

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

This should be a function call.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh hmmm, interesting I guess those just always ran, but worked fine because elower was set to 0...

antialiased=False, transform=self.ax.outer_ax.transAxes,
hatch=hatches[0])
self.ax.outer_ax.add_patch(patch)
if self._extend_upper:
Copy link
Member

Choose a reason for hiding this comment

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

Also a function call.

Copy link
Contributor

@greglucas greglucas left a comment

Choose a reason for hiding this comment

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

Some minor comments inline.

For the extra ticks in the get_ticklocs() tests now, I'm curious if that is because the locator thinks it can place ticks for the entire outer_axes viewLim, so it is placing some in the extends region for now. So, if you got rid of the extends="both" would the locator place the same as the old version? That seems like there may be some inner/outer dispatching mismatch that could be updated in the new ColorbarAxes class. Perhaps using the inner_ax._update_ticks() call from the inner_ax explicitly in the update_ticks() call on colorbar?

self._get_ticker_locator_formatter()
self._long_axis().set_major_locator(self.locator)
self._long_axis().set_minor_locator(self.minorlocator)
self._long_axis().set_major_formatter(self.formatter)

def _get_ticker_locator_formatter(self):
"""
Return the ``locator`` and ``formatter`` of the colorbar.
Copy link
Contributor

Choose a reason for hiding this comment

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

They aren't returned though, they are just set on self?

What about removing this function and adding the logic to update_ticks instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

🤷‍♂️ Maybe as a follow on. I'm not sure why this was made public in the first place, and I figure this will be invasive enough that I didn't want to try and deprecate things I didn't fully understand.

config_axis = _api.deprecate_privatize_attribute("3.3")
def update_ticks(self):
"""
Setup the ticks and ticklabels. This should not be needed by users.
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to start privatizing this function then?

expected = [r'$\mathdefault{10^{4}}$',
r'$\mathdefault{2\times10^{4}}$',
r'$\mathdefault{3\times10^{4}}$',
r'$\mathdefault{4\times10^{4}}$']
for l, exp in zip(lb, expected):
assert l.get_text() == exp
for exp in expected:
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like you should expand expected rather than using an in check because that doesn't say anything about where those ticks show up.

Copy link
Member Author

Choose a reason for hiding this comment

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

This answers your question above as well: Before we used to manually clip the ticks on the colorbar axes in the private _ColorbarLocator and _ColorbarLogLocator. Before those, we manually added all the ticks and there were no minor log ticks. This PR is a further improvement in that we get rid of these locators, and just use whatever normal Locator the user wants. This will allow any norm with a scale to be accurate represented on the colorbar.

As a consequence, cb.ax.yaxis.get_ticklabels behaves exactly the same as for any yaxis and all these extra ticks are exactly what happens with normal axises. Thats a separate battle, and should/could probably be fixed, but it should be fixed for all axis, not here. Again the point is to remove as much special casing for colorbar axes as possible.

A long way of saying that the full list of ticks is very long because it includes many ticks out of sight, and I don't think its necessary for a more precise test here...

@jklymak jklymak force-pushed the enh-better-colorbar-axes branch from 537946e to f739c48 Compare May 23, 2021 16:08
Copy link
Contributor

@greglucas greglucas left a comment

Choose a reason for hiding this comment

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

I agree, all of my other suggestions can be addressed in follow-up PRs. This looks good to me. Do you want to squash the commits down to a few descriptive ones?

@jklymak jklymak force-pushed the enh-better-colorbar-axes branch from cd36868 to 146856b Compare May 24, 2021 15:19
@jklymak
Copy link
Member Author

jklymak commented May 24, 2021

Thanks a lot @greglucas I've squashed the whole thing - I probably should get better at curating commits, but I usually mash them all together as I fix things.

@greglucas greglucas merged commit 185a298 into matplotlib:master May 25, 2021
@greglucas
Copy link
Contributor

Good work on this, @jklymak!

@dstansby
Copy link
Member

@jklymak this has caused some issues downstream in astropy (astropy/astropy#11800) with custom sub-classes of Normalize not locating the colorbar ticks properly. Off the top of your head, do you know if there's anything extra someone making a Normalize sub-class needs to now add to the new class? I couldn't find anything in the docs that went with this PR, but sorry if I missed something.

@tacaswell
Copy link
Member

I think we need a color-bar-con-fab in the next month or so (presumably by taking over one of our standing meetings).

@jklymak
Copy link
Member Author

jklymak commented May 28, 2021

I mean this is why we have pre-releases...

But, I will discuss on your astropy issue, because it looks like there is some confusion on astropy's end. But happy to open a new issue here.

@jklymak jklymak mentioned this pull request May 28, 2021
7 tasks
@jklymak jklymak mentioned this pull request Jul 6, 2021
7 tasks
@oscargus oscargus mentioned this pull request Jun 14, 2022
6 tasks
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.

Extra ticks at an extended part of colorbar
7 participants