From 8eb2cabfba7913a75ab881e9194fa638ca40319d Mon Sep 17 00:00:00 2001 From: Alex <107329684+Takusuno@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:46:04 -0400 Subject: [PATCH 1/7] Update _patch.py Commented out a check to skip the functionality of _draw_rectangle() if the rectangle has no label. This was done intentionally, but had unintended consequences. --- src/matplot2tikz/_patch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matplot2tikz/_patch.py b/src/matplot2tikz/_patch.py index 9bf1758f..a084db13 100644 --- a/src/matplot2tikz/_patch.py +++ b/src/matplot2tikz/_patch.py @@ -143,8 +143,8 @@ def _draw_rectangle(data: TikzData, obj: Rectangle, draw_options: list) -> list[ # skipped because they likely correspong to axis/legend objects which are handled by # PGFPlots label = obj.get_label() - if label == "": - return [] + #if label == "": + # return [] # Get actual label, bar charts by default only give rectangles labels of # "_nolegend_". See . From 57f196f260904639ff4d289d66557b90e8d9c981 Mon Sep 17 00:00:00 2001 From: Alex Chrzanowski Date: Tue, 30 Sep 2025 12:15:51 -0400 Subject: [PATCH 2/7] Fixed clean_figure clipping & rect --- src/matplot2tikz/_cleanfigure.py | 33 +++++++++++++----------------- src/matplot2tikz/_patch.py | 24 +++++++++------------- src/matplot2tikz/_save.py | 35 +++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/matplot2tikz/_cleanfigure.py b/src/matplot2tikz/_cleanfigure.py index 43b2551c..404103d5 100644 --- a/src/matplot2tikz/_cleanfigure.py +++ b/src/matplot2tikz/_cleanfigure.py @@ -543,30 +543,25 @@ def _move_points_closer(x_lim: np.ndarray, y_lim: np.ndarray, data: np.ndarray) the inverse transformation to project back into 3D. """ # Calculate the extension of the extended box - x_width = x_lim[1] - x_lim[0] + # (x_width not important for clipping, as it is already dealt with elsewhere (maybe by matplotlib when lim() occurs)) + #x_width = x_lim[1] - x_lim[0] y_width = y_lim[1] - y_lim[0] # Don't choose the larger box too large to make sure that the values inside # it can still be treated by TeX. + extended_factor = 0.1 - large_xlim = x_lim + extended_factor * np.array([-x_width, x_width]) - large_ylim = y_lim + extended_factor * np.array([-y_width, y_width]) - - data_is_in_large_box = _is_in_box(data, large_xlim, large_ylim) - data_is_in_large_box = np.logical_or(data_is_in_large_box, np.any(np.isnan(data), axis=1)) - id_replace = np.argwhere(np.logical_not(data_is_in_large_box)) - - data_insert = np.array([[]]) - if not _isempty(id_replace): - msg = ( - "There is data outside of the box. Don't know how to handle during cleaning. " - "Please check if x/ylim is to tight" - ) - raise NotImplementedError(msg) - data = _insert_data(data, id_replace, data_insert) - if _isempty(id_replace): - return data - raise NotImplementedError + y_min_ext = y_lim[0] - extended_factor * y_width + y_max_ext = y_lim[1] + extended_factor * y_width + + # Copy data to avoid modifying original array + clipped_data = np.array(data, copy=True) + + # Clip y-values + # We assume data is Nx2: columns [x, y] + clipped_data[:, 1] = np.clip(clipped_data[:, 1], y_min_ext, y_max_ext) + + return clipped_data def _insert_data( diff --git a/src/matplot2tikz/_patch.py b/src/matplot2tikz/_patch.py index a084db13..0f5f4dfc 100644 --- a/src/matplot2tikz/_patch.py +++ b/src/matplot2tikz/_patch.py @@ -137,29 +137,21 @@ def _draw_polygon(data: TikzData, obj: Patch, draw_options: list) -> list[str]: def _draw_rectangle(data: TikzData, obj: Rectangle, draw_options: list) -> list[str]: - """Return the PGFPlots code for rectangles.""" - # Objects with labels are plot objects (from bar charts, etc). Even those without - # labels explicitly set have a label of "_nolegend_". Everything else should be - # skipped because they likely correspong to axis/legend objects which are handled by - # PGFPlots + # Skip the Axes background rectangle, which is always white (0,0)-(1,1) + label = obj.get_label() - #if label == "": - # return [] - # Get actual label, bar charts by default only give rectangles labels of - # "_nolegend_". See . + # Try to resolve a more useful label if it's "_nolegend_" if isinstance(obj.axes, Axes): handles, labels = obj.axes.get_legend_handles_labels() - labels_found = [label for h, label in zip(handles, labels) if obj in h.get_children()] + labels_found = [lab for h, lab in zip(handles, labels) if obj in h.get_children()] if len(labels_found) == 1: label = labels_found[0] left_lower_x = obj.get_x() left_lower_y = obj.get_y() - # If we are dealing with a bar plot, left_lower_y will be 0. This is a problem if the y-scale is - # logarithmic (see https://github.com/ErwindeGelder/matplot2tikz/issues/25) - # To resolve this, the lower y limit will be used as lower_left_y + # Handle log-scale bar plots if data.current_mpl_axes is not None and data.current_mpl_axes.get_yscale() == "log": left_lower_y = data.current_mpl_axes.get_ylim()[0] @@ -172,7 +164,10 @@ def _draw_rectangle(data: TikzData, obj: Rectangle, draw_options: list) -> list[ f"rectangle (axis cs:{right_upper_x:{ff}},{right_upper_y:{ff}});\n" ] - if label != "_nolegend_" and str(label) not in data.rectangle_legends: + # Add legend info if needed + # content.append(_patch_legend(obj, draw_options, "area legend")) + + if label not in ("", "_nolegend_") and str(label) not in data.rectangle_legends: data.rectangle_legends.add(str(label)) draw_opts = ",".join(draw_options) content.append(f"\\addlegendimage{{ybar,ybar legend,{draw_opts}}}\n") @@ -181,6 +176,7 @@ def _draw_rectangle(data: TikzData, obj: Rectangle, draw_options: list) -> list[ return content + def _draw_ellipse(data: TikzData, obj: Ellipse, draw_options: list) -> list[str]: """Return the PGFPlots code for ellipses.""" if isinstance(obj, Circle): diff --git a/src/matplot2tikz/_save.py b/src/matplot2tikz/_save.py index 0bb0c1c2..dcc10d99 100644 --- a/src/matplot2tikz/_save.py +++ b/src/matplot2tikz/_save.py @@ -4,6 +4,7 @@ import sys import tempfile import warnings +from numpy import isclose, all from pathlib import Path from typing import TYPE_CHECKING, TypedDict @@ -385,12 +386,44 @@ def _recurse(data: TikzData, obj: Artist) -> list: Content is returned. """ content = _ContentManager() + for child in obj.get_children(): # Some patches are Spines, too; skip those entirely. # See . - if isinstance(child, (Spine, XAxis, YAxis)): + + # Filter out the Figure's background patch + if ( + (isinstance(obj, Figure) + and isinstance(child, Patch) + and child is obj.patch + and child.get_facecolor() == (1.0, 1.0, 1.0, 1.0) # White face color + #and (child.get_edgecolor() is None or all(isclose(child.get_edgecolor(), (0.0, 0.0, 0.0, 0.0)))) # No edge color + and child.get_linewidth() == 0.0) + or not child.get_visible() + ): + if not (child.get_edgecolor() is None or all(isclose(child.get_edgecolor(), (0.0, 0.0, 0.0, 0.0)))): + print(child.get_edgecolor()) + print(child.get_edgecolor() == (0.0, 0.0, 0.0, 0.0)) # No edge color + continue + + + # Filter out the Axes' background patch + + if ( + (isinstance(obj, Axes) + and isinstance(child, Patch) + and child is obj.patch + and child.get_facecolor() == (1.0, 1.0, 1.0, 1.0) # White face color + #and (child.get_edgecolor() is None or all(isclose(child.get_edgecolor(), (0.0, 0.0, 0.0, 0.0)))) # No edge color + and child.get_linewidth() == 0.0) + or not child.get_visible() + ): continue + + if isinstance(child, (Spine, XAxis, YAxis)): + continue + if isinstance(child, Axes): _process_axes(data, child, content) From 4a6473bd9d4be8114c2b4d52a65f3f3e7a22d0f2 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 18 Nov 2025 02:41:42 -0500 Subject: [PATCH 3/7] Fixed newline issue in titles Previously, if you were to put '\n' in your title to make a newline, it would not translate to the tex code, and instead would be eaten by the transformation. Now, it should add \\ instead, which won't be eaten, and add "title style={align=center}" which is needed for the \\ to work. Could also change this to be more customizable (e.g. align=left, align=right) if needed. --- src/matplot2tikz/_axes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matplot2tikz/_axes.py b/src/matplot2tikz/_axes.py index 0dd5b95b..fcf0aab9 100644 --- a/src/matplot2tikz/_axes.py +++ b/src/matplot2tikz/_axes.py @@ -68,6 +68,9 @@ def _set_plot_title(self) -> None: self.data.current_axis_title = title if title: title = _common_texification(title) + if '\n' in title: + title = title.replace('\n',r'\\') + self.data.current_axis_options.add(r"title style={align=center}") self.data.current_axis_options.add(f"title={{{title}}}") def _set_axis_titles(self) -> None: From 6e5afcce51a9cc88c1d263721b95153d7623ad32 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 18 Nov 2025 16:07:09 -0500 Subject: [PATCH 4/7] Added support for \\ in title to represent newline --- src/matplot2tikz/_axes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matplot2tikz/_axes.py b/src/matplot2tikz/_axes.py index fcf0aab9..731be54d 100644 --- a/src/matplot2tikz/_axes.py +++ b/src/matplot2tikz/_axes.py @@ -71,6 +71,8 @@ def _set_plot_title(self) -> None: if '\n' in title: title = title.replace('\n',r'\\') self.data.current_axis_options.add(r"title style={align=center}") + elif '\\' in title: + self.data.current_axis_options.add(r"title style={align=center}") self.data.current_axis_options.add(f"title={{{title}}}") def _set_axis_titles(self) -> None: From 32d28a29ba49b98efbf478a7ab7b84d92bbedc31 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 8 Dec 2025 17:10:25 -0500 Subject: [PATCH 5/7] Quick protection fix --- src/matplot2tikz/_save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matplot2tikz/_save.py b/src/matplot2tikz/_save.py index dcc10d99..f6f77b61 100644 --- a/src/matplot2tikz/_save.py +++ b/src/matplot2tikz/_save.py @@ -401,7 +401,7 @@ def _recurse(data: TikzData, obj: Artist) -> list: and child.get_linewidth() == 0.0) or not child.get_visible() ): - if not (child.get_edgecolor() is None or all(isclose(child.get_edgecolor(), (0.0, 0.0, 0.0, 0.0)))): + if hasattr(child, "get_edgecolor") and not (child.get_edgecolor() is None or all(isclose(child.get_edgecolor(), (0.0, 0.0, 0.0, 0.0)))): print(child.get_edgecolor()) print(child.get_edgecolor() == (0.0, 0.0, 0.0, 0.0)) # No edge color continue From 29a03823063ad0ad48476cd6ebb7eb7129617385 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Dec 2025 00:15:47 -0500 Subject: [PATCH 6/7] Added support for multiple axes plots Changed get_legend_text() in _util.py, added an import _util in _save.py and added support for dependent plots in _process_axes, and added a separate option for dependent plots in draw_line2d in _line2d.py --- src/matplot2tikz/_line2d.py | 6 +++++- src/matplot2tikz/_save.py | 16 ++++++++++++++-- src/matplot2tikz/_util.py | 10 ++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/matplot2tikz/_line2d.py b/src/matplot2tikz/_line2d.py index 94cc914b..eb641d64 100644 --- a/src/matplot2tikz/_line2d.py +++ b/src/matplot2tikz/_line2d.py @@ -65,6 +65,8 @@ def draw_line2d(data: TikzData, obj: Line2D) -> list[str]: if len(xdata) == 0: return [] + + primitive_legend = obj.axes.get_legend() # Get several plot options addplot_options = _get_line2d_options(data, obj) @@ -82,8 +84,10 @@ def draw_line2d(data: TikzData, obj: Line2D) -> list[str]: content += _table(data, obj) - if legend_text is not None: + if legend_text is not None and primitive_legend is not None: content.append(f"\\addlegendentry{{{legend_text}}}\n") + elif legend_text is not None and primitive_legend is None: + content.append(f"\\label{{{legend_text + "_plot"}}}\n") return content diff --git a/src/matplot2tikz/_save.py b/src/matplot2tikz/_save.py index f6f77b61..91118782 100644 --- a/src/matplot2tikz/_save.py +++ b/src/matplot2tikz/_save.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from matplotlib.artist import Artist -from . import _axes, _legend, _line2d, _patch, _path, _text +from . import _axes, _legend, _line2d, _patch, _path, _text, _util from . import _image as img from . import _quadmesh as qmsh from .__about__ import __version__ @@ -460,12 +460,24 @@ def _process_axes(data: TikzData, obj: Axes, content: _ContentManager) -> None: # add extra axis options if data.extra_axis_parameters: data.current_axis_options.update(data.extra_axis_parameters) - + data.current_mpl_axes = obj # Run through the child objects, gather the content. children_content = _recurse(data, obj) + fig = obj.figure + if obj == fig.axes[0]: + for other_ax in fig.axes: + if other_ax == obj: + continue + for child in other_ax.get_children(): + legend_text = _util.get_legend_text(child) + if legend_text is not None and hasattr(child, "axes") and child.axes.get_legend() is None: + plot_label = child.get_label() + "_plot" + children_content.append(f"\\addlegendimage{{/pgfplots/refstyle={plot_label}}}\n") + children_content.append(f"\\addlegendentry{{{legend_text}}}\n") + # populate content and add axis environment if desired if data.add_axis_environment: content.extend(ax.get_begin_code() + children_content + [ax.get_end_code()], 0) diff --git a/src/matplot2tikz/_util.py b/src/matplot2tikz/_util.py index ef740c16..520f977e 100644 --- a/src/matplot2tikz/_util.py +++ b/src/matplot2tikz/_util.py @@ -24,7 +24,17 @@ def get_legend_text(obj: Line2D | PathCollection) -> str | None: """Check if line is in legend.""" if obj.axes is None: return None + leg = obj.axes.get_legend() + + if leg is None: + fig = obj.axes.figure + for ax in fig.axes: + other_leg = ax.get_legend() + if other_leg is not None: + leg = other_leg + break + if leg is None: return None From 0d06925db986b8833fa3f3ad29f31ee1af45eb8c Mon Sep 17 00:00:00 2001 From: Erwin de Gelder Date: Mon, 15 Dec 2025 08:49:59 +0100 Subject: [PATCH 7/7] Weird typo. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f75a040..3294f2ab 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ For contributing, follow these steps: successful. Run `tox`. This does a linting check and runs all test scripts. To manually perform these steps, use the following commands: 1. Run `tox -e lint`. You can do the linting commands manually using: - 1. (One time) `uv pip install -r requirements-lint.txtAS` + 1. (One time) `uv pip install -r requirements-lint.txt` 2. `ruff format . --check` (remove the `--check` flag to let `ruff` do the formatting) 3. `ruff check .` 4. `mypy .`