8000 Merge pull request #29044 from Impaler343/collections-hatchcolor · matplotlib/matplotlib@65d2818 · GitHub
[go: up one dir, main page]

Skip to content

Commit 65d2818

Browse files
authored
Merge pull request #29044 from Impaler343/collections-hatchcolor
Add hatchcolor parameter for Collections
2 parents dc05767 + 3b43bc6 commit 65d2818

16 files changed

+434
-76
lines changed

doc/users/next_whats_new/separated_hatchcolor.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,39 @@ Previously, hatch colors were the same as edge colors, with a fallback to
5757
xy=(.5, 1.03), xycoords=patch4, ha='center', va='bottom')
5858

5959
plt.show()
60+
61+
For collections, a sequence of colors can be passed to the *hatchcolor* parameter
62+
which will be cycled through for each hatch, similar to *facecolor* and *edgecolor*.
63+
64+
Previously, if *edgecolor* was not specified, the hatch color would fall back to
65+
:rc:`patch.edgecolor`, but the alpha value would default to **1.0**, regardless of the
66+
alpha value of the collection. This behavior has been changed such that, if both
67+
*hatchcolor* and *edgecolor* are not specified, the hatch color will fall back
68+
to 'patch.edgecolor' with the alpha value of the collection.
69+
70+
.. plot::
71+
:include-source: true
72+
:alt: A random scatter plot with hatches on the markers. The hatches are colored in blue, orange, and green, respectively. After the first three markers, the colors are cycled through again.
73+
74+
import matplotlib.pyplot as plt
75+
import numpy as np
76+
77+
np.random.seed(19680801)
78+
79+
fig, ax = plt.subplots()
80+
81+
x = [29, 36, 41, 25, 32, 70, 62, 58, 66, 80, 58, 68, 62, 37, 48]
82+
y = [82, 76, 48, 53, 62, 70, 84, 68, 55, 75, 29, 25, 12, 17, 20]
83+
colors = ['tab:blue'] * 5 + ['tab:orange'] * 5 + ['tab:green'] * 5
84+
85+
ax.scatter(
86+
x,
87+
y,
88+
s=800,
89+
hatch="xxxx",
90+
hatchcolor=colors,
91+
facecolor="none",
92+
edgecolor="black",
93+
)
94+
95+
plt.show()

galleries/examples/shapes_and_collections/hatchcolor_demo.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
"""
2-
================
3-
Patch hatchcolor
4-
================
2+
===============
3+
Hatchcolor Demo
4+
===============
5+
6+
The color of the hatch can be set using the *hatchcolor* parameter. The following
7+
examples show how to use the *hatchcolor* parameter to set the color of the hatch
8+
in `~.patches.Patch` and `~.collections.Collection`.
9+
10+
See also :doc:`/gallery/shapes_and_collections/hatch_demo` for more usage examples
11+
of hatching.
12+
13+
Patch Hatchcolor
14+
----------------
515
616
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.
17+
the hatch in a rectangle and a bar plot. The *hatchcolor* parameter is available for
18+
`~.patches.Patch`, child classes of Patch, and methods that pass through to Patch.
919
"""
1020

1121
import matplotlib.pyplot as plt
1222
import numpy as np
1323

24+
import matplotlib.cm as cm
1425
from matplotlib.patches import Rectangle
1526

1627
fig, (ax1, ax2) = plt.subplots(1, 2)
@@ -28,6 +39,43 @@
2839
ax2.set_xlim(0, 5)
2940
ax2.set_ylim(0, 5)
3041

42+
# %%
43+
# Collection Hatchcolor
44+
# ---------------------
45+
#
46+
# The following example shows how to use the *hatchcolor* parameter to set the color of
47+
# the hatch in a scatter plot. The *hatchcolor* parameter can also be passed to
48+
# `~.collections.Collection`, child classes of Collection, and methods that pass
49+
# through to Collection.
50+
51+
fig, ax = plt.subplots()
52+
53+
num_points_x = 10
54+
num_points_y = 9
55+
x = np.linspace(0, 1, num_points_x)
56+
y = np.linspace(0, 1, num_points_y)
57+
58+
X, Y = np.meshgrid(x, y)
59+
X[1::2, :] += (x[1] - x[0]) / 2 # stagger every alternate row
60+
61+
# As ax.scatter (PathCollection) is drawn row by row, setting hatchcolors to the
62+
# first row is enough, as the colors will be cycled through for the next rows.
63+
colors = [cm.rainbow(val) for val in x]
64+
65+
ax.scatter(
66+
X.ravel(),
67+
Y.ravel(),
68+
s=1700,
69+
facecolor="none",
70+
edgecolor="gray",
71+
linewidth=2,
72+
marker="h", # Use hexagon as marker
73+
hatch="xxx",
74+
hatchcolor=colors,
75+
)
76+
ax.set_xlim(0, 1)
77+
ax.set_ylim(0, 1)
78+
3179
plt.show()
3280

3381
# %%
@@ -41,3 +89,5 @@
4189
# - `matplotlib.patches.Polygon`
4290
# - `matplotlib.axes.Axes.add_patch`
4391
# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar`
92+
# - `matplotlib.collections`
93+
# - `matplotlib.axes.Axes.scatter` / `matplotlib.pyplot.scatter`

lib/matplotlib/backend_bases.py

Lines changed: 23 additions & 10 deletions
248
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,11 @@ 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.
222+
223+
.. versionadded:: 3.11
224+
Allow *hatchcolors* to be specified.
222225
223226
*offset_position* is unused now, but the argument is kept for
224227
backwards compatibility.
@@ -235,10 +238,13 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
235238
path_ids = self._iter_collection_raw_paths(master_transform,
236239
paths, all_transforms)
237240

241+
if hatchcolors is None:
242+
hatchcolors = []
243+
238244
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
239245
gc, list(path_ids), offsets, offset_trans,
240246
facecolors, edgecolors, linewidths, linestyles,
241-
antialiaseds, urls, offset_position):
247+
antialiaseds, urls, offset_position, hatchcolors=hatchcolors):
242
path, transform = path_id
243249
# Only apply another translation if we have an offset, else we
244250
# reuse the initial transform.
@@ -252,7 +258,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
252258

253259
def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
254260
coordinates, offsets, offsetTrans, facecolors,
255-
antialiased, edgecolors):
261+
antialiased, edgecolors, *, hatchcolors=None):
256262
"""
257263
Draw a quadmesh.
258264
@@ -265,11 +271,14 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
265271

266272
if edgecolors is None:
267273
edgecolors = facecolors
274+
if hatchcolors is None:
275+
hatchcolors = []
268276
linewidths = np.array([gc.get_linewidth()], float)
269277

270278
return self.draw_path_collection(
271279
gc, master_transform, paths, [], offsets, offsetTrans, facecolors,
272-
edgecolors, linewidths, [], [antialiased], [None], 'screen')
280+
edgecolors, linewidths, [], [antialiased], [None], 'screen',
281+
hatchcolors=hatchcolors)
273282

274283
def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
275284
transform):
@@ -337,7 +346,7 @@ def _iter_collection_uses_per_path(self, paths, all_transforms,
337346

338347
def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
339348
edgecolors, linewidths, linestyles,
340-
antialiaseds, urls, offset_position):
349+
antialiaseds, urls, offset_position, *, hatchcolors):
341350
"""
342351
Helper method (along with `_iter_collection_raw_paths`) to implement
343352
`draw_path_collection` in a memory-efficient manner.
@@ -365,11 +374,12 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
365374
N = max(Npaths, Noffsets)
366375
Nfacecolors = len(facecolors)
367376
Nedgecolors = len(edgecolors)
377+
Nhatchcolors = len(hatchcolors)
368378
Nlinewidths = len(linewidths)
369379
Nlinestyles = len(linestyles)
370380
Nurls = len(urls)
371381

372-
if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0:
382+
if (Nfacecolors == 0 and Nedgecolors == 0 and Nhatchcolors == 0) or Npaths == 0:
373383
return
374384

375385
gc0 = self.new_gc()
@@ -384,6 +394,7 @@ def cycle_or_default(seq, default=None):
384394
toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0))
385395
fcs = cycle_or_default(facecolors)
386396
ecs = cycle_or_default(edgecolors)
397+
hcs = cycle_or_default(hatchcolors)
387398
lws = cycle_or_default(linewidths)
388399
lss = cycle_or_default(linestyles)
389400
aas = cycle_or_default(antialiaseds)
@@ -392,8 +403,8 @@ def cycle_or_default(seq, default=None):
392403
if Nedgecolors == 0:
393404
gc0.set_linewidth(0.0)
394405

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):
406+
for pathid, (xo, yo), fc, ec, hc, lw, ls, aa, url in itertools.islice(
407+
zip(pathids, toffsets, fcs, ecs, hcs, lws, lss, aas, urls), N):
397408
if not (np.isfinite(xo) and np.isfinite(yo)):
398409
continue
399410
if Nedgecolors:
@@ -405,6 +416,8 @@ def cycle_or_default(seq, default=None):
405416
gc0.set_linewidth(0)
406417
else 10000 :
407418
gc0.set_foreground(ec)
419+
if Nhatchcolors:
420+
gc0.set_hatch_color(hc)
408421
if fc is not None and len(fc) == 4 and fc[3] == 0:
409422
fc = None
410423
gc0.set_antialiased(aa)

lib/matplotlib/backend_bases.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class RendererBase:
6363
antialiaseds: bool | Sequence[bool],
6464
urls: str | Sequence[str],
6565
offset_position: Any,
66+
*,
67+
hatchcolors: ColorType | Sequence[ColorType] | None = None,
6668
) -> None: ...
6769
def draw_quad_mesh(
6870
self,
@@ -76,6 +78,8 @@ class RendererBase:
7678
facecolors: Sequence[ColorType],
7779
antialiased: bool,
7880
edgecolors: Sequence[ColorType] | ColorType | None,
81+
*,
82+
hatchcolors: Sequence[ColorType] | ColorType | None = None,
7983
) -> None: ...
8084
def draw_gouraud_triangles(
8185
self,

lib/matplotlib/backends/backend_pdf.py

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

2045+
if hatchcolors is None:
2046+
hatchcolors = []
2047+
20452048
if not len(facecolors):
20462049
filled = False
20472050
can_do_optimization = not gc.get_hatch()
@@ -2076,7 +2079,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
20762079
self, gc, master_transform, paths, all_transforms,
20772080
offsets, offset_trans, facecolors, edgecolors,
20782081
linewidths, linestyles, antialiaseds, urls,
2079-
offset_position)
2082+
offset_position, hatchcolors=hatchcolors)
20802083

20812084
padding = np.max(linewidths)
20822085
path_codes = []
@@ -2092,7 +2095,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
20922095
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
20932096
gc, path_codes, offsets, offset_trans,
20942097
facecolors, edgecolors, linewidths, linestyles,
2095-
antialiaseds, urls, offset_position):
2098+
antialiaseds, urls, offset_position, hatchcolors=hatchcolors):
20962099

20972100
self.check_gc(gc0, rgbFace)
20982101
dx, dy = xo - lastx, yo - lasty
@@ -2607,7 +2610,10 @@ def delta(self, other):
26072610
different = ours is not theirs
26082611
else:
26092612
different = bool(ours != theirs)
2610-
except ValueError:
2613+
except (ValueError, DeprecationWarning):
2614+
# numpy version < 1.25 raises DeprecationWarning when array shapes
2615+
# mismatch, unlike numpy >= 1.25 which raises ValueError.
2616+
# This should be removed when numpy < 1.25 is no longer supported.
26112617
ours = np.asarray(ours)
26122618
theirs = np.asarray(theirs)
26132619
different = (ours.shape != theirs.shape or

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=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=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=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=hatchcolors):
774776
url = gc0.get_url()
775777
if url is not None:
776778
writer.start('a', attrib={'xlink:href': url})

0 commit comments

Comments
 (0)
0