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
39 changes: 39 additions & 0 deletions src/maxplotlib/backends/tikzfigure/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,34 @@ def _draw(self, data, options, cycle) -> str:
parts.append("-- cycle")
return f"\\draw[{', '.join(keep)}] {' '.join(parts)};"

def _draw_loops(self, loops, options) -> str:
"""One ``\\draw`` path of several closed loops, filled by the even-odd rule.

A compound patch (a shape with holes, such as an annulus or a plate with an aperture)
is one path in Matplotlib; drawn as one path in TikZ its holes stay empty, where one
filled plot per loop would fill them.
"""
precision = self.options["precision"]
keep = [
option
for option in options
if not option.startswith(
("mark", "only marks", "forget plot", "area legend")
)
]
keep.append("even odd rule")
parts = []
for loop in loops:
points = [
f"(axis cs:{format_number(float(x), precision)},"
f"{format_number(float(y), precision)})"
for x, y in loop
if np.isfinite(x) and np.isfinite(y)
]
if len(points) > 2:
parts.append(" -- ".join(points) + " -- cycle")
return f"\\draw[{', '.join(keep)}] {' '.join(parts)};"

def _add(self, data, options, label="", cycle=False, clip=True):
data = np.asarray(data, dtype=float)
if not clip:
Expand Down Expand Up @@ -1119,6 +1147,14 @@ def _patch(self, patch, clip=None):
)
if options is None:
continue
closed_polygons = [polygon for polygon in polygons if len(polygon) > 2]
if can_fill and len(polygons) > 1 and len(closed_polygons) == len(polygons):
# one filled path of several loops: holes stay holes (even-odd rule)
loops = [self._clip_polygon(polygon) for polygon in polygons]
loops = [self._to_data(loop) for loop in loops if len(loop)]
if loops:
pieces.append((loops, options, "compound"))
continue
for polygon in polygons:
closed = can_fill and len(polygon) > 2
polygon = (
Expand All @@ -1130,6 +1166,9 @@ def _patch(self, patch, clip=None):

def emit():
for index, (data, options, closed) in enumerate(pieces):
if closed == "compound":
self.axis.add_raw(self._draw_loops(data, options))
continue
self._add(
data, options, label if index == 0 else "", cycle=closed, clip=clip
)
Expand Down
18 changes: 18 additions & 0 deletions src/maxplotlib/tests/test_tikzfigure_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,24 @@ def test_hatches_become_tikz_patterns(hatch, pattern):
assert "patterns" in figure._tikz_libraries


def test_a_patch_with_a_hole_is_one_even_odd_path():
from matplotlib.patches import Annulus, Rectangle

fig, ax = plt.subplots()
ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2)
ax.add_patch(Annulus((0, 0), 1.5, 0.5, facecolor="C0", hatch="//", label="ring"))
ax.add_patch(Rectangle((1.6, 1.6), 0.2, 0.2)) # one loop: a plot as before
ax.legend()
tikz = figure_to_tikz(fig).generate_tikz()
ring = [line for line in tikz.splitlines() if "even odd rule" in line]
assert len(ring) == 1
assert ring[0].count("-- cycle") == 2 # the outer circle and the hole, one path
assert "pattern=north east lines" in ring[0]
assert "\\addlegendentry{ring}" in tikz
assert tikz.count("\\addplot[") == 1 # the rectangle


def test_a_hatched_legend_entry_keeps_its_pattern():
from matplotlib.patches import Rectangle

Expand Down
Loading