Skip to content

Add hatchcolor parameter for Collections #29044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions doc/users/next_whats_new/separated_hatchcolor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,39 @@ Previously, hatch colors were the same as edge colors, with a fallback to
xy=(.5, 1.03), xycoords=patch4, ha='center', va='bottom')

plt.show()

For collections, a sequence of colors can be passed to the *hatchcolor* parameter
which will be cycled through for each hatch, similar to *facecolor* and *edgecolor*.

Previously, if *edgecolor* was not specified, the hatch color would fall back to
:rc:`patch.edgecolor`, but the alpha value would default to **1.0**, regardless of the
alpha value of the collection. This behavior has been changed such that, if both
*hatchcolor* and *edgecolor* are not specified, the hatch color will fall back
to 'patch.edgecolor' with the alpha value of the collection.

.. plot::
:include-source: true
: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.

import matplotlib.pyplot as plt
import numpy as np

np.random.seed(19680801)

fig, ax = plt.subplots()

x = np.random.rand(20)
y = np.random.rand(20)
colors = ["blue", "orange", "green"]

ax.scatter(
x,
y,
s=800,
hatch="xxxx",
hatchcolor=colors,
facecolor="none",
edgecolor="black",
)

plt.show()
60 changes: 55 additions & 5 deletions galleries/examples/shapes_and_collections/hatchcolor_demo.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
"""
================
Patch hatchcolor
================
===============
Hatchcolor Demo
===============

The color of the hatch can be set using the *hatchcolor* parameter. The following
examples show how to use the *hatchcolor* parameter to set the color of the hatch
in `~.patches.Patch` and `~.collections.Collection`.

See also :doc:`/gallery/shapes_and_collections/hatch_demo` for more usage examples
of hatching.

Patch Hatchcolor
----------------

This example shows how to use the *hatchcolor* parameter to set the color of
the hatch. The *hatchcolor* parameter is available for `~.patches.Patch`,
child classes of Patch, and methods that pass through to Patch.
the hatch in a rectangle and a bar plot. The *hatchcolor* parameter is available for
`~.patches.Patch`, child classes of Patch, and methods that pass through to Patch.
"""

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.cm as cm
from matplotlib.patches import Rectangle

fig, (ax1, ax2) = plt.subplots(1, 2)
Expand All @@ -28,6 +39,43 @@
ax2.set_xlim(0, 5)
ax2.set_ylim(0, 5)

# %%
# Collection Hatchcolor
# ---------------------
#
# The following example shows how to use the *hatchcolor* parameter to set the color of
# the hatch in a scatter plot. The *hatchcolor* parameter can also be passed to
# `~.collections.Collection`, child classes of Collection, and methods that pass
# through to Collection.

fig, ax = plt.subplots()

num_points_x = 10
num_points_y = 9
x = np.linspace(0, 1, num_points_x)
y = np.linspace(0, 1, num_points_y)

X, Y = np.meshgrid(x, y)
X[1::2, :] += (x[1] - x[0]) / 2 # stagger every alternate row

# As ax.scatter (PathCollection) is drawn row by row, setting hatchcolors to the
# first row is enough, as the colors will be cycled through for the next rows.
colors = [cm.rainbow(val) for val in x]

ax.scatter(
X.ravel(),
Y.ravel(),
s=1700,
facecolor="none",
edgecolor="gray",
linewidth=2,
marker="s", # Use hexagon as marker
hatch="xxx",
hatchcolor=colors,
)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)

plt.show()

# %%
Expand All @@ -41,3 +89,5 @@
# - `matplotlib.patches.Polygon`
# - `matplotlib.axes.Axes.add_patch`
# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar`
# - `matplotlib.collections`
# - `matplotlib.axes.Axes.scatter` / `matplotlib.pyplot.scatter`
33 changes: 23 additions & 10 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
offset_position, *, hatchcolors=None):
"""
Draw a collection of *paths*.

Expand All @@ -217,8 +217,11 @@
*master_transform*. They are then translated by the corresponding
entry in *offsets*, which has been first transformed by *offset_trans*.

*facecolors*, *edgecolors*, *linewidths*, *linestyles*, and
*antialiased* are lists that set the corresponding properties.
*facecolors*, *edgecolors*, *linewidths*, *linestyles*, *antialiased*
and *hatchcolors* are lists that set the corresponding properties.

.. versionadded:: 3.11
Allow *hatchcolors* to be specified.

*offset_position* is unused now, but the argument is kept for
backwards compatibility.
Expand All @@ -235,10 +238,13 @@
path_ids = self._iter_collection_raw_paths(master_transform,
paths, all_transforms)

if hatchcolors is None:
hatchcolors = []

Check warning on line 242 in lib/matplotlib/backend_bases.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backend_bases.py#L242

Added line #L242 was not covered by tests

for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
gc, list(path_ids), offsets, offset_trans,
facecolors, edgecolors, linewidths, linestyles,
antialiaseds, urls, offset_position):
antialiaseds, urls, offset_position, hatchcolors=hatchcolors):
path, transform = path_id
# Only apply another translation if we have an offset, else we
# reuse the initial transform.
Expand All @@ -252,7 +258,7 @@

def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
coordinates, offsets, offsetTrans, facecolors,
antialiased, edgecolors):
antialiased, edgecolors, *, hatchcolors=None):
"""
Draw a quadmesh.

Expand All @@ -265,11 +271,14 @@

if edgecolors is None:
edgecolors = facecolors
if hatchcolors is None:
hatchcolors = []

Check warning on line 275 in lib/matplotlib/backend_bases.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backend_bases.py#L275

Added line #L275 was not covered by tests
linewidths = np.array([gc.get_linewidth()], float)

return self.draw_path_collection(
gc, master_transform, paths, [], offsets, offsetTrans, facecolors,
edgecolors, linewidths, [], [antialiased], [None], 'screen')
edgecolors, linewidths, [], [antialiased], [None], 'screen',
hatchcolors=hatchcolors)

def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
transform):
Expand Down Expand Up @@ -337,7 +346,7 @@

def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
edgecolors, linewidths, linestyles,
antialiaseds, urls, offset_position):
antialiaseds, urls, offset_position, *, hatchcolors):
"""
Helper method (along with `_iter_collection_raw_paths`) to implement
`draw_path_collection` in a memory-efficient manner.
Expand Down Expand Up @@ -365,11 +374,12 @@
N = max(Npaths, Noffsets)
Nfacecolors = len(facecolors)
Nedgecolors = len(edgecolors)
Nhatchcolors = len(hatchcolors)
Nlinewidths = len(linewidths)
Nlinestyles = len(linestyles)
Nurls = len(urls)

if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0:
if (Nfacecolors == 0 and Nedgecolors == 0 and Nhatchcolors == 0) or Npaths == 0:
return

gc0 = self.new_gc()
Expand All @@ -384,6 +394,7 @@
toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0))
fcs = cycle_or_default(facecolors)
ecs = cycle_or_default(edgecolors)
hcs = cycle_or_default(hatchcolors)
lws = cycle_or_default(linewidths)
lss = cycle_or_default(linestyles)
aas = cycle_or_default(antialiaseds)
Expand All @@ -392,8 +403,8 @@
if Nedgecolors == 0:
gc0.set_linewidth(0.0)

for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice(
zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N):
for pathid, (xo, yo), fc, ec, hc, lw, ls, aa, url in itertools.islice(
zip(pathids, toffsets, fcs, ecs, hcs, lws, lss, aas, urls), N):
if not (np.isfinite(xo) and np.isfinite(yo)):
continue
if Nedgecolors:
Expand All @@ -405,6 +416,8 @@
gc0.set_linewidth(0)
else:
gc0.set_foreground(ec)
if Nhatchcolors:
gc0.set_hatch_color(hc)
if fc is not None and len(fc) == 4 and fc[3] == 0:
fc = None
gc0.set_antialiased(aa)
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/backend_bases.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class RendererBase:
antialiaseds: bool | Sequence[bool],
urls: str | Sequence[str],
offset_position: Any,
*,
hatchcolors: ColorType | Sequence[ColorType] | None = None,
) -> None: ...
def draw_quad_mesh(
self,
Expand All @@ -76,6 +78,8 @@ class RendererBase:
facecolors: Sequence[ColorType],
antialiased: bool,
edgecolors: Sequence[ColorType] | ColorType | None,
*,
hatchcolors: Sequence[ColorType] | ColorType | None = None,
) -> None: ...
def draw_gouraud_triangles(
self,
Expand Down
14 changes: 10 additions & 4 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2030,14 +2030,17 @@ def draw_path(self, gc, path, transform, rgbFace=None):
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
offset_position, *, hatchcolors=None):
# We can only reuse the objects if the presence of fill and
# stroke (and the amount of alpha for each) is the same for
# all of them
can_do_optimization = True
facecolors = np.asarray(facecolors)
edgecolors = np.asarray(edgecolors)

if hatchcolors is None:
hatchcolors = []

if not len(facecolors):
filled = False
can_do_optimization = not gc.get_hatch()
Expand Down Expand Up @@ -2072,7 +2075,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position)
offset_position, hatchcolors=hatchcolors)

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

self.check_gc(gc0, rgbFace)
dx, dy = xo - lastx, yo - lasty
Expand Down Expand Up @@ -2603,7 +2606,10 @@ def delta(self, other):
different = ours is not theirs
else:
different = bool(ours != theirs)
except ValueError:
except (ValueError, DeprecationWarning):
# numpy version < 1.25 raises DeprecationWarning when array shapes
# mismatch, unlike numpy >= 1.25 which raises ValueError.
# This should be removed when numpy < 1.25 is no longer supported.
ours = np.asarray(ours)
theirs = np.asarray(theirs)
different = (ours.shape != theirs.shape or
Expand Down
8 changes: 5 additions & 3 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,9 @@ def draw_markers(
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
offset_position, *, hatchcolors=None):
if hatchcolors is None:
hatchcolors = []
# Is the optimization worth it? Rough calculation:
# cost of emitting a path in-line is
# (len_path + 2) * uses_per_path
Expand All @@ -690,7 +692,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position)
offset_position, hatchcolors=hatchcolors)

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

Expand Down
8 changes: 5 additions & 3 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,9 @@ def draw_markers(
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
offset_position, *, hatchcolors=None):
if hatchcolors is None:
hatchcolors = []
# Is the optimization worth it? Rough calculation:
# cost of emitting a path in-line is
# (len_path + 5) * uses_per_path
Expand All @@ -752,7 +754,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position)
offset_position, hatchcolors=hatchcolors)

writer = self.writer
path_codes = []
Expand All @@ -770,7 +772,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
gc, path_codes, offsets, offset_trans,
facecolors, edgecolors, linewidths, linestyles,
antialiaseds, urls, offset_position):
antialiaseds, urls, offset_position, hatchcolors=hatchcolors):
url = gc0.get_url()
if url is not None:
writer.start('a', attrib={'xlink:href': url})
Expand Down
Loading
Loading