Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ad15a4c

Browse files
ENH: Implement gapcolor for patch edges (#30967)
* ENH: Implement gapcolor for patch edges Add support for gapcolor property in Patches, similar to the existing gapcolor feature in Line2D and LineCollection. This allows drawing 'stripey' patch edges with two alternating colors in dashed patterns. Use case: Drawing unfilled patches on backgrounds of unknown color, where alternating black/white edges ensure visibility. Changes: - Add gapcolor parameter to Patch.__init__ - Add get_gapcolor() and set_gapcolor() methods - Add is_dashed() method to check if edge is dashed - Modify _draw_paths_with_artist_properties() to draw gaps first with inverse dash pattern when gapcolor is set - Update update_from() to copy gapcolor - Add tests for the new functionality Closes #30934 * Add baseline image for patch gapcolor visual test * Add gapcolor methods to patches.pyi type stubs * Address review feedback for patch gapcolor implementation - Renamed gapcolor to edgegapcolor for API clarity (parameter, getter, setter) - Made is_dashed() private as _has_dashed_edge() since it's only used internally - Fixed linestyle detection to handle custom linestyles by checking for non-solid styles instead of specific dashed patterns - Moved RGBA conversion to set_edgegapcolor() for better performance (avoids redundant conversions on window resize) - Moved _gapcolor initialization next to _dash_pattern for code organization - Updated type stubs and tests accordingly * Add What's New entry and versionadded directives for edgegapcolor * Use visible colors in What's New edgegapcolor example Change edgecolor/edgegapcolor from black/white to orange/blue so the gap color is actually visible against the default white background.
1 parent 6dbc0c7 commit ad15a4c

5 files changed

Lines changed: 168 additions & 3 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
``edgegapcolor`` for Patches
2+
----------------------------
3+
4+
`~matplotlib.patches.Patch` now supports an *edgegapcolor* parameter,
5+
similar to the existing *gapcolor* in `.Line2D`. This allows patches with
6+
dashed edges to display a secondary color in the gaps, creating a "striped"
7+
edge effect.
8+
9+
This is useful when drawing unfilled patches on backgrounds of unknown color,
10+
where alternating edge colors ensure the patch boundary remains visible.
11+
12+
.. plot::
13+
:include-source: true
14+
:alt: A rectangle with a dashed orange edge and blue gaps, demonstrating the edgegapcolor feature.
15+
16+
import matplotlib.pyplot as plt
17+
from matplotlib.patches import Rectangle
18+
19+
fig, ax = plt.subplots()
20+
rect = Rectangle((0.1, 0.1), 0.6, 0.6, fill=False,
21+
edgecolor='orange', edgegapcolor='blue',
22+
linestyle='--', linewidth=3)
23+
ax.add_patch(rect)
24+
plt.show()

lib/matplotlib/patches.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(self, *,
5757
capstyle=None,
5858
joinstyle=None,
5959
hatchcolor=None,
60+
edgegapcolor=None,
6061
**kwargs):
6162
"""
6263
The following kwarg properties are supported
@@ -88,13 +89,15 @@ def __init__(self, *,
8889
self._linewidth = 0
8990
self._unscaled_dash_pattern = (0, None) # offset, dash
9091
self._dash_pattern = (0, None) # offset, dash (scaled by linewidth)
92+
self._gapcolor = None
9193

9294
self.set_linestyle(linestyle)
9395
self.set_linewidth(linewidth)
9496
self.set_antialiased(antialiased)
9597
self.set_hatch(hatch)
9698
self.set_capstyle(capstyle)
9799
self.set_joinstyle(joinstyle)
100+
self.set_edgegapcolor(edgegapcolor)
98101

99102
if len(kwargs):
100103
self._internal_update(kwargs)
@@ -294,6 +297,7 @@ def update_from(self, other):
294297
self._hatch_color = other._hatch_color
295298
self._original_hatchcolor = other._original_hatchcolor
296299
self._unscaled_dash_pattern = other._unscaled_dash_pattern
300+
self._gapcolor = other._gapcolor
297301
self.set_linewidth(other._linewidth) # also sets scaled dashes
298302
self.set_transform(other.get_data_transform())
299303
# If the transform of other needs further initialization, then it will
@@ -442,6 +446,42 @@ def set_hatchcolor(self, color):
442446
self._original_hatchcolor = color
443447
self._set_hatchcolor(color)
444448

449+
def get_edgegapcolor(self):
450+
"""
451+
Return the edge gap color.
452+
453+
.. versionadded:: 3.11
454+
455+
See also `~.Patch.set_edgegapcolor`.
456+
"""
457+
return self._gapcolor
458+
459+
def set_edgegapcolor(self, edgegapcolor):
460+
"""
461+
Set a color to fill the gaps in the dashed edge style.
462+
463+
.. versionadded:: 3.11
464+
465+
.. note::
466+
467+
Striped edges are created by drawing two interleaved dashed lines.
468+
There can be overlaps between those two, which may result in
469+
artifacts when using transparency.
470+
471+
This functionality is experimental and may change.
472+
473+
Parameters
474+
----------
475+
edgegapcolor : :mpltype:`color` or None
476+
The color with which to fill the gaps. If None, the gaps are
477+
unfilled.
478+
"""
479+
if edgegapcolor is not None:
480+
self._gapcolor = colors.to_rgba(edgegapcolor, self._alpha)
481+
else:
482+
self._gapcolor = None
483+
self.stale = True
484+
445485
def set_alpha(self, alpha):
446486
# docstring inherited
447487
super().set_alpha(alpha)
@@ -618,6 +658,17 @@ def get_hatch_linewidth(self):
618658
"""Return the hatch linewidth."""
619659
return self._hatch_linewidth
620660

661+
def _has_dashed_edge(self):
662+
"""
663+
Return whether the patch edge has a dashed linestyle.
664+
665+
A custom linestyle is assumed to be dashed, we do not inspect the
666+
``onoffseq`` directly.
667+
668+
See also `~.Patch.set_linestyle`.
669+
"""
670+
return self._linestyle not in ('solid', '-')
671+
621672
def _draw_paths_with_artist_properties(
622673
self, renderer, draw_path_args_list):
623674
"""
@@ -632,13 +683,10 @@ def _draw_paths_with_artist_properties(
632683
renderer.open_group('patch', self.get_gid())
633684
gc = renderer.new_gc()
634685

635-
gc.set_foreground(self._edgecolor, isRGBA=True)
636-
637686
lw = self._linewidth
638687
if self._edgecolor[3] == 0 or self._linestyle == 'None':
639688
lw = 0
640689
gc.set_linewidth(lw)
641-
gc.set_dashes(*self._dash_pattern)
642690
gc.set_capstyle(self._capstyle)
643691
gc.set_joinstyle(self._joinstyle)
644692

@@ -661,6 +709,18 @@ def _draw_paths_with_artist_properties(
661709
from matplotlib.patheffects import PathEffectRenderer
662710
renderer = PathEffectRenderer(self.get_path_effects(), renderer)
663711

712+
# Draw the gaps first if gapcolor is set
713+
if self._has_dashed_edge() and self._gapcolor is not None:
714+
gc.set_foreground(self._gapcolor, isRGBA=True)
715+
offset_gaps, gaps = mlines._get_inverse_dash_pattern(
716+
*self._dash_pattern)
717+
gc.set_dashes(offset_gaps, gaps)
718+
for draw_path_args in draw_path_args_list:
719+
renderer.draw_path(gc, *draw_path_args)
720+
721+
# Draw the main edge
722+
gc.set_foreground(self._edgecolor, isRGBA=True)
723+
gc.set_dashes(*self._dash_pattern)
664724
for draw_path_args in draw_path_args_list:
665725
renderer.draw_path(gc, *draw_path_args)
666726

lib/matplotlib/patches.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class Patch(artist.Artist):
2626
capstyle: CapStyleType | None = ...,
2727
joinstyle: JoinStyleType | None = ...,
2828
hatchcolor: ColorType | Literal["edge"] | None = ...,
29+
edgegapcolor: ColorType | None = ...,
2930
**kwargs,
3031
) -> None: ...
3132
def get_verts(self) -> ArrayLike: ...
@@ -43,13 +44,15 @@ class Patch(artist.Artist):
4344
def get_antialiased(self) -> bool: ...
4445
def get_edgecolor(self) -> ColorType: ...
4546
def get_facecolor(self) -> ColorType: ...
47+
def get_edgegapcolor(self) -> ColorType | None: ...
4648
def get_hatchcolor(self) -> ColorType: ...
4749
def get_linewidth(self) -> float: ...
4850
def get_linestyle(self) -> LineStyleType: ...
4951
def set_antialiased(self, aa: bool | None) -> None: ...
5052
def set_edgecolor(self, color: ColorType | None) -> None: ...
5153
def set_facecolor(self, color: ColorType | None) -> None: ...
5254
def set_color(self, c: ColorType | None) -> None: ...
55+
def set_edgegapcolor(self, edgegapcolor: ColorType | None) -> None: ...
5356
def set_hatchcolor(self, color: ColorType | Literal["edge"] | None) -> None: ...
5457
def set_alpha(self, alpha: float | None) -> None: ...
5558
def set_linewidth(self, w: float | None) -> None: ...
11.5 KB
Loading

lib/matplotlib/tests/test_patches.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,3 +1101,81 @@ def test_empty_fancyarrow():
11011101
fig, ax = plt.subplots()
11021102
arrow = ax.arrow([], [], [], [])
11031103
assert arrow is not None
1104+
1105+
1106+
def test_patch_edgegapcolor_getter_setter():
1107+
"""Test that edgegapcolor can be set and retrieved."""
1108+
patch = Rectangle((0, 0), 1, 1)
1109+
# Default is None
1110+
assert patch.get_edgegapcolor() is None
1111+
1112+
# Set to a color
1113+
patch.set_edgegapcolor('red')
1114+
assert mcolors.same_color(patch.get_edgegapcolor(), 'red')
1115+
1116+
# Set back to None
1117+
patch.set_edgegapcolor(None)
1118+
assert patch.get_edgegapcolor() is None
1119+
1120+
1121+
def test_patch_edgegapcolor_init():
1122+
"""Test that edgegapcolor can be passed in __init__."""
1123+
patch = Rectangle((0, 0), 1, 1, edgegapcolor='blue')
1124+
assert mcolors.same_color(patch.get_edgegapcolor(), 'blue')
1125+
1126+
1127+
def test_patch_has_dashed_edge():
1128+
"""Test _has_dashed_edge method for patches."""
1129+
patch = Rectangle((0, 0), 1, 1)
1130+
patch.set_linestyle('solid')
1131+
assert not patch._has_dashed_edge()
1132+
1133+
patch.set_linestyle('--')
1134+
assert patch._has_dashed_edge()
1135+
1136+
patch.set_linestyle(':')
1137+
assert patch._has_dashed_edge()
1138+
1139+
patch.set_linestyle('-.')
1140+
assert patch._has_dashed_edge()
1141+
1142+
# Test custom linestyle
1143+
patch.set_linestyle((0, (2, 2, 10, 2)))
1144+
assert patch._has_dashed_edge()
1145+
1146+
1147+
def test_patch_edgegapcolor_update_from():
1148+
"""Test that edgegapcolor is copied in update_from."""
1149+
patch1 = Rectangle((0, 0), 1, 1, edgegapcolor='green')
1150+
patch2 = Rectangle((1, 1), 2, 2)
1151+
1152+
patch2.update_from(patch1)
1153+
assert mcolors.same_color(patch2.get_edgegapcolor(), 'green')
1154+
1155+
1156+
@image_comparison(['patch_edgegapcolor.png'], remove_text=True, style='mpl20')
1157+
def test_patch_edgegapcolor_visual():
1158+
"""Visual test for patch edgegapcolor (striped edges)."""
1159+
fig, ax = plt.subplots()
1160+
1161+
# Rectangle with edgegapcolor
1162+
rect = Rectangle((0.1, 0.1), 0.3, 0.3, fill=False,
1163+
edgecolor='blue', edgegapcolor='orange',
1164+
linestyle='--', linewidth=3)
1165+
ax.add_patch(rect)
1166+
1167+
# Ellipse with edgegapcolor
1168+
ellipse = Ellipse((0.7, 0.3), 0.3, 0.2, fill=False,
1169+
edgecolor='red', edgegapcolor='yellow',
1170+
linestyle=':', linewidth=3)
1171+
ax.add_patch(ellipse)
1172+
1173+
# Polygon with edgegapcolor
1174+
polygon = Polygon([[0.1, 0.6], [0.3, 0.9], [0.4, 0.6]], fill=False,
1175+
edgecolor='green', edgegapcolor='purple',
1176+
linestyle='-.', linewidth=3)
1177+
ax.add_patch(polygon)
1178+
1179+
ax.set_xlim(0, 1)
1180+
ax.set_ylim(0, 1)
1181+
ax.set_aspect('equal')

0 commit comments

Comments
 (0)