Skip to content

Incorrect placement of Colorbar ticks using LogNorm #12155

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
rayosborn opened this issue Sep 18, 2018 · 16 comments · Fixed by #12159
Closed

Incorrect placement of Colorbar ticks using LogNorm #12155

rayosborn opened this issue Sep 18, 2018 · 16 comments · Fixed by #12159
Assignees
Milestone

Comments

@rayosborn
Copy link

Bug report

Bug summary

After upgrading to Matplotlib v3.0.0, the tick marks on the colorbar are no longer placed correctly when using a matplotlib.colors.LogNorm normalization.

Code for reproduction

The code is embedded in a class defined within the NeXpy application, but uses standard Matplotlib function calls. Here are the relevant extracted lines:

from matplotlib.colors import LogNorm, Normalize, SymLogNorm
from matplotlib.ticker import AutoLocator, LogLocator, ScalarFormatter
from matplotlib.ticker import LogFormatterSciNotation as LogFormatter
        self.norm = LogNorm(self.vaxis.lo, self.vaxis.hi)
        self.locator = LogLocator()
        self.formatter = LogFormatter()
        self.image = ax.pcolormesh(x, y, self.v, cmap=self.cmap, **opts)
        self.image.set_norm(self.norm)
        self.colorbar = self.figure.colorbar(self.image, ax=ax, norm=self.norm)
        self.colorbar.locator = self.locator
        self.colorbar.formatter = self.formatter
        self.colorbar.update_normal(self.image)

Actual outcome

In Matplotlib v3.0.0, I get:
matplotlib_3_0_0

Expected outcome

In Matplotlib v2.2.3, I get:
matplotlib_2_2_3

Matplotlib version

  • Operating system: Mac OS 10.12.6
  • Matplotlib version: 3.0.0
  • Matplotlib backend: Qt5Agg
  • Python version: 3.6.5

I got the same result when using conda to install v3.0.0 or pip to install the latest development version.

@tacaswell tacaswell added this to the v3.0.x milestone Sep 18, 2018
@tacaswell
Copy link
Member

That looks like the 'log' scale did not get propogated to the color bar axex correctly.

If you do self.colorbar.set_yscale('log') does it look right?

@rayosborn
Copy link
Author

If you mean self.colorbar.ax.set_yscale('log'), that compresses the colorbar vertically to a thin strip. I don't thinkself.colorbar has a set_yscale function.

@ImportanceOfBeingErnest
Copy link
Member

ImportanceOfBeingErnest commented Sep 18, 2018

I do have problems reproducing this. I ran the following in the current development version

import matplotlib
print(matplotlib.__version__)     # 3.0.0rc1.post326+g521b60a77
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.ticker import LogLocator, LogFormatterSciNotation as LogFormatter
import numpy as np

x,y = np.ogrid[-4:4:31j,-4:4:31j]
z = np.exp(-x**2-y**2)

fig, ax = plt.subplots()

norm = mcolors.LogNorm(z.min(), z.max())

im = ax.pcolormesh(z, norm=norm)
im.set_norm(norm)

locator = LogLocator()
formatter = LogFormatter()

cbar = fig.colorbar(im, ax=ax,norm=norm)
cbar.locator = locator
cbar.formatter = formatter
cbar.update_normal(im)

plt.show()

and this is the result which looks expected:

image

You may notice that the ticklabels look differently compared to the example which makes me think that the code shown in the original post is not complete.(NeXpy uses LogFormatterSciNotation)
A Short, Self Contained, Correct (Compilable), Example would come handy here, else it's probably hard to ever find out what's going on.

@rayosborn
Copy link
Author

I will see if I can reproduce it outside the NeXpy context. The reason I posted is that this specific code has been untouched since Matplotlib v1.5, and it has worked without problem up until v2.2.3. I was hoping someone could point to the changes in the Matplotlib API that are likely to be relevant.

@jklymak
Copy link
Member

jklymak commented Sep 18, 2018

Sorry this broke for you. Colorbar tick handling has definitely changed, though the outward facing API should not have.

https://github.com/matplotlib/matplotlib/blob/master/doc/api/api_changes.rst#the-ticks-for-colorbar-now-adjust-for-the-size-of-the-colorbar

The most intrusive PR would be #9903 but there were a couple of others.

OTOH this PR works with log scales, as @ImportanceOfBeingErnest points out above, so its hard to tell what broke for NeXpy.

(downstream package developers can subscribe to https://mail.python.org/mailman/listinfo/matplotlib-devel for updates on matplotlib release candidates - 3.0 rc1 was released 11 Aug. )

Thanks!

@rayosborn
Copy link
Author

rayosborn commented Sep 18, 2018

I don't know if this is helpful or not, but looking at the API changes, I added a colorbar with the extend='neither' option (plotview is a reference to the plotting class instance, i.e., self in the above examples):

plotview.figure.colorbar(plotview.image, norm=plotview.norm, extend='neither')

I got the following:
matplotlib_3_0_0-extended-colorbar

I still can't work out why it doesn't show up in the code example by @ImportanceOfBeingErnest but it does suggest that the underlying LogNorm instance is fine. For some reason, adding extend='neither' to the NeXpy code doesn't change anything so it may depend on the order everything is invoked. I will have to look at it in the debugger.

@rayosborn
Copy link
Author

rayosborn commented Sep 18, 2018

I've managed to produce a simple example demonstrating the problem. It turns out that the issue shows up if you make a plot with a linear color scale first and then transform it to a log scale.

import matplotlib
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from matplotlib.ticker import LogLocator, LogFormatter
import numpy as np

x,y = np.ogrid[-4:4:31j,-4:4:31j]
z = 120000*np.exp(-x**2-y**2)

fig, ax = plt.subplots()

im = ax.imshow(z)
cbar = fig.colorbar(im)

norm = LogNorm(z.min(), z.max())
im.set_norm(norm)
cbar.set_norm(norm)
cbar.locator = LogLocator()
cbar.formatter = LogFormatter()
cbar.update_normal(im)

plt.show()

matplotlib-color-bar-issue

@ImportanceOfBeingErnest
Copy link
Member

An immediate solution is to use

 cbar.update_bruteforce(im)

instead.

@jklymak
Copy link
Member

jklymak commented Sep 18, 2018

@rayosborn OK, thats great. Thanks.

The issue is that before #9903 the colorbar drawing logic was all linear, and the log-space ticks were drawn by hand. This made automatic scaling of the colorbar cumbersome. However, it did allow you to simply change the locator and formatter in place, because the axes was always linear. I had no idea people would change the locator in place, but its not an unreasonable thing to do (switching the norm from linear to logarithmic). However, the axes now has no way of knowing that it is now a logarithmic axes because it doesn't check the norm again.

As @ImportanceOfBeingErnest says above, you can do:

cbar.update_bruteforce(im)

or

cbar.update_normal(im)
cbar.config_axis()

I'm not sure what a "good" solution for 3.0.x is for this. I guess at draw time we could call cbar.config_axis() I don't think its too expensive...

@rayosborn
Copy link
Author

Thanks for giving such quick feedback. NeXpy has a checkbox for switching between linear and log scales, which we use all the time when looking for features on different intensity scales. It looks like replacing update_normal with update_bruteforce fixes my problem in NeXpy - if I use update_normal and config_axis, then the height of my color bar changes when I change the intensity scale. Is this what the shrink option is for?

@jklymak
Copy link
Member

jklymak commented Sep 18, 2018

What do you do to change the intensity scale? If I do the following, it all seems to work fine, but maybe I'm misunderstanding. You shouldn't have to play around w/ shrink after you have initially made the colorbar.

norm = LogNorm(z.min(), z.max())
im.set_norm(norm)
cbar.set_norm(norm)
cbar.update_normal(im)
cbar.config_axis()

# display in here, and then user changes zmax:

norm = LogNorm(z.min(), z.max()*10)
im.set_norm(norm)
cbar.set_norm(norm)
cbar.update_normal(im)
cbar.config_axis()

@ImportanceOfBeingErnest
Copy link
Member

ImportanceOfBeingErnest commented Sep 18, 2018

To reproduce the shrinking:

import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
import numpy as np

x,y = np.ogrid[-4:4:31j,-4:4:31j]
z = 120000*np.exp(-x**2-y**2)

fig, ax = plt.subplots()

im = ax.imshow(z)
cbar = fig.colorbar(im)

# nopw change the data and scale
z*=1000
im.set_data(z)

norm = LogNorm(z.min(), z.max())
im.set_norm(norm)
cbar.set_norm(norm)

cbar.update_normal(im)
cbar.config_axis()

plt.show()

image

@jklymak
Copy link
Member

jklymak commented Sep 18, 2018

Huh. That’s a very strange bug. I’ll check it out soon.

@jklymak
Copy link
Member

jklymak commented Sep 18, 2018

import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
import numpy as np

x,y = np.ogrid[-4:4:31j,-4:4:31j]
z = 120000*np.exp(-x**2-y**2)

fig, ax = plt.subplots()

im = ax.imshow(z)
cbar = fig.colorbar(im)

norm = LogNorm(z.min(), z.max())
im.set_norm(norm)
cbar.set_norm(norm)
cbar.update_normal(im)
cbar.config_axis()

# display in here, and then user changes zmax * and* zmin...

norm = LogNorm(z.min() * 1000, z.max() * 1000)
im.set_norm(norm)
cbar.set_norm(norm)
cbar.update_normal(im)
cbar.config_axis()

plt.show()

@jklymak jklymak self-assigned this Sep 18, 2018
@jklymak
Copy link
Member

jklymak commented Sep 19, 2018

Fix in #12159 now fixes the above as well. Needs a test I guess...

@jklymak
Copy link
Member

jklymak commented Sep 19, 2018

I think #12159 fixes this, but I guess I think if a GUI is going to change so much of the colorbar, it should indeed call cbar.update_bruteforce(im). I'm not thrilled with the name of this function, but it does what the OP wanted, which is to remove the old colorbar and make a new colorbar based on im. The info about locators etc is lost, but presumably the user has kept track of those.

That it used to work before is because we drew colorbars ticks by hand, basically. That it doesn't work now is that colorbars behave more like a normal axes and let the normal tick creation do its thing.

Note that self.locator = LogLocator() will tend to add extra ticks now if extend='both'. We added colorbar._ColorbarLogLocator to trim the extra ticks. We are trying to add that functionality to all the locators soon, but we are not there yet.

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 a pull request may close this issue.

4 participants