Skip to content

Fix handling single contour level out of data range #17179

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
wants to merge 1 commit into from
Closed

Fix handling single contour level out of data range #17179

wants to merge 1 commit into from

Conversation

lhuedepohl
Copy link

PR Summary

Previously, the behavior of ContourSet was special when a single contour
level was specified that was out of range of the given z array.

In that case, instead of the given value the minimum of the dataset was
contoured, instead. This is very unexpected, in my opinion.

This simple change simply makes no contour line at all, instead.

PR Checklist

The change is so simple and small, I believe none of the entries in the checklist is really applicable. I tested, of course, that with [np.nan] in place plt.contour() still works. I am unsure, however, in what other places that change could have unintended side-effects, so I would be glad for some review.

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

Previously, the behavior of ContourSet was special when a single contour
level was specified that was out of range of the given z array.

In that case, instead of the given value the minimum of the dataset was
contoured, instead. This is very unexpected, in my opinion.

This simple change simply makes no contour line at all, instead.
@dstansby
Copy link
Member

Since there aren't any levels, would it be possible to just set it to an empty list?

@lhuedepohl
Copy link
Author

That is what I initially tried, but this seems to break some assumptions elsewhere. If I set self.levels = [] the following test script,

test.py:

import matplotlib
import matplotlib.pyplot as plt
import numpy as np

z = np.ones((5, 5))
z[0, 0] = 2
plt.contour(z, [3.0])
plt.show()

produces this error

./test.py:7: UserWarning: No contour levels were found within the data range.
  plt.contour(z, [3.0])
Traceback (most recent call last):
  File "./test.py", line 7, in <module>
    plt.contour(z, [3.0])
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/pyplot.py", line 2397, in contour
    if __ret._A is not None: sci(__ret)  # noqa
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/pyplot.py", line 2929, in sci
    return gca()._sci(im)
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/axes/_base.py", line 1866, in _sci
    if im.collections[0] not in self.collections:
IndexError: list index out of range

The observation was that with np.nan no visible contours are produced. Of course, this is also a somewhat unclean solution - someone familiar with the contouring code should probably decide if this is appropriate or if some rework on other parts might be necessary.

@jklymak
Copy link
Member

jklymak commented Apr 29, 2020

This PR could use a little more explanation and motivating example. It seems like maybe its reasonable, but its an API change to change the returned type, so it'll need some justification and tests. Also what does this proposed change do to colorbars that are attached to the contour? I'll guess they don't like the NaN, but I've not tested. Also, what is the use case that needs this fixed?

@lhuedepohl
Copy link
Author

lhuedepohl commented Apr 30, 2020

I fully concede that the commit is a bit of a hack. I believe, however, that it strictly better than the current behavior, where an arbitrary contour line is drawn instead.

Let me demonstrate. Take an array with values between 0. and 1. and try to draw a contour line at value 3.0:

demo.py:

#!/usr/bin/python3

import matplotlib.pyplot as plt
import numpy as np

z = np.ones((5, 5)) * 1.0
z[0, 0] = 0.
z[1, 0] = 0.
z[0, 1] = 0.

plt.contour(z, [3.0])
plt.title("Contour of the value $3$?")
plt.savefig("bogus_contour.png")

This produces an output file, an a user warning at run-time:

./demo.py:11: UserWarning: No contour levels were found within the data range.
  plt.contour(z, [3.0])

The output file has a contour line drawn at 0.0, instead of 3.0:
bogus_contour

I think it is quite dangerous to simply print such a run-time warning and then proceed to contour a basically arbitrary z-value instead.

@lhuedepohl
Copy link
Author

Also what does this proposed change do to colorbars that are attached to the contour?

Colorbars do not work in this case, even without my patch:

test_colorbar.py:

#!/usr/bin/python3

import matplotlib.pyplot as plt
import numpy as np

z = np.ones((5, 5))
z[0, 0] = 0
z[1, 0] = 0
z[0, 1] = 0

plt.figure(1)
plt.contour(z, [3.0])
plt.colorbar()
plt.title("Contour of the value $3$?")
plt.savefig("bogus_contour.png")

produces

./test_colorbar.py:12: UserWarning: No contour levels were found within the data range.
  plt.contour(z, [3.0])
Traceback (most recent call last):
  File "./test_colorbar.py", line 13, in <module>
    plt.colorbar()
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/pyplot.py", line 2031, in colorbar
    ret = gcf().colorbar(mappable, cax=cax, ax=ax, **kw)
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/figure.py", line 2206, in colorbar
    cb = cbar.colorbar_factory(cax, mappable, **cb_kw)
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/colorbar.py", line 1714, in colorbar_factory
    cb = Colorbar(cax, mappable, **kwargs)
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/colorbar.py", line 1222, in __init__
    ColorbarBase.__init__(self, ax, **kwargs)
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/colorbar.py", line 480, in __init__
    self.draw_all()
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/colorbar.py", line 514, in draw_all
    self._config_axes(X, Y)
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/colorbar.py", line 725, in _config_axes
    xy = self._outline(X, Y)
  File "/home/lorenz/.local/lib/python3.8/site-packages/matplotlib/colorbar.py", line 775, in _outline
    x = X.T.reshape(-1)[ii]
  File "/usr/lib64/python3.8/site-packages/numpy/ma/core.py", line 3188, in __getitem__
    dout = self.data[indx]
IndexError: index 2 is out of bounds for axis 0 with size 2

@jklymak jklymak marked this pull request as draft April 23, 2021 15:15
@jklymak
Copy link
Member

jklymak commented Apr 23, 2021

This still seems useful, but needs more work...

@dstansby
Copy link
Member

The example given in #17179 (comment) is now fixed on the Matplotlib main branch - I'm not sure when or how it was fixed, but given that I'm going to close this PR. If I've got anything wrong there please shout and we can re-open!

@dstansby dstansby closed this Jan 22, 2023
@rcomer
Copy link
Member

rcomer commented Jan 22, 2023

I haven't read in detail, but I think #24912 might be the relevant fix.

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.

4 participants