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
59 changes: 59 additions & 0 deletions src/maxplotlib/backends/tikzfigure/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ def convert(self) -> TikzFigure:
figure._layout_engine = engine
self.tikz.add_package("lmodern")
self.tikz.add_package("amsmath")
self.tikz.add_package("amssymb")
definitions = self.colors.definitions()
if definitions:
self.tikz.add_raw(definitions)
Expand Down Expand Up @@ -410,6 +411,18 @@ def figure_to_tikz(
return converter.convert()


#: The TikZ tips (arrows.meta, which tikzfigure loads) of Matplotlib's straight arrow styles.
_ARROW_TIPS = {
"-": ("", ""),
"->": ("", "Straight Barb"),
"<-": ("Straight Barb", ""),
"<->": ("Straight Barb", "Straight Barb"),
"-|>": ("", "Triangle"),
"<|-": ("Triangle", ""),
"<|-|>": ("Triangle", "Triangle"),
}


def _dash(pattern) -> str | None:
"""A Matplotlib ``(offset, [on, off, ...])`` dash pattern in points as TikZ."""
if pattern is None:
Expand Down Expand Up @@ -968,9 +981,55 @@ def _line(self, line):
clip = line.get_clip_on()
return lambda: self._add(data, options, label, clip=clip)

def _arrow(self, patch):
"""A straight ``FancyArrowPatch`` as a ``\\draw`` with TikZ arrow tips, or ``None``.

The tips of ``->``, ``<-``, ``<->`` and their filled forms (``-|>``, ``<|-``,
``<|-|>``) become ``Straight Barb`` and ``Triangle`` tips of the size Matplotlib
gives them (the arrow style's head sizes times ``mutation_scale``); ``shrinkA`` and
``shrinkB`` shorten the line. Other arrow styles and curved connections are drawn as
the paths Matplotlib renders.
"""
style = patch.get_arrowstyle()
arrow = getattr(style, "arrow", None)
ends = getattr(patch, "_posA_posB", None)
if arrow not in _ARROW_TIPS or ends is None or ends[0] is None:
return None
connection = type(patch.get_connectionstyle()).__name__
if connection != "Arc3" or getattr(patch.get_connectionstyle(), "rad", 0.0):
return None # a curved connection
options = self._stroke(
patch.get_edgecolor(), patch.get_linewidth(), _dash(patch._dash_pattern)
)
if options is None:
return lambda: None
scale = float(patch.get_mutation_scale())
length = _pt(getattr(style, "head_length", 0.4) * scale)
width = _pt(2 * getattr(style, "head_width", 0.2) * scale)
begin, end = _ARROW_TIPS[arrow]
tips = "-".join(
f"{{{tip}[length={length}, width={width}]}}" if tip else ""
for tip in (begin, end)
)
options = [tips, *options]
for key, shrink in (("<", patch.shrinkA), (">", patch.shrinkB)):
if shrink:
options.append(f"shorten {key}={_pt(shrink)}")
display = patch.get_transform().transform(np.asarray(ends, dtype=float))
data = self._to_data(display)

def emit():
self.axis.add_raw(self._draw(data, options, False))

return emit

def _patch(self, patch, clip=None):
if clip is None:
clip = patch.get_clip_on()
if isinstance(patch, FancyArrowPatch):
emit = self._arrow(patch)
if emit is not None:
return emit
transform = patch.get_transform()
path = patch.get_path()
fillable = None
Expand Down
1 change: 1 addition & 0 deletions src/maxplotlib/backends/tikzfigure/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

# Unicode characters pdflatex does not typeset by default, as math.
_UNICODE_MATH = {
"⌀": r"\varnothing",
"α": r"\alpha",
"β": r"\beta",
"γ": r"\gamma",
Expand Down
31 changes: 31 additions & 0 deletions src/maxplotlib/tests/test_tikzfigure_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,37 @@ def test_text_and_annotation_arrows_are_not_clipped():
assert re.search(r"\\draw\[.*\] \(axis cs:", tikz), "the arrow is a \\draw"


def test_straight_arrows_get_tikz_tips():
from matplotlib.patches import FancyArrowPatch

fig, ax = plt.subplots()
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.add_patch(
FancyArrowPatch(
(0.1, 0.2), (0.9, 0.2), arrowstyle="<->", mutation_scale=10, shrinkA=0
)
)
ax.add_patch(FancyArrowPatch((0.1, 0.5), (0.9, 0.5), arrowstyle="-|>"))
ax.add_patch( # a curved arrow stays a path
FancyArrowPatch(
(0.1, 0.8), (0.9, 0.8), arrowstyle="->", connectionstyle="arc3,rad=0.3"
)
)
ax.text(0.5, 0.9, "⌀ 2 mm")
tikz = figure_to_tikz(fig).generate_tikz()
double = re.search(
r"\\draw\[\{Straight Barb\[length=4pt, width=4pt\]\}-\{Straight Barb\["
r"length=4pt, width=4pt\]\}, .*shorten >=2pt\] \(axis cs:0\.1,0\.2\) -- "
r"\(axis cs:0\.9,0\.2\);",
tikz,
)
assert double, tikz
assert re.search(r"\\draw\[-\{Triangle\[length=\d", tikz)
assert tikz.count("\\draw[") == 2 and tikz.count("\\addplot") >= 2 # the curve
assert "{$\\varnothing$ 2 mm}" in tikz


def test_an_annotation_arrow_without_text_is_kept():
fig, ax = plt.subplots()
ax.set_xlim(0, 1)
Expand Down
Loading