FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

[ENH]: Allow override of contour level autoscaling · Issue #23778 · matplotlib/matplotlib · GitHub

Repository navigation

[ENH]: Allow override of contour level autoscaling #23778

Closed

Description

Problem

In Matplotlib 3, when using a list of values for the levels argument in contour(), the list of values is overridden in the case that all requested levels fall outside the data range. While this may be desirable for casually browsing data when the user is unfamiliar with the data range, it causes serious problems for batch applications where the user legitimately intends to use their list of levels but does not know whether every input array will produce contours.

Example:

myplot = plt.contour( x , y , data , levels = [100] )
print( myplot.levels )

The above prints [0.0] when data is an array of values ranging from 0 to 50 (i.e., the requested contour level of 100 is outside the data range). As a result, the plot contains erroneous contours around near-zero values, presumably due to floating point precision.

This is a consequence of the change described here (https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.0.0.html?highlight=contour%20levels):

Selection of contour levels is now the same for contour and contourf; previously, for contour, levels outside the data range were deleted. (Exception: if no contour levels are found within the data range, the levels attribute is replaced with a list holding only the minimum of the data range.)

Proposed solution

Add a kwarg to contour() that overrides the autoscaling behavior. When the kwarg is set, it would trigger a flag in _process_contour_level_args() (https://github.com/matplotlib/matplotlib/blob/main/lib/matplotlib/contour.py):

if not self.filled:
            inside = (self.levels > self.zmin) & (self.levels < self.zmax)
            levels_in = self.levels[inside]
            if len(levels_in) == 0 and not(OVERRIDE_AUTOSCALE_FLAG):
                self.levels = [self.zmin]
                _api.warn_external(
                    "No contour levels were found within the data range.")

Activity

  1. jklymak commented on Aug 30, 2022

    Member

    I agree that at the very least this should be an option. I think the original 3.0 "feature" to provide a contour that wasn't asked for is of pretty dubious value

  2. tacaswell commented on Aug 30, 2022

    Member

    What ever we do to contour we need to do the same thing for contourf.

    Looking at the code around

    if not self.filled:
    inside = (self.levels > self.zmin) & (self.levels < self.zmax)
    levels_in = self.levels[inside]
    if len(levels_in) == 0:
    self.levels = [self.zmin]
    _api.warn_external(
    "No contour levels were found within the data range.")
    my suspicion is that we did not add this feature for 3.0, but preserved the behavior from previous versions.

    Looking at the blame, this originally came in via #8719 (mpl2.1) to fix #7486 which crashed rather than continuing on if there was no data in the levels.

    It is probably worth revisiting given subsequent work on the Python side management around contouring and pulling contourpy out.

    I'm nominally in favor of changing this warning to say "and we will plot to contours in the future" and changing the default behavior, but we can not trade the current behavior for bringing back a crash ;)

    The user-side work around is to in your batch processing check the limits and do not call contour if nothing is in range.

    attn @ianthomas23

  3. brettjrob commented on Aug 30, 2022

    Author

    I can confirm that the "unwanted" contours were not present in 2.X (2.0.2, at least). My code has been running on python 2.7 (matplotlib 2.0.2) for years without any unwanted contours, but they appeared today when testing my code with python 3.10 (matplotlib 3.5.3).

    After struggling a bit, my attempted workaround was something close to what you said:

    DO_CONTOURS = False
    data_min = np.nanmin(data)
    data_max = np.nanmax(data)
    
    for level in levels:
        if level > data_min and level < data_max:
            DO_CONTOURS = True
            break
    
    if DO_CONTOURS:
        plt.contour(...)
    

    However, this workaround fails when the axes are only displaying a subset of the full array. For example, my levels may fall within the range of data, but I may be plotting a region where no contour will be needed. In that case, matplotlib once again overrides levels and I end up with a mess of unwanted contours.

  4. tacaswell commented on Aug 30, 2022

    Member

    That does track as we added it is 2.1.

    I do not think we are doing any clipping in x/y internally and are always considering the full data passed in so I assume you are doing the sub-selection? I would do something like

    def contour_safe(data, levels):
        return np.any(data.max() > levels) & np.any(data.min() < levels)
    
    if contour_safe(trimmed_data, levels):
        ax.contour(..., trimmed_data, levels, ...)

    rather than trying to cache it.

    If you have this through out your code base, it might be worth writing a helper like

    def fixed_contour(...):
        if contour_safe(...):
            return plt.contour(...)

    (but that does require absorbing the type instability). Hopefully you can find-and-replace to victory of plt.contour -> fixed_contour. This approach should also be back and forward compatible.


    Did you have any other big surprises jumping from 2.0 -> 3.5 (effectively 6 feature releases!)?


    https://www.youtube.com/watch?v=LTMguK-XJEo might be of interest as well....

  5. ianthomas23 commented on Sep 2, 2022

    Member

    I'll take a look. I can't offhand think of any reason why the contouring itself needs it to be this way, and I have a vague recollection that it is the interaction with the colorbar which makes it more complicated. But that is a recollection from n years ago where n > 4!

  6. ianthomas23 commented on Sep 7, 2022

    Member

    Looking into this, I think we are absolutely fine to remove the overriding of self.levels = [self.zmin]. contour handles this without any problem. It seems that both matplotlib and contourpy are more robust to strange inputs than they used to be.

    We should probably add a check for no levels specified by the user for a contour call, as already happens for contourf.

    There are problems with the use of colorbar with this fix, but the error is IndexError: index 0 is out of bounds for axis 0 with size 0 which is exactly the same as in issue #23817 and occurs regardless of overriding self.levels. I conclude that we need to make colorbar more robust to corner cases.

  7. added this to the v3.7.0 milestone on Jan 12, 2023
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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL