diff --git a/src/matplot2tikz/_axes.py b/src/matplot2tikz/_axes.py index 0dd5b95b..731be54d 100644 --- a/src/matplot2tikz/_axes.py +++ b/src/matplot2tikz/_axes.py @@ -68,6 +68,11 @@ 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}") + 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: 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/_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/_patch.py b/src/matplot2tikz/_patch.py index 4532b518..9456d8b3 100644 --- a/src/matplot2tikz/_patch.py +++ b/src/matplot2tikz/_patch.py @@ -138,29 +138,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] @@ -173,7 +165,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") @@ -182,6 +177,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..91118782 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 @@ -24,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__ @@ -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 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 + + + # 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) @@ -427,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