From 1df6d653c8dd4fff53a366e0aa56079afbe3b462 Mon Sep 17 00:00:00 2001 From: Krishna Acharya Date: Tue, 31 Mar 2026 03:59:17 +0545 Subject: [PATCH] Add support for AnchoredText conversion to TikZ (fixes #53) AnchoredText objects from matplotlib.offsetbox were previously ignored during conversion, producing a 'Don't know how to handle' warning. Now they are converted to TikZ \draw nodes with correct positioning and styling: - Map matplotlib loc codes (1-10) to rel axis cs coordinates with pgfplots anchors (e.g., upper left -> rel axis cs:0.02,0.98, anchor=north west) - Handle bounding box patch (fill, border, line width) - Handle font properties (size scaling, bold, italic, color) - Handle invisible patches (no box drawn) - Handle multiline text Added test_anchored_text.py with reference file. --- src/matplot2tikz/_save.py | 2 + src/matplot2tikz/_text.py | 91 ++++++++++++++++++++++++++ tests/test_anchored_text.py | 19 ++++++ tests/test_anchored_text_reference.tex | 34 ++++++++++ 4 files changed, 146 insertions(+) create mode 100644 tests/test_anchored_text.py create mode 100644 tests/test_anchored_text_reference.tex diff --git a/src/matplot2tikz/_save.py b/src/matplot2tikz/_save.py index 39e86e7..c3763c3 100644 --- a/src/matplot2tikz/_save.py +++ b/src/matplot2tikz/_save.py @@ -16,6 +16,7 @@ from matplotlib.image import AxesImage from matplotlib.legend import Legend from matplotlib.lines import Line2D +from matplotlib.offsetbox import AnchoredText from matplotlib.patches import Patch from matplotlib.spines import Spine from matplotlib.text import Text @@ -421,6 +422,7 @@ def _recurse(data: TikzData, obj: Artist) -> list: (AxesImage, img.draw_image), (Patch, _patch.draw_patch), (Collection, _draw_collection), + (AnchoredText, _text.draw_anchored_text), (Text, _text.draw_text), ): if isinstance(child, child_type): diff --git a/src/matplot2tikz/_text.py b/src/matplot2tikz/_text.py index b760e82..7d46f0a 100644 --- a/src/matplot2tikz/_text.py +++ b/src/matplot2tikz/_text.py @@ -12,6 +12,8 @@ from . import _color if TYPE_CHECKING: + from matplotlib.offsetbox import AnchoredText + from ._tikzdata import TikzData @@ -88,6 +90,95 @@ def draw_text(data: TikzData, obj: Text) -> list[str]: return content +# Mapping from matplotlib loc codes to (rel axis cs x, rel axis cs y, anchor). +_LOC_TO_TIKZ: dict[int, tuple[float, float, str]] = { + 1: (0.98, 0.98, "north east"), + 2: (0.02, 0.98, "north west"), + 3: (0.02, 0.02, "south west"), + 4: (0.98, 0.02, "south east"), + 5: (0.98, 0.5, "east"), + 6: (0.02, 0.5, "west"), + 7: (0.98, 0.5, "east"), + 8: (0.5, 0.02, "south"), + 9: (0.5, 0.98, "north"), + 10: (0.5, 0.5, "center"), +} + + +def draw_anchored_text(data: TikzData, obj: AnchoredText) -> list[str]: + """Convert a matplotlib AnchoredText to TikZ. + + :return: Content for tikz plot. + """ + # Extract the Text object(s) from the AnchoredText's TextArea. + text_children = [c for c in obj.txt.get_children() if isinstance(c, Text)] + if not text_children: + return [] + + content: list[str] = [] + ff = data.float_format + + x, y, anchor = _LOC_TO_TIKZ.get(obj.loc, (0.5, 0.5, "center")) + tikz_pos = f"(rel axis cs:{x:{ff}},{y:{ff}})" + + for text_obj in text_children: + text = text_obj.get_text() + if not text: + continue + + properties: list[str] = [] + style: list[str] = [] + + properties.append(f"anchor={anchor}") + + # Font scaling + size = text_obj.get_fontsize() + if isinstance(size, str): + size = font_scalings[size] + scaling = 0.5 * size / data.font_size + if scaling != 1.0: + properties.append(f"scale={scaling:{ff}}") + + # Bounding box from the AnchoredText's patch + bbox = obj.patch + if bbox is not None and bbox.get_visible(): + _bbox(data, bbox, properties, scaling) + + # Text color + converter = mpl.colors.ColorConverter() + col, _ = _color.mpl_color2xcolor(data, converter.to_rgb(text_obj.get_color())) + properties.append(f"text={col}") + properties.append("rotate=0.0") + + # Font style + if text_obj.get_fontstyle() == "italic": + style.append("\\itshape") + + weight = text_obj.get_fontweight() + min_weight_bold = 550 + if weight in [ + "semibold", + "demibold", + "demi", + "bold", + "heavy", + "extra bold", + "black", + ] or (isinstance(weight, int) and weight > min_weight_bold): + style.append("\\bfseries") + + if "\n" in text: + ha = text_obj.get_horizontalalignment() + properties.append(f"align={ha}") + text = text.replace("\n ", "\\\\") + + props = ",\n ".join(properties) + text = " ".join([*style, text]) + content.append(f"\\draw {tikz_pos} node[\n {props}\n]{{{text}}};\n") + + return content + + def _get_tikz_pos(data: TikzData, obj: Text, content: list[str]) -> str: """Gets the position in tikz format.""" pos = _annotation(data, obj, content) if isinstance(obj, Annotation) else obj.get_position() diff --git a/tests/test_anchored_text.py b/tests/test_anchored_text.py new file mode 100644 index 0000000..2d8be8b --- /dev/null +++ b/tests/test_anchored_text.py @@ -0,0 +1,19 @@ +"""Test for AnchoredText conversion (issue #53).""" + +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.offsetbox import AnchoredText + +from .helpers import assert_equality + + +def plot() -> Figure: + fig, ax = plt.subplots() + ax.plot([1, 2, 3], [1, 4, 9]) + at = AnchoredText("Test label", loc="upper left") + ax.add_artist(at) + return fig + + +def test() -> None: + assert_equality(plot, "test_anchored_text_reference.tex") diff --git a/tests/test_anchored_text_reference.tex b/tests/test_anchored_text_reference.tex new file mode 100644 index 0000000..3315295 --- /dev/null +++ b/tests/test_anchored_text_reference.tex @@ -0,0 +1,34 @@ +\begin{tikzpicture} + +\definecolor{darkgray176}{RGB}{176,176,176} +\definecolor{steelblue31119180}{RGB}{31,119,180} + +\begin{axis}[ +tick align=outside, +tick pos=left, +x grid style={darkgray176}, +xmin=0.9, xmax=3.1, +xtick style={color=black}, +y grid style={darkgray176}, +ymin=0.6, ymax=9.4, +ytick style={color=black} +] +\addplot [semithick, steelblue31119180] +table {% +1 1 +2 4 +3 9 +}; +\draw (rel axis cs:0.02,0.98) node[ + anchor=north west, + scale=0.5, + fill=white, + draw=black, + line width=0.4pt, + inner sep=0pt, + text=black, + rotate=0.0 +]{Test label}; +\end{axis} + +\end{tikzpicture}