From 94dc3dfae2164a5756d19dad34866edad9e7e572 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Fri, 27 Mar 2026 18:01:54 +0000 Subject: [PATCH 1/9] Fix: Prevent Cursor from forcing full canvas draw when useblit=True (#26901) --- lib/matplotlib/widgets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9ab4548157fd..add57a6fe044 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2119,7 +2119,13 @@ def onmove(self, event): self.linev.set_visible(False) self.lineh.set_visible(False) if self.needclear: - self.canvas.draw() + if self.useblit: + background = self._load_blit_background() + if background is not None: + self.canvas.restore_region(background) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw() self.needclear = False return self.needclear = True From f2680d78917ff39eaf51c9f0e1676e97ffeda532 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Tue, 31 Mar 2026 04:06:58 +0000 Subject: [PATCH 2/9] fix: use draw_idle to clear Cursor lag on move --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index add57a6fe044..6f31f70f809b 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2125,7 +2125,7 @@ def onmove(self, event): self.canvas.restore_region(background) self.canvas.blit(self.ax.bbox) else: - self.canvas.draw() + self.canvas.draw_idle() self.needclear = False return self.needclear = True From 649f67a491938c7517c6cb9beb682a631450a251 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Tue, 31 Mar 2026 20:18:28 +0000 Subject: [PATCH 3/9] Safeguard blitting and add test coverage for Cursor onmove --- lib/matplotlib/tests/test_widgets.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index a35e310c2962..5e971dc49bb9 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1796,6 +1796,32 @@ def test_MultiCursor(horizOn, vertOn, with_deprecated_canvas): assert l.get_ydata() == (.25, .25) +def test_cursor_clear_on_move_outside(): + """Test that moving the mouse outside the axes clears the cursor safely.""" + import matplotlib.pyplot as plt + from matplotlib.widgets import Cursor + + fig, ax = plt.subplots() + cursor = Cursor(ax, useblit=True) + + # Simulate the cursor lines being visible + cursor.linev.set_visible(True) + cursor.lineh.set_visible(True) + cursor.needclear = True + + # Create a mock mouse event where 'inaxes' is None (meaning outside) + class MockEvent: + inaxes = None + + # Trigger the movement cleanup + cursor.onmove(MockEvent()) + + # Verify the lines were hidden and the flag reset + assert not cursor.linev.get_visible() + assert not cursor.lineh.get_visible() + assert not cursor.needclear + + def test_parent_axes_removal(): fig, (ax_radio, ax_checks) = plt.subplots(1, 2) From 59f00ab3eadb7db12a01cffed3a4568d41ca6bae Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Wed, 1 Apr 2026 20:03:21 +0000 Subject: [PATCH 4/9] Fix MockEvent attributes and formatting in test_widgets --- lib/matplotlib/tests/test_widgets.py | 37 +++++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 5e971dc49bb9..40d4f65b4ffb 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1845,17 +1845,30 @@ def test_parent_axes_removal(): checks._clear(evt) -def test_cursor_overlapping_axes_blitting_warning(): - """Test that a warning is raised and useblit is disabled for overlapping axes.""" - fig = plt.figure() - ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.8]) - ax2 = fig.add_axes([0.2, 0.2, 0.6, 0.6]) # Explicitly overlaps ax1 +def test_cursor_clear_on_move_outside(): + """Test that moving the mouse outside the axes clears the cursor safely.""" + import matplotlib.pyplot as plt + from matplotlib.widgets import Cursor - match_text = ( - "Cursor blitting is currently not supported on " - "overlapping axes" - ) - with pytest.warns(UserWarning, match=match_text): - cursor = widgets.Cursor(ax1, useblit=True) + fig, ax = plt.subplots() + cursor = Cursor(ax, useblit=True) + + # Simulate the cursor lines being visible + cursor.linev.set_visible(True) + cursor.lineh.set_visible(True) + cursor.needclear = True + + # Create a mock mouse event where 'inaxes' is None (meaning outside) + class MockEvent: + inaxes = None + x = 0 + y = 0 + + # Trigger the movement cleanup + cursor.onmove(MockEvent()) - assert cursor.useblit is False + # Verify the lines were hidden and the flag reset + assert not cursor.linev.get_visible() + assert not cursor.lineh.get_visible() + assert not cursor.needclear + \ No newline at end of file From 9e6816f0a226501f49224063a559b4a90e5735a0 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Wed, 1 Apr 2026 20:14:59 +0000 Subject: [PATCH 5/9] Remove duplicate test function --- lib/matplotlib/tests/test_widgets.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 40d4f65b4ffb..3eb41b29ede9 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1796,32 +1796,6 @@ def test_MultiCursor(horizOn, vertOn, with_deprecated_canvas): assert l.get_ydata() == (.25, .25) -def test_cursor_clear_on_move_outside(): - """Test that moving the mouse outside the axes clears the cursor safely.""" - import matplotlib.pyplot as plt - from matplotlib.widgets import Cursor - - fig, ax = plt.subplots() - cursor = Cursor(ax, useblit=True) - - # Simulate the cursor lines being visible - cursor.linev.set_visible(True) - cursor.lineh.set_visible(True) - cursor.needclear = True - - # Create a mock mouse event where 'inaxes' is None (meaning outside) - class MockEvent: - inaxes = None - - # Trigger the movement cleanup - cursor.onmove(MockEvent()) - - # Verify the lines were hidden and the flag reset - assert not cursor.linev.get_visible() - assert not cursor.lineh.get_visible() - assert not cursor.needclear - - def test_parent_axes_removal(): fig, (ax_radio, ax_checks) = plt.subplots(1, 2) @@ -1871,4 +1845,3 @@ class MockEvent: assert not cursor.linev.get_visible() assert not cursor.lineh.get_visible() assert not cursor.needclear - \ No newline at end of file From 6f454d2b3942b5ffd02c67b9e52d120a307b31a3 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Fri, 3 Apr 2026 13:26:30 +0000 Subject: [PATCH 6/9] Fix: restore draw() to fix cursor rendering --- lib/matplotlib/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6f31f70f809b..15669d832508 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2125,7 +2125,7 @@ def onmove(self, event): self.canvas.restore_region(background) self.canvas.blit(self.ax.bbox) else: - self.canvas.draw_idle() + self.canvas.draw() self.needclear = False return self.needclear = True @@ -2144,7 +2144,7 @@ def onmove(self, event): self.ax.draw_artist(self.lineh) self.canvas.blit(self.ax.bbox) else: - self.canvas.draw_idle() + self.canvas.draw() class MultiCursor(Widget): From 121d89fa6c9f82d419e0383aaabf144780c09e46 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Sat, 18 Apr 2026 04:51:52 +0000 Subject: [PATCH 7/9] Fix: Optimize Cursor onmove by conditionally blitting background --- lib/matplotlib/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 15669d832508..1af7bcccdaef 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2123,6 +2123,9 @@ def onmove(self, event): background = self._load_blit_background() if background is not None: self.canvas.restore_region(background) + background = self._load_blit_background() + if self.useblit and background is not None: + self.canvas.restore_region(background) self.canvas.blit(self.ax.bbox) else: self.canvas.draw() From ce80b26700a20d4274e4c49d9c11f2231a754fd2 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Sat, 18 Apr 2026 09:38:11 +0000 Subject: [PATCH 8/9] Fix: Clean up git merge artifacts and restore deleted tests --- lib/matplotlib/tests/test_widgets.py | 37 +++++++++------------------- lib/matplotlib/widgets.py | 6 +---- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 3eb41b29ede9..9e78798a4212 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1819,29 +1819,16 @@ def test_parent_axes_removal(): checks._clear(evt) -def test_cursor_clear_on_move_outside(): - """Test that moving the mouse outside the axes clears the cursor safely.""" - import matplotlib.pyplot as plt - from matplotlib.widgets import Cursor +def test_cursor_overlapping_axes_blitting_warning(): + """Test that a warning is raised and useblit is disabled for overlapping axes.""" + fig = plt.figure() + ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.8]) + ax2 = fig.add_axes([0.2, 0.2, 0.6, 0.6]) + match_text = ( + "Cursor blitting is currently not supported on " + "overlapping axes" + ) + with pytest.warns(UserWarning, match=match_text): + cursor = widgets.Cursor(ax1, useblit=True) - fig, ax = plt.subplots() - cursor = Cursor(ax, useblit=True) - - # Simulate the cursor lines being visible - cursor.linev.set_visible(True) - cursor.lineh.set_visible(True) - cursor.needclear = True - - # Create a mock mouse event where 'inaxes' is None (meaning outside) - class MockEvent: - inaxes = None - x = 0 - y = 0 - - # Trigger the movement cleanup - cursor.onmove(MockEvent()) - - # Verify the lines were hidden and the flag reset - assert not cursor.linev.get_visible() - assert not cursor.lineh.get_visible() - assert not cursor.needclear + assert cursor.useblit is False diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 1af7bcccdaef..f27e3f21bec5 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2119,10 +2119,6 @@ def onmove(self, event): self.linev.set_visible(False) self.lineh.set_visible(False) if self.needclear: - if self.useblit: - background = self._load_blit_background() - if background is not None: - self.canvas.restore_region(background) background = self._load_blit_background() if self.useblit and background is not None: self.canvas.restore_region(background) @@ -2147,7 +2143,7 @@ def onmove(self, event): self.ax.draw_artist(self.lineh) self.canvas.blit(self.ax.bbox) else: - self.canvas.draw() + self.canvas.draw_idle() class MultiCursor(Widget): From 6b91973ea8253412933b2e26f5ab4a2c1b87b510 Mon Sep 17 00:00:00 2001 From: Mohit Pal Date: Sat, 18 Apr 2026 11:55:53 +0000 Subject: [PATCH 9/9] Test: Restore accidentally removed overlapping axis in test --- lib/matplotlib/tests/test_widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 9e78798a4212..a35e310c2962 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1823,7 +1823,8 @@ def test_cursor_overlapping_axes_blitting_warning(): """Test that a warning is raised and useblit is disabled for overlapping axes.""" fig = plt.figure() ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.8]) - ax2 = fig.add_axes([0.2, 0.2, 0.6, 0.6]) + ax2 = fig.add_axes([0.2, 0.2, 0.6, 0.6]) # Explicitly overlaps ax1 + match_text = ( "Cursor blitting is currently not supported on " "overlapping axes"