diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 2725dd6340a4..f1b6597b2460 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -350,6 +350,11 @@ def _split_path_and_get_label_rotation(self, path, idx, screen_pos, lw, spacing= taken into account when breaking the path, but not when computing the angle. """ if hasattr(self, "_old_style_split_collections"): + vis = False + for coll in self._old_style_split_collections: + vis |= coll.get_visible() + coll.remove() + self.set_visible(vis) del self._old_style_split_collections # Invalidate them. xys = path.vertices @@ -383,7 +388,7 @@ def _split_path_and_get_label_rotation(self, path, idx, screen_pos, lw, spacing= # If the path is closed, rotate it s.t. it starts at the label. is_closed_path = codes[stop - 1] == Path.CLOSEPOLY if is_closed_path: - cc_xys = np.concatenate([xys[idx:-1], xys[:idx+1]]) + cc_xys = np.concatenate([cc_xys[idx:-1], cc_xys[:idx+1]]) idx = 0 # Like np.interp, but additionally vectorized over fp. @@ -418,8 +423,13 @@ def interp_vec(x, xp, fp): return [np.interp(x, xp, col) for col in fp.T] new_code_blocks = [] if is_closed_path: if i0 != -1 and i1 != -1: - new_xy_blocks.extend([[(x1, y1)], cc_xys[i1:i0+1], [(x0, y0)]]) - new_code_blocks.extend([[Path.MOVETO], [Path.LINETO] * (i0 + 2 - i1)]) + # This is probably wrong in the case that the entire contour would + # be discarded, but ensures that a valid path is returned and is + # consistent with behavior of mpl <3.8 + points = cc_xys[i1:i0+1] + new_xy_blocks.extend([[(x1, y1)], points, [(x0, y0)]]) + nlines = len(points) + 1 + new_code_blocks.extend([[Path.MOVETO], [Path.LINETO] * nlines]) else: if i0 != -1: new_xy_blocks.extend([cc_xys[:i0 + 1], [(x0, y0)]]) diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_disconnected_segments.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_disconnected_segments.png new file mode 100644 index 000000000000..ceb700e09de2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_contour/contour_disconnected_segments.png differ diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index c911d499ea96..74165faad9fc 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -116,11 +116,31 @@ def test_contour_manual_labels(split_collections): plt.figure(figsize=(6, 2), dpi=200) cs = plt.contour(x, y, z) + + _maybe_split_collections(split_collections) + pts = np.array([(1.0, 3.0), (1.0, 4.4), (1.0, 6.0)]) plt.clabel(cs, manual=pts) pts = np.array([(2.0, 3.0), (2.0, 4.4), (2.0, 6.0)]) plt.clabel(cs, manual=pts, fontsize='small', colors=('r', 'g')) + +@pytest.mark.parametrize("split_collections", [False, True]) +@image_comparison(['contour_disconnected_segments'], + remove_text=True, style='mpl20', extensions=['png']) +def test_contour_label_with_disconnected_segments(split_collections): + x, y = np.mgrid[-1:1:21j, -1:1:21j] + z = 1 / np.sqrt(0.01 + (x + 0.3) ** 2 + y ** 2) + z += 1 / np.sqrt(0.01 + (x - 0.3) ** 2 + y ** 2) + + plt.figure() + cs = plt.contour(x, y, z, levels=[7]) + + # Adding labels should invalidate the old style + _maybe_split_collections(split_collections) + + cs.clabel(manual=[(0.2, 0.1)]) + _maybe_split_collections(split_collections) @@ -232,6 +252,9 @@ def test_labels(split_collections): disp_units = [(216, 177), (359, 290), (521, 406)] data_units = [(-2, .5), (0, -1.5), (2.8, 1)] + # Adding labels should invalidate the old style + _maybe_split_collections(split_collections) + CS.clabel() for x, y in data_units: @@ -338,6 +361,22 @@ def test_clabel_zorder(use_clabeltext, contour_zorder, clabel_zorder): assert clabel.get_zorder() == expected_clabel_zorder +def test_clabel_with_large_spacing(): + # When the inline spacing is large relative to the contour, it may cause the + # entire contour to be removed. In current implementation, one line segment is + # retained between the identified points. + # This behavior may be worth reconsidering, but check to be sure we do not produce + # an invalid path, which results in an error at clabel call time. + # see gh-27045 for more information + x = y = np.arange(-3.0, 3.01, 0.05) + X, Y = np.meshgrid(x, y) + Z = np.exp(-X**2 - Y**2) + + fig, ax = plt.subplots() + contourset = ax.contour(X, Y, Z, levels=[0.01, 0.2, .5, .8]) + ax.clabel(contourset, inline_spacing=100) + + # tol because ticks happen to fall on pixel boundaries so small # floating point changes in tick location flip which pixel gets # the tick.