From 3f42ea84018ba9e0b95e6529a61a1050a5bfacd9 Mon Sep 17 00:00:00 2001 From: francisayyad03 Date: Sun, 15 Mar 2026 00:11:16 -0400 Subject: [PATCH 1/4] FIX: avoid applying dashed patterns to zero-width lines and patches --- lib/matplotlib/lines.py | 12 +++++++++--- lib/matplotlib/patches.py | 10 +++++++--- lib/matplotlib/tests/test_axes.py | 8 ++++++++ lib/matplotlib/tests/test_patches.py | 6 ++++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 0cfda07dd627..9ce2228b5954 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -798,8 +798,11 @@ def draw(self, renderer): if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) - # We first draw a path within the gaps if needed. - if self.is_dashed() and self._gapcolor is not None: + # We first draw a path within the gaps if needed, but only for + # visible dashed lines; zero-width lines would otherwise yield + # all-zero dashes. + if (self._linewidth > 0 and self.is_dashed() + and self._gapcolor is not None): lc_rgba = mcolors.to_rgba(self._gapcolor, self._alpha) gc.set_foreground(lc_rgba, isRGBA=True) @@ -812,7 +815,10 @@ def draw(self, renderer): lc_rgba = mcolors.to_rgba(self._color, self._alpha) gc.set_foreground(lc_rgba, isRGBA=True) - gc.set_dashes(*self._dash_pattern) + if self._linewidth > 0: + gc.set_dashes(*self._dash_pattern) + else: + gc.set_dashes(0, None) renderer.draw_path(gc, tpath, affine.frozen()) gc.restore() diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 4a4bd698db04..8dabb548642d 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -709,8 +709,9 @@ def _draw_paths_with_artist_properties( from matplotlib.patheffects import PathEffectRenderer renderer = PathEffectRenderer(self.get_path_effects(), renderer) - # Draw the gaps first if gapcolor is set - if self._has_dashed_edge() and self._gapcolor is not None: + # We first draw a path within the gaps if needed, but only for visible + # dashed edges; zero-width edges would otherwise yield all-zero dashes. + if lw > 0 and self._has_dashed_edge() and self._gapcolor is not None: gc.set_foreground(self._gapcolor, isRGBA=True) offset_gaps, gaps = mlines._get_inverse_dash_pattern( *self._dash_pattern) @@ -720,7 +721,10 @@ def _draw_paths_with_artist_properties( # Draw the main edge gc.set_foreground(self._edgecolor, isRGBA=True) - gc.set_dashes(*self._dash_pattern) + if lw > 0: + gc.set_dashes(*self._dash_pattern) + else: + gc.set_dashes(0, None) for draw_path_args in draw_path_args_list: renderer.draw_path(gc, *draw_path_args) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 45e9cc75fec0..d62d1321d39a 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8084,6 +8084,14 @@ def test_twinning_default_axes_class(): def test_zero_linewidth(): # Check that setting a zero linewidth doesn't error plt.plot([0, 1], [0, 1], ls='--', lw=0) + plt.gcf().canvas.draw() + + +@mpl.style.context('mpl20') +def test_stairs_fill_zero_linewidth(): + fig, ax = plt.subplots() + ax.stairs([1, 2, 3, 4], [1, 2, 3, 4, 5], fill=True, ls='--') + fig.canvas.draw() def test_empty_errorbar_legend(): diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 51144a7652f3..f86d7ccb235a 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -907,6 +907,12 @@ def test_default_linestyle(): assert patch.get_linestyle() == 'solid' +def test_patch_zero_linewidth_dashed_draw(): + fig, ax = plt.subplots() + ax.add_patch(Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='--')) + fig.canvas.draw() + + def test_default_capstyle(): patch = Patch() assert patch.get_capstyle() == 'butt' From 0fe9994aa85a998a95fed5a0b8a6c71a03a9721e Mon Sep 17 00:00:00 2001 From: francisayyad03 Date: Mon, 16 Mar 2026 13:15:43 -0400 Subject: [PATCH 2/4] use figure-equality tests for zero-width dashed rendering --- lib/matplotlib/tests/test_axes.py | 18 ++++++++++-------- lib/matplotlib/tests/test_patches.py | 10 ++++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index d62d1321d39a..fbb853533f71 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8081,17 +8081,19 @@ def test_twinning_default_axes_class(): assert type(twiny) is Axes -def test_zero_linewidth(): - # Check that setting a zero linewidth doesn't error - plt.plot([0, 1], [0, 1], ls='--', lw=0) - plt.gcf().canvas.draw() +@check_figures_equal() +def test_zero_linewidth(fig_test, fig_ref): + fig_test.subplots().plot([0, 1], [0, 1], ls='--', lw=0) + fig_ref.subplots().plot([0, 1], [0, 1], ls='-', lw=0) @mpl.style.context('mpl20') -def test_stairs_fill_zero_linewidth(): - fig, ax = plt.subplots() - ax.stairs([1, 2, 3, 4], [1, 2, 3, 4, 5], fill=True, ls='--') - fig.canvas.draw() +@check_figures_equal() +def test_stairs_fill_zero_linewidth(fig_test, fig_ref): + fig_test.subplots().stairs( + [1, 2, 3, 4], [1, 2, 3, 4, 5], fill=True, ls='--') + fig_ref.subplots().stairs( + [1, 2, 3, 4], [1, 2, 3, 4, 5], fill=True, ls='-') def test_empty_errorbar_legend(): diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index f86d7ccb235a..861a93be8db5 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -907,10 +907,12 @@ def test_default_linestyle(): assert patch.get_linestyle() == 'solid' -def test_patch_zero_linewidth_dashed_draw(): - fig, ax = plt.subplots() - ax.add_patch(Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='--')) - fig.canvas.draw() +@check_figures_equal() +def test_patch_zero_linewidth_dashed_draw(fig_test, fig_ref): + fig_test.subplots().add_patch( + Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='--')) + fig_ref.subplots().add_patch( + Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='-')) def test_default_capstyle(): From 7b3f389c0a0bec2161a5c41e3a35aa3d945c2186 Mon Sep 17 00:00:00 2001 From: francisayyad03 Date: Mon, 16 Mar 2026 16:53:57 -0400 Subject: [PATCH 3/4] strengthen zero-width dash regression tests --- lib/matplotlib/tests/test_axes.py | 6 ------ lib/matplotlib/tests/test_lines.py | 16 ++++++++++++++++ lib/matplotlib/tests/test_patches.py | 21 +++++++++++++++------ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index fbb853533f71..3cf2c4dfd1c7 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8081,12 +8081,6 @@ def test_twinning_default_axes_class(): assert type(twiny) is Axes -@check_figures_equal() -def test_zero_linewidth(fig_test, fig_ref): - fig_test.subplots().plot([0, 1], [0, 1], ls='--', lw=0) - fig_ref.subplots().plot([0, 1], [0, 1], ls='-', lw=0) - - @mpl.style.context('mpl20') @check_figures_equal() def test_stairs_fill_zero_linewidth(fig_test, fig_ref): diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index a5b42d5de874..f18c5de4ad0a 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -5,6 +5,7 @@ import itertools import platform from types import SimpleNamespace +import unittest.mock from cycler import cycler import numpy as np @@ -92,6 +93,21 @@ def test_valid_linestyles(): line.set_linestyle('aardvark') +@mpl.style.context('mpl20') +def test_zero_linewidth_dashed_uses_solid_gc_dashes(): + fig, ax = plt.subplots() + line, = ax.plot([0, 1], [0, 1], ls='--', lw=0) + renderer = fig.canvas.get_renderer() + with unittest.mock.patch( + "matplotlib.backend_bases.GraphicsContextBase.set_dashes", + autospec=True, + wraps=matplotlib.backend_bases.GraphicsContextBase.set_dashes, + ) as set_dashes: + line.draw(renderer) + + assert set_dashes.call_args_list[-1].args[1:] == (0, None) + + @image_comparison(['drawstyle_variants.png'], remove_text=True, tol=0 if platform.machine() == 'x86_64' else 0.03) def test_drawstyle_variants(): diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 861a93be8db5..e525409013c4 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -2,6 +2,7 @@ Tests specific to the patches module. """ import platform +import unittest.mock import numpy as np from numpy.testing import assert_almost_equal, assert_array_equal @@ -907,12 +908,20 @@ def test_default_linestyle(): assert patch.get_linestyle() == 'solid' -@check_figures_equal() -def test_patch_zero_linewidth_dashed_draw(fig_test, fig_ref): - fig_test.subplots().add_patch( - Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='--')) - fig_ref.subplots().add_patch( - Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='-')) +@mpl.style.context('mpl20') +def test_patch_zero_linewidth_dashed_uses_solid_gc_dashes(): + _, ax = plt.subplots() + rect = Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='--') + ax.add_patch(rect) + renderer = ax.figure.canvas.get_renderer() + with unittest.mock.patch( + "matplotlib.backend_bases.GraphicsContextBase.set_dashes", + autospec=True, + wraps=mpl.backend_bases.GraphicsContextBase.set_dashes, + ) as set_dashes: + rect.draw(renderer) + + assert set_dashes.call_args_list[-1].args[1:] == (0, None) def test_default_capstyle(): From aafeacd4e81c5853f75101884c68a103e478023e Mon Sep 17 00:00:00 2001 From: francisayyad03 Date: Tue, 17 Mar 2026 12:36:50 -0400 Subject: [PATCH 4/4] simplify zero-width dash regression tests --- lib/matplotlib/tests/test_lines.py | 13 ++----------- lib/matplotlib/tests/test_patches.py | 17 ++++------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index f18c5de4ad0a..e2a63bbf962b 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -5,7 +5,6 @@ import itertools import platform from types import SimpleNamespace -import unittest.mock from cycler import cycler import numpy as np @@ -96,16 +95,8 @@ def test_valid_linestyles(): @mpl.style.context('mpl20') def test_zero_linewidth_dashed_uses_solid_gc_dashes(): fig, ax = plt.subplots() - line, = ax.plot([0, 1], [0, 1], ls='--', lw=0) - renderer = fig.canvas.get_renderer() - with unittest.mock.patch( - "matplotlib.backend_bases.GraphicsContextBase.set_dashes", - autospec=True, - wraps=matplotlib.backend_bases.GraphicsContextBase.set_dashes, - ) as set_dashes: - line.draw(renderer) - - assert set_dashes.call_args_list[-1].args[1:] == (0, None) + ax.plot([0, 1], [0, 1], ls='--', lw=0) + fig.draw_without_rendering() @image_comparison(['drawstyle_variants.png'], remove_text=True, diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index e525409013c4..80dcc43894c4 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -2,7 +2,6 @@ Tests specific to the patches module. """ import platform -import unittest.mock import numpy as np from numpy.testing import assert_almost_equal, assert_array_equal @@ -910,18 +909,10 @@ def test_default_linestyle(): @mpl.style.context('mpl20') def test_patch_zero_linewidth_dashed_uses_solid_gc_dashes(): - _, ax = plt.subplots() - rect = Rectangle((0, 0), 1, 1, fill=False, linewidth=0, linestyle='--') - ax.add_patch(rect) - renderer = ax.figure.canvas.get_renderer() - with unittest.mock.patch( - "matplotlib.backend_bases.GraphicsContextBase.set_dashes", - autospec=True, - wraps=mpl.backend_bases.GraphicsContextBase.set_dashes, - ) as set_dashes: - rect.draw(renderer) - - assert set_dashes.call_args_list[-1].args[1:] == (0, None) + fig, ax = plt.subplots() + ax.add_patch(Rectangle( + (0, 0), 1, 1, fill=False, linewidth=0, linestyle='--')) + fig.draw_without_rendering() def test_default_capstyle():