diff --git a/README.md b/README.md
index 584d9056..3294f2ab 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,33 @@
-
-
-
- 🌐 Language
-
-
+
+
# 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