Skip to content

Incorrect placement of Colorbar ticks using LogNorm #12155

Closed

Description

@rayosborn

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.

Activity

  1. added this to the v3.0.x milestone on Sep 18, 2018
  2. tacaswell commented on Sep 18, 2018

    @tacaswell
    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?

  3. rayosborn commented on Sep 18, 2018

    @rayosborn
    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.

  4. ImportanceOfBeingErnest commented on Sep 18, 2018

    @ImportanceOfBeingErnest
    Member

    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.

  5. rayosborn commented on Sep 18, 2018

    @rayosborn
    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.

  6. jklymak commented on Sep 18, 2018

    @jklymak
    Member

    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!

  7. rayosborn commented on Sep 18, 2018

    @rayosborn
    Author

    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.

  8. rayosborn commented on Sep 18, 2018

    @rayosborn
    Author

    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

  9. ImportanceOfBeingErnest commented on Sep 18, 2018

    @ImportanceOfBeingErnest
    Member

    An immediate solution is to use

     cbar.update_bruteforce(im)
    

    instead.

  10. jklymak commented on Sep 18, 2018

    @jklymak
    Member

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

  11. rayosborn commented on Sep 18, 2018

    @rayosborn
    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?

  12. jklymak commented on Sep 18, 2018

    @jklymak
    Member

    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()
  13. ImportanceOfBeingErnest commented on Sep 18, 2018

    @ImportanceOfBeingErnest
    Member

    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

  14. jklymak commented on Sep 18, 2018

    @jklymak
    Member

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

  15. jklymak commented on Sep 18, 2018

    @jklymak
    Member
    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()
  16. self-assigned this
    on Sep 18, 2018
  17. jklymak commented on Sep 19, 2018

    @jklymak
    Member

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

  18. jklymak commented on Sep 19, 2018

    @jklymak
    Member

    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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions