diff --git a/README.md b/README.md index 584d9056..3294f2ab 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,33 @@ - -
-
- 🌐 Language -
-
- English - | 简体中文 - | 繁體中文 - | 日本語 - | 한국어 - | हिन्दी - | ไทย - | Français - | Deutsch - | Español - | Italiano - | Русский - | Português - | Nederlands - | Polski - | العربية - | فارسی - | Türkçe - | Tiếng Việt - | Bahasa Indonesia - | অসমীয়া -
-
+ +
+
+ 🌐 Language +
+
+ English + | 简体中文 + | 繁體中文 + | 日本語 + | 한국어 + | हिन्दी + | ไทย + | Français + | Deutsch + | Español + | Italiano + | Русский + | Português + | Nederlands + | Polski + | العربية + | فارسی + | Türkçe + | Tiếng Việt + | Bahasa Indonesia + | অসমীয়া +
+
# matplot2tikz diff --git a/src/matplot2tikz/_axes.py b/src/matplot2tikz/_axes.py index 68b0bc87..2de1211a 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 39533099..8b28d2f6 100644 --- a/src/matplot2tikz/_cleanfigure.py +++ b/src/matplot2tikz/_cleanfigure.py @@ -206,7 +206,7 @@ def _cleanline( cfd.visual_data = _get_visual_data(cfd.axes, cfd.data) if not isinstance(linehandle, art3d.Line3D): - cfd.visual_data = _move_points_closer(cfd.x_lim, cfd.y_lim, cfd.visual_data) + cfd.visual_data = _move_points_closer(cfd.y_lim, cfd.visual_data) cfd.has_markers = linehandle.get_marker() != "None" cfd.has_lines = linehandle.get_linestyle() != "None" @@ -244,7 +244,7 @@ def _clean_collections( cfd.visual_data = _get_visual_data(cfd.axes, cfd.data) if not isinstance(collection, art3d.Path3DCollection): - cfd.visual_data = _move_points_closer(cfd.x_lim, cfd.y_lim, cfd.visual_data) + cfd.visual_data = _move_points_closer(cfd.y_lim, cfd.visual_data) cfd.visual_data = _get_visual_data(cfd.axes, cfd.visual_data) cfd.has_markers = True @@ -530,7 +530,7 @@ def _prune_outside_box(cfd: CleanFigureData) -> np.ndarray: return _remove_nans(data) -def _move_points_closer(x_lim: np.ndarray, y_lim: np.ndarray, data: np.ndarray) -> np.ndarray: +def _move_points_closer(y_lim: np.ndarray, data: np.ndarray) -> np.ndarray: """Move points closer if needed. Move all points outside a box much larger than the visible one @@ -538,35 +538,29 @@ def _move_points_closer(x_lim: np.ndarray, y_lim: np.ndarray, data: np.ndarray) box are preserved. This typically involves replacing one point by two new ones and a NaN. + Only y-coordinates considered as x-coordinates are already dealt with + elsewhere (but where?) + Not implemented: 3D simplification of frontal 2D projection. This requires the full transformation rather than the projection, as we have to calculate the inverse transformation to project back into 3D. """ # Calculate the extension of the extended box - 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 a3eb21b7..14ebfaff 100644 --- a/src/matplot2tikz/_line2d.py +++ b/src/matplot2tikz/_line2d.py @@ -69,6 +69,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) # Check if a line is in a legend and forget it if not. @@ -85,8 +87,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 bafe2609..cfbbf6d4 100644 --- a/src/matplot2tikz/_patch.py +++ b/src/matplot2tikz/_patch.py @@ -140,17 +140,9 @@ 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 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 = [ @@ -162,9 +154,7 @@ def _draw_rectangle(data: TikzData, obj: Rectangle, draw_options: list) -> list[ 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] @@ -177,7 +167,7 @@ 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: + 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") @@ -198,13 +188,11 @@ def _draw_ellipse(data: TikzData, obj: Ellipse, draw_options: list) -> list[str] draw_options.append(f"rotate around={{{obj.angle:{ff}}:(axis cs:{x:{ff}},{y:{ff}})}}") do = ",".join(draw_options) - content = [ + return [ f"\\draw[{do}] (axis cs:{x:{ff}},{y:{ff}}) ellipse " - f"({0.5 * obj.width:{ff}} and {0.5 * obj.height:{ff}});\n" + f"({0.5 * obj.width:{ff}} and {0.5 * obj.height:{ff}});\n", + _patch_legend(obj, draw_options, "area legend"), ] - content.append(_patch_legend(obj, draw_options, "area legend")) - - return content def _draw_circle(data: TikzData, obj: Circle, draw_options: list) -> list[str]: diff --git a/src/matplot2tikz/_save.py b/src/matplot2tikz/_save.py index 0bb0c1c2..70d7611a 100644 --- a/src/matplot2tikz/_save.py +++ b/src/matplot2tikz/_save.py @@ -24,7 +24,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,7 +385,28 @@ def _recurse(data: TikzData, obj: Artist) -> list: Content is returned. """ content = _ContentManager() + for child in obj.get_children(): + # 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_linewidth() == 0.0 + ) or not child.get_visible(): + 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_linewidth() == 0.0 + ) or not child.get_visible(): + continue + # Some patches are Spines, too; skip those entirely. # See . if isinstance(child, (Spine, XAxis, YAxis)): @@ -433,6 +454,24 @@ def _process_axes(data: TikzData, obj: Axes, content: _ContentManager) -> None: # 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 00062692..f912a596 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