Skip to content
Open
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/users/next_whats_new/contour_boolean_levels.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Boolean contour defaults
------------------------

`.Axes.contour` and `.Axes.contourf` now recognise boolean input arrays when
``levels`` is omitted. Line contours default to a single threshold at ``0.5``
while filled contours draw the three regions ``[0.0, 0.5, 1.0]`` to
distinguish ``False`` and ``True`` values. Passing explicit ``levels``, using a
custom locator, or enabling logarithmic scaling continues to behave exactly as
before.
34 changes: 28 additions & 6 deletions lib/matplotlib/contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -1123,15 +1123,24 @@ def _process_contour_level_args(self, args):
"""
if self.levels is None:
if len(args) == 0:
levels_arg = 7 # Default, hard-wired.
levels_arg = None
else:
levels_arg = args[0]
else:
levels_arg = self.levels
if isinstance(levels_arg, Integral):
self.levels = self._autolev(levels_arg)
if (levels_arg is None and getattr(self, "_is_bool_z", False)
and self.locator is None and not self.logscale):
if self.filled:
self.levels = np.array([0.0, 0.5, 1.0], dtype=np.float64)
else:
self.levels = np.array([0.5], dtype=np.float64)
else:
self.levels = np.asarray(levels_arg, np.float64)
if levels_arg is None:
levels_arg = 7 # Default, hard-wired.
if isinstance(levels_arg, Integral):
self.levels = self._autolev(levels_arg)
else:
self.levels = np.asarray(levels_arg, np.float64)

if not self.filled:
inside = (self.levels > self.zmin) & (self.levels < self.zmax)
Expand Down Expand Up @@ -1446,8 +1455,11 @@ def _contour_args(self, args, kwargs):
else:
fn = 'contour'
nargs = len(args)
self._is_bool_z = False
if nargs <= 2:
z = ma.asarray(args[0], dtype=np.float64)
z_input = args[0]
self._is_bool_z = ma.getdata(ma.asarray(z_input)).dtype.kind == "b"
z = ma.asarray(z_input, dtype=np.float64)
x, y = self._initialize_x_y(z)
args = args[1:]
elif nargs <= 4:
Expand Down Expand Up @@ -1475,7 +1487,9 @@ def _check_xyz(self, args, kwargs):

x = np.asarray(x, dtype=np.float64)
y = np.asarray(y, dtype=np.float64)
z = ma.asarray(args[2], dtype=np.float64)
z_input = args[2]
self._is_bool_z = ma.getdata(ma.asarray(z_input)).dtype.kind == "b"
z = ma.asarray(z_input, dtype=np.float64)

if z.ndim != 2:
raise TypeError(f"Input z must be 2D, not {z.ndim}D")
Expand Down Expand Up @@ -1585,6 +1599,14 @@ def _initialize_x_y(self, z):
If array-like, draw contour lines at the specified levels.
The values must be in increasing order.

.. note::

If *Z* has a boolean dtype and *levels* is omitted (or ``None``),
`.contour` adds a single level at ``0.5`` and `.contourf` draws
filled regions using the levels ``[0.0, 0.5, 1.0]``. Supplying
explicit *levels*, configuring *locator*, or using a logarithmic
scale preserves the provided behaviour.

Returns
-------
`~.contour.QuadContourSet`
Expand Down
51 changes: 51 additions & 0 deletions lib/matplotlib/tests/test_contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,57 @@ def test_contour_Nlevels():
assert (cs1.levels == cs2.levels).all()


def test_contour_bool_default_levels():
z = np.array([[True, False], [False, True]])

fig, ax = plt.subplots()
cs = ax.contour(z)
assert np.allclose(cs.levels, [0.5])


def test_contourf_bool_default_levels():
z = np.array([[True, False], [False, True]])

fig, ax = plt.subplots()
cs = ax.contourf(z)
assert np.allclose(cs.levels, [0.0, 0.5, 1.0])


def test_contour_bool_explicit_levels_preserved():
z = np.array([[True, False], [False, True]])

fig, ax = plt.subplots()
levels = [0.25, 0.75]
cs = ax.contour(z, levels=levels)
assert np.allclose(cs.levels, levels)


def test_contourf_masked_bool_default_levels():
data = np.array([[True, False], [False, True]])
mask = [[False, True], [False, False]]
z = np.ma.array(data, mask=mask)

fig, ax = plt.subplots()
cs = ax.contourf(z)
assert np.allclose(cs.levels, [0.0, 0.5, 1.0])


def test_contour_int_data_no_bool_special_case():
z = np.array([[1, 0], [0, 1]], dtype=int)

fig, ax = plt.subplots()
cs = ax.contour(z)
assert not np.allclose(cs.levels, [0.5])


def test_contourf_bool_lognorm_no_override():
z = np.ones((2, 2), dtype=bool)

fig, ax = plt.subplots()
cs = ax.contourf(z, norm=LogNorm())
assert not np.allclose(cs.levels, [0.0, 0.5, 1.0])


def test_contour_badlevel_fmt():
# Test edge case from https://github.com/matplotlib/matplotlib/issues/9742
# User supplied fmt for each level as a dictionary, but Matplotlib changed
Expand Down