Skip to content

Commit d7da1fc

Browse files
committed
add hatchcolors param for collections
1 parent 91f52bc commit d7da1fc

File tree

11 files changed

+190
-51
lines changed

11 files changed

+190
-51
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path,
208208
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
209209
offsets, offset_trans, facecolors, edgecolors,
210210
linewidths, linestyles, antialiaseds, urls,
211-
offset_position):
211+
offset_position, hatchcolors=None):
212212
"""
213213
Draw a collection of *paths*.
214214
@@ -217,8 +217,8 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
217217
*master_transform*. They are then translated by the corresponding
218218
entry in *offsets*, which has been first transformed by *offset_trans*.
219219
220-
*facecolors*, *edgecolors*, *linewidths*, *linestyles*, and
221-
*antialiased* are lists that set the corresponding properties.
220+
*facecolors*, *edgecolors*, *linewidths*, *linestyles*, *antialiased*
221+
and *hatchcolors* are lists that set the corresponding properties.
222222
223223
*offset_position* is unused now, but the argument is kept for
224224
backwards compatibility.
@@ -235,10 +235,13 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
235235
path_ids = self._iter_collection_raw_paths(master_transform,
236236
paths, all_transforms)
237237

238+
if hatchcolors is None:
239+
hatchcolors = []
240+
238241
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
239242
gc, list(path_ids), offsets, offset_trans,
240243
facecolors, edgecolors, linewidths, linestyles,
241-
antialiaseds, urls, offset_position):
244+
antialiaseds, urls, offset_position, hatchcolors):
242245
path, transform = path_id
243246
# Only apply another translation if we have an offset, else we
244247
# reuse the initial transform.
@@ -252,7 +255,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
252255

253256
def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
254257
coordinates, offsets, offsetTrans, facecolors,
255-
antialiased, edgecolors):
258+
antialiased, edgecolors, hatchcolors=None):
256259
"""
257260
Draw a quadmesh.
258261
@@ -265,11 +268,13 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
265268

266269
if edgecolors is None:
267270
edgecolors = facecolors
271+
if hatchcolors is None:
272+
hatchcolors = []
268273
linewidths = np.array([gc.get_linewidth()], float)
269274

270275
return self.draw_path_collection(
271276
gc, master_transform, paths, [], offsets, offsetTrans, facecolors,
272-
edgecolors, linewidths, [], [antialiased], [None], 'screen')
277+
edgecolors, linewidths, [], [antialiased], [None], 'screen', hatchcolors)
273278

274279
def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
275280
transform):
@@ -337,7 +342,7 @@ def _iter_collection_uses_per_path(self, paths, all_transforms,
337342

338343
def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
339344
edgecolors, linewidths, linestyles,
340-
antialiaseds, urls, offset_position):
345+
antialiaseds, urls, offset_position, hatchcolors=None):
341346
"""
342347
Helper method (along with `_iter_collection_raw_paths`) to implement
343348
`draw_path_collection` in a memory-efficient manner.
@@ -360,16 +365,20 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
360365
*path_ids*; *gc* is a graphics context and *rgbFace* is a color to
361366
use for filling the path.
362367
"""
368+
if hatchcolors is None:
369+
hatchcolors = []
370+
363371
Npaths = len(path_ids)
364372
Noffsets = len(offsets)
365373
N = max(Npaths, Noffsets)
366374
Nfacecolors = len(facecolors)
367375
Nedgecolors = len(edgecolors)
376+
Nhatchcolors = len(hatchcolors)
368377
Nlinewidths = len(linewidths)
369378
Nlinestyles = len(linestyles)
370379
Nurls = len(urls)
371380

372-
if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0:
381+
if (Nfacecolors == 0 and Nedgecolors == 0 and Nhatchcolors == 0) or Npaths == 0:
373382
return
374383

375384
gc0 = self.new_gc()
@@ -384,6 +393,7 @@ def cycle_or_default(seq, default=None):
384393
toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0))
385394
fcs = cycle_or_default(facecolors)
386395
ecs = cycle_or_default(edgecolors)
396+
hcs = cycle_or_default(hatchcolors)
387397
lws = cycle_or_default(linewidths)
388398
lss = cycle_or_default(linestyles)
389399
aas = cycle_or_default(antialiaseds)
@@ -392,8 +402,8 @@ def cycle_or_default(seq, default=None):
392402
if Nedgecolors == 0:
393403
gc0.set_linewidth(0.0)
394404

395-
for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice(
396-
zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N):
405+
for pathid, (xo, yo), fc, ec, hc, lw, ls, aa, url in itertools.islice(
406+
zip(pathids, toffsets, fcs, ecs, hcs, lws, lss, aas, urls), N):
397407
if not (np.isfinite(xo) and np.isfinite(yo)):
398408
continue
399409
if Nedgecolors:
@@ -405,6 +415,8 @@ def cycle_or_default(seq, default=None):
405415
gc0.set_linewidth(0)
406416
else:
407417
gc0.set_foreground(ec)
418+
if Nhatchcolors:
419+
gc0.set_hatch_color(hc)
408420
if fc is not None and len(fc) == 4 and fc[3] == 0:
409421
fc = None
410422
gc0.set_antialiased(aa)

lib/matplotlib/backend_bases.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class RendererBase:
6363
antialiaseds: bool | Sequence[bool],
6464
urls: str | Sequence[str],
6565
offset_position: Any,
66+
hatchcolors: ColorType | Sequence[ColorType] | None = None,
6667
) -> None: ...
6768
def draw_quad_mesh(
6869
self,
@@ -76,6 +77,7 @@ class RendererBase:
7677
facecolors: Sequence[ColorType],
7778
antialiased: bool,
7879
edgecolors: Sequence[ColorType] | ColorType | None,
80+
hatchcolors: Sequence[ColorType] | ColorType | None = None,
7981
) -> None: ...
8082
def draw_gouraud_triangles(
8183
self,

lib/matplotlib/backends/backend_pdf.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,14 +2030,17 @@ def draw_path(self, gc, path, transform, rgbFace=None):
20302030
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
20312031
offsets, offset_trans, facecolors, edgecolors,
20322032
linewidths, linestyles, antialiaseds, urls,
2033-
offset_position):
2033+
offset_position, hatchcolors=None):
20342034
# We can only reuse the objects if the presence of fill and
20352035
# stroke (and the amount of alpha for each) is the same for
20362036
# all of them
20372037
can_do_optimization = True
20382038
facecolors = np.asarray(facecolors)
20392039
edgecolors = np.asarray(edgecolors)
20402040

2041+
if hatchcolors is None:
2042+
hatchcolors = []
2043+
20412044
if not len(facecolors):
20422045
filled = False
20432046
can_do_optimization = not gc.get_hatch()
@@ -2072,7 +2075,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
20722075
self, gc, master_transform, paths, all_transforms,
20732076
offsets, offset_trans, facecolors, edgecolors,
20742077
linewidths, linestyles, antialiaseds, urls,
2075-
offset_position)
2078+
offset_position, hatchcolors)
20762079

20772080
padding = np.max(linewidths)
20782081
path_codes = []
@@ -2088,7 +2091,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
20882091
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
20892092
gc, path_codes, offsets, offset_trans,
20902093
facecolors, edgecolors, linewidths, linestyles,
2091-
antialiaseds, urls, offset_position):
2094+
antialiaseds, urls, offset_position, hatchcolors):
20922095

20932096
self.check_gc(gc0, rgbFace)
20942097
dx, dy = xo - lastx, yo - lasty

lib/matplotlib/backends/backend_ps.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,9 @@ def draw_markers(
674674
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
675675
offsets, offset_trans, facecolors, edgecolors,
676676
linewidths, linestyles, antialiaseds, urls,
677-
offset_position):
677+
offset_position, hatchcolors=None):
678+
if hatchcolors is None:
679+
hatchcolors = []
678680
# Is the optimization worth it? Rough calculation:
679681
# cost of emitting a path in-line is
680682
# (len_path + 2) * uses_per_path
@@ -690,7 +692,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
690692
self, gc, master_transform, paths, all_transforms,
691693
offsets, offset_trans, facecolors, edgecolors,
692694
linewidths, linestyles, antialiaseds, urls,
693-
offset_position)
695+
offset_position, hatchcolors)
694696

695697
path_codes = []
696698
for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
@@ -709,7 +711,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
709711
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
710712
gc, path_codes, offsets, offset_trans,
711713
facecolors, edgecolors, linewidths, linestyles,
712-
antialiaseds, urls, offset_position):
714+
antialiaseds, urls, offset_position, hatchcolors):
713715
ps = f"{xo:g} {yo:g} {path_id}"
714716
self._draw_ps(ps, gc0, rgbFace)
715717

lib/matplotlib/backends/backend_svg.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,9 @@ def draw_markers(
736736
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
737737
offsets, offset_trans, facecolors, edgecolors,
738738
linewidths, linestyles, antialiaseds, urls,
739-
offset_position):
739+
offset_position, hatchcolors=None):
740+
if hatchcolors is None:
741+
hatchcolors = []
740742
# Is the optimization worth it? Rough calculation:
741743
# cost of emitting a path in-line is
742744
# (len_path + 5) * uses_per_path
@@ -752,7 +754,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
752754
gc, master_transform, paths, all_transforms,
753755
offsets, offset_trans, facecolors, edgecolors,
754756
linewidths, linestyles, antialiaseds, urls,
755-
offset_position)
757+
offset_position, hatchcolors)
756758

757759
writer = self.writer
758760
path_codes = []
@@ -770,7 +772,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
770772
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
771773
gc, path_codes, offsets, offset_trans,
772774
facecolors, edgecolors, linewidths, linestyles,
773-
antialiaseds, urls, offset_position):
775+
antialiaseds, urls, offset_position, hatchcolors):
774776
url = gc0.get_url()
775777
if url is not None:
776778
writer.start('a', attrib={'xlink:href': url})

lib/matplotlib/collections.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class Collection(mcolorizer.ColorizingArtist):
7979
def __init__(self, *,
8080
edgecolors=None,
8181
facecolors=None,
82+
hatchcolors=None,
8283
linewidths=None,
8384
linestyles='solid',
8485
capstyle=None,
@@ -174,13 +175,6 @@ def __init__(self, *,
174175
self._face_is_mapped = None
175176
self._edge_is_mapped = None
176177
self._mapped_colors = None # calculated in update_scalarmappable
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)
184178
self._hatch_linewidth = mpl.rcParams['hatch.linewidth']
185179
self.set_facecolor(facecolors)
186180
self.set_edgecolor(edgecolors)
@@ -190,6 +184,7 @@ def __init__(self, *,
190184
self.set_pickradius(pickradius)
191185
self.set_urls(urls)
192186
self.set_hatch(hatch)
187+
self.set_hatchcolor(hatchcolors)
193188
self.set_zorder(zorder)
194189

195190
if capstyle:
@@ -371,7 +366,6 @@ def draw(self, renderer):
371366

372367
if self._hatch:
373368
gc.set_hatch(self._hatch)
374-
gc.set_hatch_color(self._hatch_color)
375369
gc.set_hatch_linewidth(self._hatch_linewidth)
376370

377371
if self.get_sketch_params() is not None:
@@ -431,15 +425,16 @@ def draw(self, renderer):
431425
[mcolors.to_rgba("none")], self._gapcolor,
432426
self._linewidths, ilinestyles,
433427
self._antialiaseds, self._urls,
434-
"screen")
428+
"screen", self.get_hatchcolor())
435429

436430
renderer.draw_path_collection(
437431
gc, transform.frozen(), paths,
438432
self.get_transforms(), offsets, offset_trf,
439433
self.get_facecolor(), self.get_edgecolor(),
440434
self._linewidths, self._linestyles,
441435
self._antialiaseds, self._urls,
442-
"screen") # offset_position, kept for backcompat.
436+
"screen", # offset_position, kept for backcompat.
437+
self.get_hatchcolor())
443438

444439
gc.restore()
445440
renderer.close_group(self.__class__.__name__)
@@ -814,23 +809,27 @@ def _get_default_edgecolor(self):
814809
# This may be overridden in a subclass.
815810
return mpl.rcParams['patch.edgecolor']
816811

812+
def get_hatchcolor(self):
813+
if cbook._str_equal(self._hatchcolors, 'edge'):
814+
if self.get_edgecolor().size == 0:
815+
return mpl.colors.to_rgba_array(self._get_default_edgecolor(),
816+
self._alpha)
817+
return self.get_edgecolor()
818+
return self._hatchcolors
819+
817820
def _set_edgecolor(self, c):
818-
set_hatch_color = True
819821
if c is None:
820822
if (mpl.rcParams['patch.force_edgecolor']
821823
or self._edge_default
822824
or cbook._str_equal(self._original_facecolor, 'none')):
823825
c = self._get_default_edgecolor()
824826
else:
825827
c = 'none'
826-
set_hatch_color = False
827828
if cbook._str_lower_equal(c, 'face'):
828829
self._edgecolors = 'face'
829830
self.stale = True
830831
return
831832
self._edgecolors = mcolors.to_rgba_array(c, self._alpha)
832-
if set_hatch_color and len(self._edgecolors):
833-
self._hatch_color = tuple(self._edgecolors[0])
834833
self.stale = True
835834

836835
def set_edgecolor(self, c):
@@ -851,6 +850,29 @@ def set_edgecolor(self, c):
851850
self._original_edgecolor = c
852851
self._set_edgecolor(c)
853852

853+
def _set_hatchcolor(self, c):
854+
c = mpl._val_or_rc(c, 'hatch.color')
855+
if c == 'edge':
856+
self._hatchcolors = 'edge'
857+
else:
858+
self._hatchcolors = mcolors.to_rgba_array(c, self._alpha)
859+
self.stale = True
860+
861+
def set_hatchcolor(self, c):
862+
"""
863+
Set the hatchcolor(s) of the collection.
864+
865+
Parameters
866+
----------
867+
c : :mpltype:`color` or list of :mpltype:`color` or 'edge'
868+
The collection hatchcolor(s). If a sequence, the patches cycle
869+
through it.
870+
"""
871+
if cbook._str_equal(c, 'edge'):
872+
c = 'edge'
873+
self._original_hatchcolor = c
874+
self._set_hatchcolor(c)
875+
854876
def set_alpha(self, alpha):
855877
"""
856878
Set the transparency of the collection.
@@ -968,6 +990,7 @@ def update_from(self, other):
968990
self._us_linestyles = other._us_linestyles
969991
self._pickradius = other._pickradius
970992
self._hatch = other._hatch
993+
self._hatchcolors = other._hatchcolors
971994

972995
# update_from for scalarmappable
973996
self._A = other._A
@@ -2465,7 +2488,8 @@ def draw(self, renderer):
24652488
coordinates, offsets, offset_trf,
24662489
# Backends expect flattened rgba arrays (n*m, 4) for fc and ec
24672490
self.get_facecolor().reshape((-1, 4)),
2468-
self._antialiased, self.get_edgecolors().reshape((-1, 4)))
2491+
self._antialiased, self.get_edgecolors().reshape((-1, 4)),
2492+
self.get_hatchcolor().reshape((-1, 4)))
24692493
gc.restore()
24702494
renderer.close_group(self.__class__.__name__)
24712495
self.stale = False

lib/matplotlib/collections.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Collection(colorizer.ColorizingArtist):
2121
*,
2222
edgecolors: ColorType | Sequence[ColorType] | None = ...,
2323
facecolors: ColorType | Sequence[ColorType] | None = ...,
24+
hatchcolors: ColorType | Sequence[ColorType] | None = ...,
2425
linewidths: float | Sequence[float] | None = ...,
2526
linestyles: LineStyleType | Sequence[LineStyleType] = ...,
2627
capstyle: CapStyleType | None = ...,
@@ -66,6 +67,10 @@ class Collection(colorizer.ColorizingArtist):
6667
def get_facecolor(self) -> ColorType | Sequence[ColorType]: ...
6768
def get_edgecolor(self) -> ColorType | Sequence[ColorType]: ...
6869
def set_edgecolor(self, c: ColorType | Sequence[ColorType]) -> None: ...
70+
def get_hatchcolor(self) -> ColorType | Sequence[ColorType]: ...
71+
def set_hatchcolor(
72+
self, c: ColorType | Sequence[ColorType] | Literal["edge"]
73+
) -> None: ...
6974
def set_alpha(self, alpha: float | Sequence[float] | None) -> None: ...
7075
def get_linewidth(self) -> float | Sequence[float]: ...
7176
def get_linestyle(self) -> LineStyleType | Sequence[LineStyleType]: ...

lib/matplotlib/legend_handler.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,12 +790,11 @@ def get_first(prop_array):
790790
# Directly set Patch color attributes (must be RGBA tuples).
791791
legend_handle._facecolor = first_color(orig_handle.get_facecolor())
792792
legend_handle._edgecolor = first_color(orig_handle.get_edgecolor())
793+
legend_handle._hatch_color = first_color(orig_handle.get_hatchcolor())
793794
legend_handle._original_facecolor = orig_handle._original_facecolor
794795
legend_handle._original_edgecolor = orig_handle._original_edgecolor
795796
legend_handle._fill = orig_handle.get_fill()
796797
legend_handle._hatch = orig_handle.get_hatch()
797-
# Hatch color is anomalous in having no getters and setters.
798-
legend_handle._hatch_color = orig_handle._hatch_color
799798
# Setters are fine for the remaining attributes.
800799
legend_handle.set_linewidth(get_first(orig_handle.get_linewidths()))
801800
legend_handle.set_linestyle(get_first(orig_handle.get_linestyles()))

0 commit comments

Comments
 (0)