8000 Separates edgecolor from hatchcolor (#28104) · matplotlib/matplotlib@c11175d · GitHub
[go: up one dir, main page]

Skip to content

Commit c11175d

Browse files
Impaler343r3kstetimhoffm
authored
Separates edgecolor from hatchcolor (#28104)
* separated hatchcolor from edgecolor in patches * fixed logic for inherit in collections * added a gallery example * made suggested changes * reverted use of `inherit` * fixed the logic for inheriting from edgecolor * fixed docs and enhanced tests * Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * made suggested changes * minor fix * use 'edge' instead of 'inherit' * fix rcsetup * enhanced docs and added note for follow-up PR * Made suggested changes * made suggested changes * enhanced tests and minor fix in docs * minor changes to docs * Changed wording and alt text * Minor Fix --------- Co-authored-by: anTon <138380708+r3kste@users.noreply.github.com> Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
1 parent c2d502d commit c11175d

File tree

11 files changed

+225
-11
lines changed

11 files changed

+225
-11
lines changed
Lines changed: 59 additions & 0 deletions
48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Separated ``hatchcolor`` from ``edgecolor``
2+
-------------------------------------------
3+
4+
When the *hatchcolor* parameter is specified, it will be used for the hatch.
5+
If it is not specified, it will fall back to using :rc:`hatch.color`.
6+
The special value 'edge' uses the patch edgecolor, with a fallback to
7+
:rc:`patch.edgecolor` if the patch edgecolor is 'none'.
8+
Previously, hatch colors were the same as edge colors, with a fallback to
9+
:rc:`hatch.color` if the patch did not have an edge color.
10+
11+
.. plot::
12+
:include-source: true
13+
:alt: Four Rectangle patches, each displaying the color of hatches in different specifications of edgecolor and hatchcolor. Top left has hatchcolor='black' representing the default value when both hatchcolor and edgecolor are not set, top right has edgecolor='blue' and hatchcolor='black' which remains when the edgecolor is set again, bottom left has edgecolor='red' and hatchcolor='orange' on explicit specification and bottom right has edgecolor='green' and hatchcolor='green' when the hatchcolor is not set.
14+
15+
import matplotlib as mpl
16+
import matplotlib.pyplot as plt
17+
from matplotlib.patches import Rectangle
18+
19+
fig, ax = plt.subplots()
20+
21+
# In this case, hatchcolor is orange
22+
patch1 = Rectangle((0.1, 0.1), 0.3, 0.3, edgecolor='red', linewidth=2,
23+
hatch='//', hatchcolor='orange')
24+
ax.add_patch(patch1)
25+
26+
# When hatchcolor is not specified, it matches edgecolor
27+
# In this case, hatchcolor is green
28+
patch2 = Rectangle((0.6, 0.1), 0.3, 0.3, edgecolor='green', linewidth=2,
29+
hatch='//', facecolor='none')
30+
ax.add_patch(patch2)
31+
32+
# If both hatchcolor and edgecolor are not specified
33+
# it will default to the 'patch.edgecolor' rcParam, which is black by default
34+
# In this case, hatchcolor is black
35+
patch3 = Rectangle((0.1, 0.6), 0.3, 0.3, hatch='//')
36+
ax.add_patch(patch3)
37+
38+
# When using `hatch.color` in the `rcParams`
39+
# edgecolor will now not overwrite hatchcolor
40+
# In this case, hatchcolor is black
41+
with plt.rc_context({'hatch.color': 'black'}):
42+
patch4 = Rectangle((0.6, 0.6), 0.3, 0.3, edgecolor='blue', linewidth=2,
43+
hatch='//', facecolor='none')
44+
45+
# hatchcolor is black (it uses the `hatch.color` rcParam value)
46+
patch4.set_edgecolor('blue')
47+
# hatchcolor is still black (here, it does not update when edgecolor changes)
+
ax.add_patch(patch4)
49+
50+
ax.annotate("hatchcolor = 'orange'",
51+
xy=(.5, 1.03), xycoords=patch1, ha='center', va='bottom')
52+
ax.annotate("hatch color unspecified\nedgecolor='green'",
53+
xy=(.5, 1.03), xycoords=patch2, ha='center', va='bottom')
54+
ax.annotate("hatch color unspecified\nusing patch.edgecolor",
55+
xy=(.5, 1.03), xycoords=patch3, ha='center', va='bottom')
56+
ax.annotate("hatch.color='black'",
57+
xy=(.5, 1.03), xycoords=patch4, ha='center', va='bottom')
58+
59+
plt.show()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
================
3+
Patch hatchcolor
4+
================
5+
6+
This example shows how to use the *hatchcolor* parameter to set the color of
7+
the hatch. The *hatchcolor* parameter is available for `~.patches.Patch`,
8+
child classes of Patch, and methods that pass through to Patch.
9+
"""
10+
11+
import matplotlib.pyplot as plt
12+
import numpy as np
13+
14+
from matplotlib.patches import Rectangle
15+
16+
fig, (ax1, ax2) = plt.subplots(1, 2)
17+
18+
# Rectangle with red hatch color and black edge color
19+
ax1.add_patch(Rectangle((0.1, 0.5), 0.8, 0.3, hatch=".", hatchcolor='red',
20+
edgecolor='black', lw=2))
21+
# If hatchcolor is not passed, the hatch will match the edge color
22+
ax1.add_patch(Rectangle((0.1, 0.1), 0.8, 0.3, hatch='x', edgecolor='orange', lw=2))
23+
24+
x = np.arange(1, 5)
25+
y = np.arange(1, 5)
26+
27+
ax2.bar(x, y, facecolor='none', edgecolor='red', hatch='//', hatchcolor='blue')
28+
ax2.set_xlim(0, 5)
29+
ax2.set_ylim(0, 5)
30+
31+
plt.show()
32+
33+
# %%
34+
#
35+
# .. admonition:: References
36+
#
37+
# The use of the following functions, methods, classes and modules is shown
38+
# in this example:
39+
#
40+
# - `matplotlib.patches`
41+
# - `matplotlib.patches.Polygon`
42+
# - `matplotlib.axes.Axes.add_patch`
43+
# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar`

lib/matplotlib/backend_bases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ def __init__(self):
690690
self._linewidth = 1
691691
self._rgb = (0.0, 0.0, 0.0, 1.0)
692692
self._hatch = None
693-
self._hatch_color = colors.to_rgba(rcParams['hatch.color'])
693+
self._hatch_color = None
694694
self._hatch_linewidth = rcParams['hatch.linewidth']
695695
self._url = None
696696
self._gid = None

lib/matplotlib/collections.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,13 @@ def __init__(self, *,
174174
self._face_is_mapped = None
175175
self._edge_is_mapped = None
176176
self._mapped_colors = None # calculated in update_scalarmappable
177-
self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color'])
177+
178+
# Temporary logic to set hatchcolor. This eager resolution is temporary
179+
# and will be replaced by a proper mechanism in a follow-up PR.
180+
hatch_color = mpl.rcParams['hatch.color']
181+
if hatch_color == 'edge':
182+
hatch_color = mpl.rcParams['patch.edgecolor']
183+
self._hatch_color = mcolors.to_rgba(hatch_color)
178184
self._hatch_linewidth = mpl.rcParams['hatch.linewidth']
179185
self.set_facecolor(facecolors)
180186
self.set_edgecolor(edgecolors)

lib/matplotlib/mpl-data/matplotlibrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
## ***************************************************************************
163163
## * HATCHES *
164164
## ***************************************************************************
165-
#hatch.color: black
165+
#hatch.color: edge
166166
#hatch.linewidth: 1.0
167167

168168

lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
text.kerning_factor : 6
55

66
ytick.alignment: center_baseline
7+
8+
hatch.color: edge

lib/matplotlib/patches.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(self, *,
5656
fill=True,
5757
capstyle=None,
5858
joinstyle=None,
59+
hatchcolor=None,
5960
**kwargs):
6061
"""
6162
The following kwarg properties are supported
@@ -71,7 +72,6 @@ def __init__(self, *,
7172
if joinstyle is None:
7273
joinstyle = JoinStyle.miter
7374

74-
self._hatch_color = colors.to_rgba(mpl.rcParams['hatch.color'])
7575
self._hatch_linewidth = mpl.rcParams['hatch.linewidth']
7676
self._fill = bool(fill) # needed for set_facecolor call
7777
if color is not None:
@@ -82,6 +82,7 @@ def __init__(self, *,
8282
self.set_color(color)
8383
else:
8484
self.set_edgecolor(edgecolor)
85+
self.set_hatchcolor(hatchcolor)
8586
self.set_facecolor(facecolor)
8687

8788
self._linewidth = 0
@@ -291,6 +292,7 @@ def update_from(self, other):
291292
self._fill = other._fill
292293
self._hatch = other._hatch
293294
self._hatch_color = other._hatch_color
295+
self._original_hatchcolor = other._original_hatchcolor
294296
self._unscaled_dash_pattern = other._unscaled_dash_pattern
295297
self.set_linewidth(other._linewidth) # also sets scaled dashes
296298
self.set_transform(other.get_data_transform())
@@ -338,6 +340,14 @@ def get_facecolor(self):
338340
"""Return the face color."""
339341
return self._facecolor
340342

343+
def get_hatchcolor(self):
344+
"""Return the hatch color."""
345+
if self._hatch_color == 'edge':
346+
if self._edgecolor[3] == 0: # fully transparent
347+
return colors.to_rgba(mpl.rcParams['patch.edgecolor'])
348+
return self.get_edgecolor()
349+
return self._hatch_color
350+
341351
def get_linewidth(self):
342352
"""Return the line width in points."""
343353
return self._linewidth
@@ -358,18 +368,14 @@ def set_antialiased(self, aa):
358368
self.stale = True
359369

360370
def _set_edgecolor(self, color):
361-
set_hatch_color = True
362371
if color is None:
363372
if (mpl.rcParams['patch.force_edgecolor'] or
364373
not self._fill or self._edge_default):
365374
color = mpl.rcParams['patch.edgecolor']
366375
else:
367376
color = 'none'
368-
set_hatch_color = False
369377

370378
self._edgecolor = colors.to_rgba(color, self._alpha)
371-
if set_hatch_color:
372-
self._hatch_color = self._edgecolor
373379
self.stale = True
374380

375381
def set_edgecolor(self, color):
@@ -413,14 +419,37 @@ def set_color(self, c):
413419
Patch.set_facecolor, Patch.set_edgecolor
414420
For setting the edge or face color individually.
415421
"""
416-
self.set_facecolor(c)
417422
self.set_edgecolor(c)
423+
self.set_hatchcolor(c)
424+
self.set_facecolor(c)
425+
426+
def _set_hatchcolor(self, color):
427+
color = mpl._val_or_rc(color, 'hatch.color')
428+
if color == 'edge':
429+
self._hatch_color = 'edge'
430+
else:
431+
self._hatch_color = colors.to_rgba(color, self._alpha)
432+
self.stale = True
433+
434+
def set_hatchcolor(self, color):
435+
"""
436+
Set the patch hatch color.
437+
438+
Parameters
439+
----------
440+
color : :mpltype:`color` or 'edge' or None
441+
"""
442+
if cbook._str_equal(color, 'edge'):
443+
color = 'edge'
444+
self._original_hatchcolor = color
445+
self._set_hatchcolor(color)
418446

419447
def set_alpha(self, alpha):
420448
# docstring inherited
421449
super().set_alpha(alpha)
422450
self._set_facecolor(self._original_facecolor)
423451
self._set_edgecolor(self._original_edgecolor)
452+
self._set_hatchcolor(self._original_hatchcolor)
424453
# stale is already True
425454

426455
def set_linewidth(self, w):
@@ -482,6 +511,7 @@ def set_fill(self, b):
482511
self._fill = bool(b)
483512
self._set_facecolor(self._original_facecolor)
484513
self._set_edgecolor(self._original_edgecolor)
514+
self._set_hatchcolor(self._original_hatchcolor)
485515
self.stale = True
486516

487517
def get_fill(self):
@@ -608,7 +638,7 @@ def _draw_paths_with_artist_properties(
608638

609639
if self._hatch:
610640
gc.set_hatch(self._hatch)
611-
gc.set_hatch_color(self._hatch_color)
641+
gc.set_hatch_color(self.get_hatchcolor())
612642
gc.set_hatch_linewidth(self._hatch_linewidth)
613643

614644
if self.get_sketch_params() is not None:

lib/matplotlib/patches.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Patch(artist.Artist):
2525
fill: bool = ...,
2626
capstyle: CapStyleType | None = ...,
2727
joinstyle: JoinStyleType | None = ...,
28+
hatchcolor: ColorType | None = ...,
2829
**kwargs,
2930
) -> None: ...
3031
def get_verts(self) -> ArrayLike: ...
@@ -42,12 +43,14 @@ class Patch(artist.Artist):
4243
def get_antialiased(self) -> bool: ...
4344
def get_edgecolor(self) -> ColorType: ...
4445
def get_facecolor(self) -> ColorType: ...
46+
def get_hatchcolor(self) -> ColorType: ...
4547
def get_linewidth(self) -> float: ...
4648
def get_linestyle(self) -> LineStyleType: ...
4749
def set_antialiased(self, aa: bool | None) -> None: ...
4850
def set_edgecolor(self, color: ColorType | None) -> None: ...
4951
def set_facecolor(self, color: ColorType | None) -> None: ...
5052
def set_color(self, c: ColorType | None) -> None: ...
53+
def set_hatchcolor(self, color: ColorType | Literal["edge"] | None) -> None: ...
5154
def set_alpha(self, alpha: float | None) -> None: ...
5255
def set_linewidth(self, w: float | None) -> None: ...
5356
def set_linestyle(self, ls: LineStyleType | None) -> None: ...

lib/matplotlib/rcsetup.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,12 @@ def validate_color_or_auto(s):
301301
return validate_color(s)
302302

303303

304+
def _validate_color_or_edge(s):
305+
if cbook._str_equal(s, 'edge'):
306+
return s
307+
return validate_color(s)
308+
309+
304310
def validate_color_for_prop_cycle(s):
305311
# N-th color cycle syntax can't go into the color cycle.
306312
if isinstance(s, str) and re.match("^C[0-9]$", s):
@@ -950,7 +956,7 @@ def _convert_validator_spec(key, conv):
950956
"patch.antialiased": validate_bool, # antialiased (no jaggies)
951957

952958
## hatch props
953-
"hatch.color": validate_color,
959+
"hatch.color": _validate_color_or_edge,
954960
"hatch.linewidth": validate_float,
955961

956962
## Histogram properties

lib/matplotlib/rcsetup.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ _auto_backend_sentinel: object
4848
def validate_backend(s: Any) -> str: ...
4949 F438
def validate_color_or_inherit(s: Any) -> Literal["inherit"] | ColorType: ...
5050
def validate_color_or_auto(s: Any) -> ColorType | Literal["auto"]: ...
51+
def _validate_color_or_edge(s: Any) -> ColorType | Literal["edge"]: ...
5152
def validate_color_for_prop_cycle(s: Any) -> ColorType: ...
5253
def validate_color(s: Any) -> ColorType: ...
5354
def validate_colorlist(s: Any) -> list[ColorType]: ...

lib/matplotlib/tests/test_patches.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,3 +999,67 @@ def test_set_and_get_hatch_linewidth(fig_test, fig_ref):
999999

10001000
assert ax_ref.patches[0].get_hatch_linewidth() == lw
10011001
assert ax_test.patches[0].get_hatch_linewidth() == lw
1002+
1003+
1004+
def test_patch_hatchcolor_inherit_logic():
1005+
with mpl.rc_context({'hatch.color': 'edge'}):
1006+
# Test for when edgecolor and hatchcolor is set
1007+
rect = Rectangle((0, 0), 1, 1, hatch='//', ec='red',
1008+
hatchcolor='yellow')
1009+
assert mcolors.same_color(rect.get_edgecolor(), 'red')
1010+
assert mcolors.same_color(rect.get_hatchcolor(), 'yellow')
1011+
1012+
# Test for explicitly setting edgecolor and then hatchcolor
1013+
rect = Rectangle((0, 0), 1, 1, hatch='//')
1014+
rect.set_edgecolor('orange')
1015+
assert mcolors.same_color(rect.get_hatchcolor(), 'orange')
1016+
rect.set_hatchcolor('cyan')
1017+
assert mcolors.same_color(rect.get_hatchcolor(), 'cyan')
1018+
1019+
# Test for explicitly setting hatchcolor and then edgecolor
1020+
rect = Rectangle((0, 0), 1, 1, hatch='//')
1021+
rect.set_hatchcolor('purple')
1022+
assert mcolors.same_color(rect.get_hatchcolor(), 'purple')
1023+
rect.set_edgecolor('green')
1024+
assert mcolors.same_color(rect.get_hatchcolor(), 'purple')
1025+
1026+
1027+
def test_patch_hatchcolor_fallback_logic():
1028+
# Test for when hatchcolor parameter is passed
1029+
rect = Rectangle((0, 0), 1, 1, hatch='//', hatchcolor='green')
1030+
assert mcolors.same_color(rect.get_hatchcolor(), 'green')
1031+
1032+
# Test that hatchcolor parameter takes precedence over rcParam
1033+
# When edgecolor is not set
1034+
with mpl.rc_context({'hatch.color': 'blue'}):
1035+
rect = Rectangle((0, 0), 1, 1, hatch='//', hatchcolor='green')
1036+
assert mcolors.same_color(rect.get_hatchcolor(), 'green')
1037+
# When edgecolor is set
1038+
with mpl.rc_context({'hatch.color': 'yellow'}):
1039+
rect = Rectangle((0, 0), 1, 1, hatch='//', hatchcolor='green', edgecolor='red')
1040+
assert mcolors.same_color(rect.get_hatchcolor(), 'green')
1041+
1042+
# Test that hatchcolor is not overridden by edgecolor when
1043+
# hatchcolor parameter is not passed and hatch.color rcParam is set to a color
1044+
# When edgecolor is not set
1045+
with mpl.rc_context({'hatch.color': 'blue'}):
1046+
rect = Rectangle((0, 0), 1, 1, hatch='//')
1047+
assert mcolors.same_color(rect.get_hatchcolor(), 'blue')
1048+
# When edgecolor is set
1049+
with mpl.rc_context({'hatch.color': 'blue'}):
1050+
rect = Rectangle((0, 0), 1, 1, hatch='//', edgecolor='red')
1051+
assert mcolors.same_color(rect.get_hatchcolor(), 'blue')
1052+
1053+
# Test that hatchcolor matches edgecolor when
1054+
# hatchcolor parameter is not passed and hatch.color rcParam is set to 'edge'
1055+
with mpl.rc_context({'hatch.color': 'edge'}):
1056+
rect = Rectangle((0, 0), 1, 1, hatch='//', edgecolor='red')
1057+
assert mcolors.same_color(rect.get_hatchcolor(), 'red')
1058+
# hatchcolor parameter is set to 'edge'
1059+
rect = Rectangle((0, 0), 1, 1, hatch='//', hatchcolor='edge', edgecolor='orange')
1060+
assert mcolors.same_color(rect.get_hatchcolor(), 'orange')
1061+
1062+
# Test for default hatchcolor when hatchcolor parameter is not passed and
1063+
# hatch.color rcParam is set to 'edge' and edgecolor is not set
1064+
rect = Rectangle((0, 0), 1, 1, hatch='//')
1065+
assert mcolors.same_color(rect.get_hatchcolor(), mpl.rcParams['patch.edgecolor'])

0 commit comments

Comments
 (0)
0