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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions doc/api/typing_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Artist styles
=============

.. autodata:: matplotlib.typing.BlendModeType
.. autodata:: matplotlib.typing.FillRuleType
.. autodata:: matplotlib.typing.LineStyleType
.. autodata:: matplotlib.typing.DrawStyleType
.. autodata:: matplotlib.typing.MarkEveryType
Expand Down
10 changes: 10 additions & 0 deletions doc/release/next_whats_new/fill_rules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Option to use the even-odd fill rule for patches
------------------------------------------------

By default, patches such as `~.patches.Polygon` are filled according to the
`non-zero winding fill rule <https://en.wikipedia.org/wiki/Nonzero-rule>`__.
There is now the option to instead use the
`even-odd fill rule <https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule>`__,
which is specified by setting the patch's ``fill_rule`` property to "evenodd".
See :doc:`/gallery/shapes_and_collections/fill_rule_demo` for more details and
an illustration of the difference.
67 changes: 67 additions & 0 deletions galleries/examples/shapes_and_collections/fill_rule_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
==============
Fill rule demo
==============

By default, patches such as `~.patches.Polygon` are filled according to the
`non-zero winding fill rule <https://en.wikipedia.org/wiki/Nonzero-rule>`__.
Any given point has a winding number, which is the number of times the path
wraps around the point in the clockwise direction. For this fill rule, the
filled regions are where the winding number is non-zero. See
:doc:`/gallery/shapes_and_collections/donut` for an example of how to leverage
the winding directions in multiple segments of a path under this fill rule.

The other option for fill rule is the
`even-odd fill rule <https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule>`__,
which is specified by setting the patch's ``fill_rule`` property to "evenodd".
For this fill rule, the filled regions are where the winding number is an odd
number. This fill rule allows for the construction of patterns of fill
regions that would otherwise take many more vertices to construct under the
non-zero winding fill rule.

This example demonstrates the difference between the two fill rules for a single
`~.patches.Polygon` that intersects itself multiple times. The winding number
for each closed region is labeled.

"""

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.patches import Polygon

fig, axs = plt.subplots(1, 2)

vertices = np.array([[0, 0, 6, 6, 1, 1, 5, 5, 2, 2, 4, 4, 3, 3, 5, 5],
[2, 5, 5, 0, 0, 7, 7, 3, 3, 4, 4, 6, 6, 1, 1, 2]]).T

labels = ['1', '0', '1', '2', '3', '2', '1', '1', '0']
label_xys = np.array([[2.0, 4.0, 0.5, 1.5, 2.5, 4.0, 3.5, 2.0, 3.5],
[1.5, 1.5, 3.5, 3.5, 3.5, 3.5, 4.5, 5.5, 5.5]]).T

for ax, fill_rule in zip(axs, ['nonzero', 'evenodd']):
polygon = Polygon(vertices, facecolor='green', edgecolor='red',
fill_rule=fill_rule)
ax.add_patch(polygon)

ax.plot(*vertices.T, '.', markersize=10, color='red')

for label, label_xy in zip(labels, label_xys):
ax.text(*label_xy, label, ha='center', va='center')

ax.set_axis_off()
ax.set_title(f'fill_rule={fill_rule}')

plt.show()

# %%
#
# .. admonition:: References
#
# The use of the following functions, methods, classes and modules is shown
# in this example:
#
# - `matplotlib.patches`
# - `matplotlib.patches.Polygon`
# - `matplotlib.axes.Axes.add_patch`
# - `matplotlib.patches.Patch.set_fill_rule`
2 changes: 2 additions & 0 deletions galleries/tutorials/artists.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class in the Matplotlib API, and the one you will be working with most
# animated = False
# antialiased or aa = False
# bbox = Bbox(x0=0.0, y0=0.0, x1=1.0, y1=1.0)
# blend_mode = normal
# capstyle = butt
# children = []
# clip_box = None
Expand All @@ -217,6 +218,7 @@ class in the Matplotlib API, and the one you will be working with most
# facecolor or fc = (1.0, 1.0, 1.0, 1.0)
# figure = Figure(640x480)
# fill = True
# fill_rule = nonzero
# gid = None
# hatch = None
# height = 1
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def __init__(self):
self._animated = False
self._alpha = None
self._blend_mode = "normal"
self._fill_rule = "nonzero"
self.clipbox = None
self._clippath = None
self._clipon = True
Expand Down Expand Up @@ -1278,6 +1279,7 @@ def update_from(self, other):
self._visible = other._visible
self._alpha = other._alpha
self._blend_mode = other._blend_mode
self._fill_rule = other._fill_rule
self.clipbox = other.clipbox
self._clipon = other._clipon
self._clippath = other._clippath
Expand Down
8 changes: 8 additions & 0 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ def __init__(self):
self._alpha = 1.0
self._forced_alpha = False # if True, _alpha overrides A from RGBA
self._blend_mode = "normal"
self._fill_rule = "nonzero"
self._antialiased = 1 # use 0, 1 not True, False for extension code
self._capstyle = CapStyle('butt')
self._cliprect = None
Expand All @@ -779,6 +780,7 @@ def copy_properties(self, gc):
self._alpha = gc._alpha
self._forced_alpha = gc._forced_alpha
self._blend_mode = gc._blend_mode
self._fill_rule = gc._fill_rule
self._antialiased = gc._antialiased
self._capstyle = gc._capstyle
self._cliprect = gc._cliprect
Expand Down Expand Up @@ -813,6 +815,9 @@ def get_blend_mode(self):
"""Return the blend mode for compositing - not supported on all backends."""
return self._blend_mode

def get_fill_rule(self):
return self._fill_rule

def get_antialiased(self):
"""Return whether the object should try to do antialiased rendering."""
return self._antialiased
Expand Down Expand Up @@ -924,6 +929,9 @@ def set_blend_mode(self, blend_mode):
# Backend-independent input validation is done in Artist.set_blend_mode()
self._blend_mode = blend_mode

def set_fill_rule(self, fill_rule):
self._fill_rule = fill_rule

def set_antialiased(self, b):
"""Set whether object should be drawn with antialiased rendering."""
# Use ints to make life easier on extension code trying to read the gc.
Expand Down
7 changes: 5 additions & 2 deletions lib/matplotlib/backend_bases.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from matplotlib import (
widgets,
_api,
)
from matplotlib.artist import Artist, BlendMode
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib.backend_managers import ToolManager
from matplotlib.backend_tools import Cursors, ToolBase
Expand All @@ -27,6 +27,7 @@ from .typing import (
CloseEventType,
ColorType,
DrawEventType,
FillRuleType,
JoinStyleType,
KeyEventType,
LineStyleType,
Expand Down Expand Up @@ -159,6 +160,7 @@ class GraphicsContextBase:
def restore(self) -> None: ...
def get_alpha(self) -> float: ...
def get_blend_mode(self) -> str: ...
def get_fill_rule(self) -> FillRuleType: ...
def get_antialiased(self) -> int: ...
def get_capstyle(self) -> Literal["butt", "projecting", "round"]: ...
def get_clip_rectangle(self) -> Bbox | None: ...
Expand All @@ -174,7 +176,8 @@ class GraphicsContextBase:
def get_gid(self) -> int | None: ...
def get_snap(self) -> bool | None: ...
def set_alpha(self, alpha: float) -> None: ...
def set_blend_mode(self, blend_mode: str | BlendMode) -> None: ...
def set_blend_mode(self, blend_mode: BlendModeType) -> None: ...
def set_fill_rule(self, fill_rule: FillRuleType) -> None: ...
def set_antialiased(self, b: bool) -> None: ...
def set_capstyle(self, cs: CapStyleType) -> None: ...
def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ...
Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/backends/backend_cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,11 @@ class GraphicsContextCairo(GraphicsContextBase):
'luminosity': cairo.OPERATOR_HSL_LUMINOSITY,
}

_filld = {
'nonzero': cairo.FILL_RULE_WINDING,
'evenodd': cairo.FILL_RULE_EVEN_ODD,
}

def __init__(self, renderer):
super().__init__()
self.renderer = renderer
Expand Down Expand Up @@ -501,6 +506,11 @@ def set_blend_mode(self, blend_mode):
self.ctx.set_operator(_api.getitem_checked(self._operatord,
blend_mode=self._blend_mode))

def set_fill_rule(self, fill_rule):
super().set_fill_rule(fill_rule)
self.ctx.set_fill_rule(_api.getitem_checked(self._filld,
fill_rule=self._fill_rule))


class _CairoRegion:
def __init__(self, slices, data):
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 @@ -440,7 +440,9 @@ class Op(Enum):

close_fill_stroke = b'b'
fill_stroke = b'B'
fill_evenodd_stroke = b'B*'
fill = b'f'
fill_evenodd = b'f*'
closepath = b'h'
close_stroke = b's'
stroke = b'S'
Expand Down Expand Up @@ -481,7 +483,7 @@ def pdfRepr(self):
return self.value

@classmethod
def paint_path(cls, fill, stroke):
def paint_path(cls, fill, stroke, *, fill_rule="nonzero"):
"""
Return the PDF operator to paint a path.

Expand All @@ -494,11 +496,15 @@ def paint_path(cls, fill, stroke):
"""
if stroke:
if fill:
if fill_rule == "evenodd":
return cls.fill_evenodd_stroke
return cls.fill_stroke
else:
return cls.stroke
else:
if fill:
if fill_rule == "evenodd":
return cls.fill_evenodd
return cls.fill
else:
return cls.endpath
Expand Down Expand Up @@ -1983,7 +1989,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
path, transform,
rgbFace is None and gc.get_hatch_path() is None,
gc.get_sketch_params())
self.file.output(self.gc.paint())
self.file.output(self.gc.paint(fill_rule=gc._fill_rule))

def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
Expand Down Expand Up @@ -2489,12 +2495,12 @@ def fill(self, *args):
(_fillcolor is not None and
(len(_fillcolor) <= 3 or _fillcolor[3] != 0.0)))

def paint(self):
def paint(self, *, fill_rule="nonzero"):
"""
Return the appropriate pdf operator to cause the path to be
stroked, filled, or both.
"""
return Op.paint_path(self.fill(), self.stroke())
return Op.paint_path(self.fill(), self.stroke(), fill_rule=fill_rule)

capstyles = {'butt': 0, 'round': 1, 'projecting': 2}
joinstyles = {'miter': 0, 'round': 1, 'bevel': 2}
Expand Down
6 changes: 4 additions & 2 deletions lib/matplotlib/backends/backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,10 @@ def _print_pgf_path_styles(self, gc, rgbFace):
r"\definecolor{currentfill}{rgb}{%f,%f,%f}"
% tuple(rgbFace[:3]))
_writeln(self.fh, r"\pgfsetfillcolor{currentfill}")
if has_fill and fillopacity != 1.0:
_writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity)
if fillopacity != 1.0:
_writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity)
if gc.get_fill_rule() == "evenodd":
_writeln(self.fh, r"\pgfseteorule")

# linewidth and color
lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True):
if self._is_transparent(rgbFace):
fill = False
hatch = gc.get_hatch()
fill_op = "eofill" if gc.get_fill_rule() == "evenodd" else "fill"

if mightstroke:
self.set_linewidth(gc.get_linewidth())
Expand All @@ -943,15 +944,15 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True):
if stroke or hatch:
write("gsave\n")
self.set_color(*rgbFace[:3], store=False)
write("fill\n")
write(f"{fill_op}\n")
if stroke or hatch:
write("grestore\n")

if hatch:
hatch_name = self.create_hatch(hatch, gc.get_hatch_linewidth())
write("gsave\n")
write(_nums_to_str(*gc.get_hatch_color()[:3]))
write(f" {hatch_name} setpattern fill grestore\n")
write(f" {hatch_name} setpattern {fill_op} grestore\n")

if stroke:
write("stroke\n")
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,8 @@ def _get_style_dict(self, gc, rgbFace):
attrib['opacity'] = _short_float_fmt(gc.get_alpha())
if (blend_mode := _svg_blend_mode(gc.get_blend_mode())) != "normal":
attrib["mix-blend-mode"] = blend_mode
if (fill_rule := gc.get_fill_rule()) != "nonzero":
attrib["fill-rule"] = fill_rule
Comment thread
iccir marked this conversation as resolved.

offset, seq = gc.get_dashes()
if seq is not None:
Expand Down
24 changes: 24 additions & 0 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,29 @@ def get_hatch_linewidth(self):
"""Return the hatch linewidth."""
return self._hatch_linewidth

def set_fill_rule(self, fill_rule):
"""
Set the rule for filling a shape.

See :doc:`/gallery/shapes_and_collections/fill_rule_demo`.

Parameters
----------
fill_rule : {'nonzero', 'evenodd'}
'nonzero' for the non-zero winding rule (the default), or
'evenodd' for the even-odd rule

References
----------
* `Wikipedia: Non-zero winding rule <https://en.wikipedia.org/wiki/Nonzero-rule>`__
* `Wikipedia: Even-odd rule <https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule>`__
"""
_api.check_in_list(["nonzero", "evenodd"], fill_rule=fill_rule)
self._fill_rule = fill_rule

def get_fill_rule(self):
return self._fill_rule

def _has_dashed_edge(self):
"""
Return whether the patch edge has a dashed linestyle.
Expand Down Expand Up @@ -673,6 +696,7 @@ def _draw_paths_with_artist_properties(

gc.set_alpha(self._alpha)
gc.set_blend_mode(self.get_blend_mode())
gc.set_fill_rule(self.get_fill_rule())

if self._hatch:
gc.set_hatch(self._hatch)
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/patches.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ from typing import Any, Literal, overload

import numpy as np
from numpy.typing import ArrayLike
from .typing import ColorType, LineStyleType, CapStyleType, JoinStyleType
from .typing import ColorType, FillRuleType, LineStyleType, CapStyleType, JoinStyleType

class Patch(artist.Artist):
zorder: float
Expand Down Expand Up @@ -68,6 +68,8 @@ class Patch(artist.Artist):
def set_hatch_linewidth(self, lw: float) -> None: ...
def get_hatch_linewidth(self) -> float: ...
def get_hatch(self) -> str: ...
def set_fill_rule(self, fill_rule: FillRuleType) -> None: ...
def get_fill_rule(self) -> FillRuleType: ...
def get_path(self) -> Path: ...

class Shadow(Patch):
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading