Skip to content

BUG: Contours with LogNorm #19856

Open
Open

Description

@neutrinoceros

Bug report

Bug summary
Contour plots don't interact nicely with matplotlib.colors.LogNorm.
Likely related to #19748

Code for reproduction

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

# BUG: setting levels as a scalar has no effect and no warning or error is printed
fig, ax = plt.subplots()
im = ax.contourf(x, y, data, norm=LogNorm(), levels=4)
fig.colorbar(im, ax=ax)
plt.show()

mpl_logcontours_bug1

# kinda buggy too: setting levels as an array does have some of the desired effects
# but ticks and ticklabels are incorrect
fig, ax = plt.subplots()
levels = np.logspace(np.log10(data.min()), np.log10(data.max()), 5)
im = ax.contourf(x, y, data, norm=LogNorm(), levels=levels)
fig.colorbar(im, ax=ax)
plt.show()

mpl_logcontours_bug2

Expected outcome

The experience should be comparable with what happens when the norm isn't specified: the levels kwarg should be usable in both cases.
I note that the output from the first snippet is consistent with the case where neither levels or norm are specified, and is correct in that case, so it's definetely not completely broken and I hope the fix is somewhat straightforward.

I'm happy to take a look at the source and see if I can come up with an easy patch, but I'm not sure when I'll have time to dive in there myself. Any piece advice from maintainers or contributors is more than welcome !

Matplotlib version

  • Operating system: MacOS
  • Matplotlib version (import matplotlib; print(matplotlib.__version__)): 3.4.1
  • Matplotlib backend (print(matplotlib.get_backend())): module://ipykernel.pylab.backend_inline
  • Python version: 3.9.1
  • Jupyter version (if applicable): NA
  • Other libraries:

I installed matplotlib using pip.

Activity

  1. jklymak commented on Apr 3, 2021

    @jklymak
    Member

    I'm not sure what you feel the bug is. It would be better if you provided some data. Thanks!

  2. jklymak commented on Apr 3, 2021

    @jklymak
    Member

    Oh I see you specified the bug in your code.

    I believe we use a LogLocator to determine the levels if you use a log norm. It's possible/likely this doesn't interact well with the nlevels argument. But it does provide equal decade levels, which is probably worth it.

    I am still not clear what you feel is wrong with the second plot because you didn't specify what levels you set.

  3. neutrinoceros commented on Apr 3, 2021

    @neutrinoceros
    ContributorAuthor

    I am still not clear what you feel is wrong with the second plot because you didn't specify what levels you set.

    the levels are defined as an array, supposedly I'm defining 4 (= n-1, with n=5) of them

    levels = np.logspace(np.log10(data.min()), np.log10(data.max()), 5)

    I think the plot itself is what I want, but the colorbar is broken in that I only get on tick label, and the minor ticks don't make any sense to me.

  4. jklymak commented on Apr 3, 2021

    @jklymak
    Member

    What do you think should happen? I guess the other labels not being labeled is bad, but otherwise the ticking seems ok to me. You have put five log-spaced levels with an arbitrary starting point. The minor ticks are just the normal minor ticks.

    Note that much of this will change in 3.5 with the colorbar axis overhaul; colorbar axes will behave like normal axes as far as possible.

  5. neutrinoceros commented on Apr 3, 2021

    @neutrinoceros
    ContributorAuthor

    What do you think should happen? I guess the other labels not being labeled is bad, but otherwise the ticking seems ok to me.

    The ticking itself seems off to me because the pattern seen in minor ticks is offset from one level to an other. I'm not sure I'm making myself clear (second language, sorry), so I'll try an alternative phrasing as well:
    In logscale, I expect the visual interval between two consecutive minor ticks to be smaller and smaller (in a given decade, or more generally between two major ticks). It's obviously not the case in the first (purple, bottom) level in my second example.

    Note that much of this will change in 3.5 with the colorbar axis overhaul; colorbar axes will behave like normal axes as far as possible.

    Sounds like fixing this for the 3.4 branch only might be a waste of time then. I'm curious, what's the status of this overhaul ?

  6. jklymak commented on Apr 3, 2021

    @jklymak
    Member

    The minor ticks are ticking out tenths of decades. Your major ticks are ticking out arbitrary spacing in log space. They are definitely not going to line up.

  7. neutrinoceros commented on Apr 3, 2021

    @neutrinoceros
    ContributorAuthor

    Oh right, I guess I was expecting the minor ticks to magically retro-engineer my arbitrary spacing and line up somehow (not even sure how actually, there's no obvious generalization of decades that would work with arbitrary spacing)...
    I didn't realize how deeply rooted the base 10 log was. Thanks for this !

    What I actually want is to have more than one color per decade, maybe there's just something I'm missing here ?

  8. jklymak commented on Apr 3, 2021

    @jklymak
    Member

    I would just manually set the level, but I'd start with integer decades for the min and max.

  9. neutrinoceros commented on Apr 4, 2021

    @neutrinoceros
    ContributorAuthor

    So I tried to do just that and, for the specific example I used in the report, I find that this works as intended

    levels = np.concatenate(
        [
            10**n * np.arange(1, 10, dtype="float64")
            for n in range(-2, 7)
        ]
    )

    mpl_logcontours_bug3

    Thought this doesn't (labels in the colorbar are still missing)

    levels = np.concatenate(
        [
            10**n * np.arange(1, 10, dtype="float64")
            for n in range(-2, 6) # <--- changed the upper boundary here
        ]
    )

    mpl_logcontours_bug4

    Note that in both cases I'm reaching over the actual maximal value in the data (which is a little under 2e4) by at least one full decade, and I apparently need to add another one to get a sensible result, leading unused colors at the end of the cmap.

  10. jklymak commented on Apr 5, 2021

    @jklymak
    Member

    Please provide a self-contained runnable script. Thanks!

  11. neutrinoceros commented on Apr 6, 2021

    @neutrinoceros
    ContributorAuthor

    Here you go !
    (I realised while compiling this that I didn't include the data generation in the initial report, sorry !)

    import matplotlib.pyplot as plt
    import matplotlib.colors import LogNorm
    import numpy as np
    
    x, y = np.mgrid[1:10:0.1, 1:10:0.1]
    data = np.abs(np.sin(x)*np.exp(y))
    
    
    # CASE 0,a: leave norm unspecified, pass an int value to the `levels` keyword argument
    # RESULT: works fine
    fig, ax = plt.subplots()
    im = ax.contourf(x, y, data, levels=4)
    fig.colorbar(im, ax=ax)
    plt.savefig("/tmp/mpl_logcontours_bug0a.png")
    
    
    # CASE 0,b: specify a log norm, leave levels unspecified
    # RESULT: works fine, but only one level is used per decade
    fig, ax = plt.subplots()
    im = ax.contourf(x, y, data, norm=LogNorm())
    fig.colorbar(im, ax=ax)
    plt.savefig("/tmp/mpl_logcontours_bug0b.png")
    
    
    # CASE 1: pass an int value to the `levels` keyword argument
    # RESULT: the argument is ignored
    fig, ax = plt.subplots()
    im = ax.contourf(x, y, data, norm=LogNorm(), levels=4)
    fig.colorbar(im, ax=ax)
    plt.savefig("/tmp/mpl_logcontours_bug1.png")
    
    
    # CASE 2: construct arbitrary levels that don't match whole decades
    # RESULT: major ticks' labels are not displayed, except for the second one 
    fig, ax = plt.subplots()
    levels = np.logspace(np.log10(data.min()), np.log10(data.max()), 5)
    im = ax.contourf(x, y, data, norm=LogNorm(), levels=levels)
    fig.colorbar(im, ax=ax)
    plt.savefig("/tmp/mpl_logcontours_bug2.png")
    
    
    # CASE 3: construct arbitrary levels, matching whole decades
    # RESULT: this one works fine, but the max value is more than one decade above the
    # actual max value in the data, so the extreme colors in the colormap are not used.
    levels = np.concatenate(
        [
            10**n * np.arange(1, 10, dtype="float64")
            for n in range(-2, 7)
        ]
    )
    
    fig, ax = plt.subplots()
    im = ax.contourf(x, y, data, norm=LogNorm(), levels=levels)
    fig.colorbar(im, ax=ax)
    plt.savefig("/tmp/mpl_logcontours_bug3.png")
    
    
    # CASE 4: same as case 3, but max level is closer to global max in the data
    # RESULT: colorbar labels are missing (except for the min value, 1e-2)
    levels = np.concatenate(
        [
            10**n * np.arange(1, 10, dtype="float64")
            for n in range(-2, 6) # <--- changed the upper boundary here
        ]
    )
    
    fig, ax = plt.subplots()
    im = ax.contourf(x, y, data, norm=LogNorm(), levels=levels)
    fig.colorbar(im, ax=ax)
    plt.savefig("/tmp/mpl_logcontours_bug4.png")
  12. jklymak commented on Apr 6, 2021

    @jklymak
    Member

    Thanks, thats super helpful.

    You are kind of reporting a couple of issues here:

    1. levels=4 doesn't work for LogNorm. That seems a bug to me. If you do
    im = ax.contourf(x, y, data, norm=LogNorm(), locator=mticker.LogLocator(numticks=4))
    

    it works fine, so I wonder if in def _autolev(self, N): we should be calling the LogLocator with numticks=N? Right now it is called with no arguments.

    1. the labelling doesn't work. I think that will be closed by Enh better colorbar axes #18900, though I note that Enh better colorbar axes #18900 doesn't properly make the formatter scientific, so that seems a bug.
  13. jklymak commented on Apr 6, 2021

    @jklymak
    Member

    I'll make 1 as a good first issue, though it may have some back-combat issues that will need to be tested.

  14. added
    🌱 Good first issueOpen a pull request against these issues if there are no active ones!
    on Apr 6, 2021
  15. added this to the v3.5.0 milestone on Apr 6, 2021
  16. 3 remaining items

  17. added this to the v3.6.0 milestone on Sep 25, 2021
  18. oscargus commented on Jan 19, 2022

    @oscargus
    Member

    I didn't realize how deeply rooted the base 10 log was.

    You can provide an arbitrary base to LogFormatter (and subclasses):

    def __init__(self, base=10.0, labelOnlyBase=False,
    minor_thresholds=None,
    linthresh=None):

    This will (probably) also show the ticks, which seems to be removed by:

    # only label the decades
    fx = math.log(x) / math.log(b)
    is_x_decade = is_close_to_int(fx)

    and
    if self.labelOnlyBase and not is_x_decade:
    return ''

  19. modified the milestones: v3.6.0, v3.7.0 on Aug 24, 2022
  20. modified the milestones: v3.7.0, v3.7.1 on Feb 14, 2023
  21. modified the milestones: v3.7.1, v3.7.2 on Mar 4, 2023
  22. modified the milestones: v3.7.2, v3.7.3 on Jul 5, 2023
  23. modified the milestones: v3.7.3, v3.8.0 on Sep 9, 2023
  24. removed this from the v3.8.0 milestone on Sep 15, 2023
  25. removed
    🌱 Good first issueOpen a pull request against these issues if there are no active ones!
    on Jan 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions