You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bug summary
Contour plots don't interact nicely with matplotlib.colors.LogNorm.
Likely related to #19748
Code for reproduction
importmatplotlib.pyplotaspltfrommatplotlib.colorsimportLogNormimportnumpyasnp# BUG: setting levels as a scalar has no effect and no warning or error is printedfig, ax=plt.subplots()
im=ax.contourf(x, y, data, norm=LogNorm(), levels=4)
fig.colorbar(im, ax=ax)
plt.show()
# kinda buggy too: setting levels as an array does have some of the desired effects# but ticks and ticklabels are incorrectfig, 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()
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
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.
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.
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 ?
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.
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 ?
Thought this doesn't (labels in the colorbar are still missing)
levels=np.concatenate(
[
10**n*np.arange(1, 10, dtype="float64")
forninrange(-2, 6) # <--- changed the upper boundary here
]
)
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.
Here you go !
(I realised while compiling this that I didn't include the data generation in the initial report, sorry !)
importmatplotlib.pyplotaspltimportmatplotlib.colorsimportLogNormimportnumpyasnpx, 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 finefig, 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 decadefig, 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 ignoredfig, 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")
forninrange(-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")
forninrange(-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")
Activity
jklymak commentedon Apr 3, 2021
I'm not sure what you feel the bug is. It would be better if you provided some data. Thanks!
jklymak commentedon Apr 3, 2021
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.
neutrinoceros commentedon Apr 3, 2021
the levels are defined as an array, supposedly I'm defining 4 (= n-1, with n=5) of them
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.
jklymak commentedon Apr 3, 2021
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.
neutrinoceros commentedon Apr 3, 2021
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.
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 ?
jklymak commentedon Apr 3, 2021
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.
neutrinoceros commentedon Apr 3, 2021
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 ?
jklymak commentedon Apr 3, 2021
I would just manually set the level, but I'd start with integer decades for the min and max.
neutrinoceros commentedon Apr 4, 2021
So I tried to do just that and, for the specific example I used in the report, I find that this works as intended
Thought this doesn't (labels in the colorbar are still missing)
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.
jklymak commentedon Apr 5, 2021
Please provide a self-contained runnable script. Thanks!
neutrinoceros commentedon Apr 6, 2021
Here you go !
(I realised while compiling this that I didn't include the data generation in the initial report, sorry !)
jklymak commentedon Apr 6, 2021
Thanks, thats super helpful.
You are kind of reporting a couple of issues here:
it works fine, so I wonder if in
def _autolev(self, N):we should be calling the LogLocator withnumticks=N? Right now it is called with no arguments.jklymak commentedon Apr 6, 2021
I'll make 1 as a good first issue, though it may have some back-combat issues that will need to be tested.
3 remaining items
oscargus commentedon Jan 19, 2022
You can provide an arbitrary base to
LogFormatter(and subclasses):matplotlib/lib/matplotlib/ticker.py
Lines 879 to 881 in 612cfae
This will (probably) also show the ticks, which seems to be removed by:
matplotlib/lib/matplotlib/ticker.py
Lines 1073 to 1075 in 612cfae
and
matplotlib/lib/matplotlib/ticker.py
Lines 1081 to 1082 in 612cfae