Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/api/next_api_changes/behavior/32206-MMF.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Contour levels may be decreasing
--------------------------------

`~.axes.Axes.contour`, `~.axes.Axes.contourf` and the corresponding
`~.axes.Axes.tricontour`/`~.axes.Axes.tricontourf` functions now accept contour
levels given in monotonically decreasing order; the levels (and any per-level
colors, linewidths, linestyles or hatches supplied with them) are reversed
internally instead of raising a ``ValueError``. Levels that are not monotonic
still raise.
49 changes: 45 additions & 4 deletions lib/matplotlib/contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ def __init__(self, ax, *args,
self.extent = extent
self.colors = colors
self.extend = extend
self._levels_reversed = False

self.nchunk = nchunk
self.locator = locator
Expand Down Expand Up @@ -731,6 +732,31 @@ def __init__(self, ax, *args,

self._extend_min = self.extend in ['min', 'both']
self._extend_max = self.extend in ['max', 'both']
if self._levels_reversed:
# The user supplied the levels in decreasing order, so they were
# reversed internally to increasing order. Reverse any per-level
# styling given by the user so that it stays associated with the
# same levels.
if self.colors is not None:
color_sequence = (
list(self.colors)
if not mcolors.is_color_like(self.colors)
else [self.colors])
ncolors = len(self.levels) - int(self.filled)
total_levels = ncolors + int(self._extend_min) + int(self._extend_max)
if len(color_sequence) == total_levels:
# Keep the extended (under/over) colors at the ends.
i0 = int(self._extend_min)
i1 = len(color_sequence) - int(self._extend_max)
color_sequence = (color_sequence[:i0]
+ color_sequence[i0:i1][::-1]
+ color_sequence[i1:])
else:
color_sequence = color_sequence[::-1]
self.colors = color_sequence
if (self.filled and self.hatches is not None
and not all(h is None for h in self.hatches)):
self.hatches = list(self.hatches)[::-1]
if self.colors is not None:
if mcolors.is_color_like(self.colors):
color_sequence = [self.colors]
Expand Down Expand Up @@ -1054,8 +1080,15 @@ def _process_contour_level_args(self, args, z_dtype):

if self.filled and len(self.levels) < 2:
raise ValueError("Filled contours require at least 2 levels.")
if len(self.levels) > 1 and np.min(np.diff(self.levels)) <= 0.0:
raise ValueError("Contour levels must be increasing")
if len(self.levels) > 1:
diffs = np.diff(self.levels)
if not (np.all(diffs > 0) or np.all(diffs < 0)):
raise ValueError("Contour levels must be increasing")
if np.all(diffs < 0):
# The levels were given in decreasing order; reverse them so
# that the rest of the code can assume increasing levels.
self.levels = self.levels[::-1]
self._levels_reversed = True

def _process_levels(self):
"""
Expand Down Expand Up @@ -1149,7 +1182,10 @@ def _process_linewidths(self, linewidths):
return [linewidths] * Nlev
else:
linewidths = list(linewidths)
return (linewidths * math.ceil(Nlev / len(linewidths)))[:Nlev]
linewidths = (linewidths * math.ceil(Nlev / len(linewidths)))[:Nlev]
# If the levels were reversed, reverse the linewidths as well so
# that each width stays with the level it was given for.
return linewidths[::-1] if self._levels_reversed else linewidths

def _process_linestyles(self, linestyles):
Nlev = len(self.levels)
Expand All @@ -1170,6 +1206,10 @@ def _process_linestyles(self, linestyles):
tlinestyles = tlinestyles * nreps
if len(tlinestyles) > Nlev:
tlinestyles = tlinestyles[:Nlev]
# If the levels were reversed, reverse the linestyles as well
# so that each style stays with the level it was given for.
if self._levels_reversed:
tlinestyles = tlinestyles[::-1]
else:
raise ValueError("Unrecognized type for linestyles kwarg")
return tlinestyles
Expand Down Expand Up @@ -1521,7 +1561,8 @@ def _initialize_x_y(self, z):
*n*=7 is the default.

If array-like, draw contour lines at the specified levels.
The values must be in increasing order.
The values must be in increasing order, or decreasing order, in which
case they are reversed internally.

If not specified, a reasonable default is automatically chosen. For
linear scales, this corresponds to *levels=7*. For logarithmic
Expand Down
44 changes: 41 additions & 3 deletions lib/matplotlib/tests/test_contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import matplotlib as mpl
from matplotlib import pyplot as plt, rc_context, ticker
from matplotlib.colors import LogNorm, same_color
from matplotlib import colors as mcolors
import matplotlib.patches as mpatches
from matplotlib.testing.decorators import check_figures_equal, image_comparison
import pytest
Expand Down Expand Up @@ -337,11 +338,48 @@ def test_corner_mask():


def test_contourf_decreasing_levels():
# github issue 5477.
# Non-monotonic levels are rejected (github issue 5477).
z = [[0.1, 0.3], [0.5, 0.7]]
plt.figure()
with pytest.raises(ValueError):
plt.contourf(z, [1.0, 0.0])
with pytest.raises(ValueError, match="Contour levels must be increasing"):
plt.contourf(z, [1.0, 0.0, 0.5])


@pytest.mark.parametrize("func", ["contour", "contourf"])
def test_decreasing_levels(func):
# github issue 31227: monotonically decreasing levels are reversed
# internally instead of raising an error.
z = [[0, 1], [1, 2]]
fig, ax = plt.subplots()
cs = getattr(ax, func)(z, levels=[2, 1, 0])
assert_array_almost_equal(cs.levels, [0, 1, 2])
# Same levels given positionally or as an array behave identically.
cs2 = getattr(ax, func)(z, np.array([2, 1, 0]))
assert_array_almost_equal(cs2.levels, cs.levels)
cs3 = getattr(ax, func)(z, [2, 1, 0])
assert_array_almost_equal(cs3.levels, cs.levels)


def test_decreasing_levels_styling():
# Per-level styling stays associated with the level it was given for
# when the levels are provided in decreasing order.
z = [[0, 1], [1, 2]]
fig, (ax1, ax2) = plt.subplots(1, 2)
cs = ax1.contour(z, [2, 1, 0], colors=['red', 'green', 'blue'],
linewidths=[1, 2, 3],
linestyles=['solid', 'dashed', 'dotted'])
assert_array_almost_equal(cs.levels, [0, 1, 2])
for edgecolor, color in zip(cs.get_edgecolors(),
[mcolors.to_rgba('blue'),
mcolors.to_rgba('green'),
mcolors.to_rgba('red')]):
assert_array_almost_equal(edgecolor, color)
assert_array_almost_equal(cs.get_linewidths(), [3, 2, 1])
# The reversed contour is equivalent to the same contour drawn with
# increasing levels and reversed styling.
cs_ref = ax2.contour(z, [0, 1, 2], linestyles=['dotted', 'dashed',
'solid'])
assert cs.get_linestyles() == cs_ref.get_linestyles()


def test_contourf_symmetric_locator():
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/tri/_tricontour.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def _contour_args(self, args, kwargs):
between minimum and maximum numeric values of *Z*.

If array-like, draw contour lines at the specified levels. The values must
be in increasing order.
be in increasing order, or decreasing order, in which case they are reversed
internally.

Returns
-------
Expand Down
Loading