diff --git a/.gitignore b/.gitignore
index 6d712b30..cc40fcda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
*.egg-info/
*.pdf
*.png
+!examples/output/png/
+!examples/output/png/*.png
*.pyc
*.swp
.cache/
diff --git a/README.md b/README.md
index 3c51fbbc..9a02cb83 100644
--- a/README.md
+++ b/README.md
@@ -131,8 +131,25 @@ Tweaking the plot is straightforward and can be done as part of your TeX work fl
[The fantastic PGFPlots manual](http://pgfplots.sourceforge.net/pgfplots.pdf) contains
great examples of how to make your plot look even better.
-Of course, not all figures produced by matplotlib can be converted without error.
-Notably, [3D plots don't work](https://github.com/matplotlib/matplotlib/issues/7243).
+Of course, not all figures produced by matplotlib can be converted without error.
+3D plots are supported for the common mplot3d artists exported by matplot2tikz,
+including 3D lines, scatter plots, surfaces, wireframes, contours, bar charts,
+quiver plots, custom ticks, and optional 3D axis-limit clipping.
+
+### 3D examples
+
+The example generator in [`examples`](examples) writes standalone PGFPlots files to
+`examples/output/tex`.
+The previews below were compiled with `pdflatex` and rendered from the generated
+PDFs.
+
+| Line, scatter, text | Surface and wireframe | Contours |
+| --- | --- | --- |
+|  |  |  |
+| 3D bars with custom ticks | Semantic quiver | Log-scale clipping |
+|  |  |  |
+| No clipping | Hide outside limits | Clip to limits |
+|  |  |  |
## Installation
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..ce951d3f
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,18 @@
+# 3D examples
+
+Run the gallery generator from the repository root:
+
+```powershell
+$env:PYTHONPATH = "src"
+python examples/plot_3d_gallery.py
+```
+
+or on POSIX shells:
+
+```sh
+PYTHONPATH=src python examples/plot_3d_gallery.py
+```
+
+The script writes standalone PGFPlots examples into `examples/output/tex/`. If
+`pdflatex` and `pdftoppm` are available, it also compiles the examples and writes
+PNG previews into `examples/output/png/`.
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 00000000..1994f5b4
--- /dev/null
+++ b/examples/__init__.py
@@ -0,0 +1 @@
+"""Example scripts for matplot2tikz."""
diff --git a/examples/output/png/bar3d.png b/examples/output/png/bar3d.png
new file mode 100644
index 00000000..16deedd0
Binary files /dev/null and b/examples/output/png/bar3d.png differ
diff --git a/examples/output/png/clipping_clip.png b/examples/output/png/clipping_clip.png
new file mode 100644
index 00000000..45bb9b08
Binary files /dev/null and b/examples/output/png/clipping_clip.png differ
diff --git a/examples/output/png/clipping_hide.png b/examples/output/png/clipping_hide.png
new file mode 100644
index 00000000..76572f31
Binary files /dev/null and b/examples/output/png/clipping_hide.png differ
diff --git a/examples/output/png/clipping_none.png b/examples/output/png/clipping_none.png
new file mode 100644
index 00000000..d854bc5d
Binary files /dev/null and b/examples/output/png/clipping_none.png differ
diff --git a/examples/output/png/contour_projection.png b/examples/output/png/contour_projection.png
new file mode 100644
index 00000000..02fc4720
Binary files /dev/null and b/examples/output/png/contour_projection.png differ
diff --git a/examples/output/png/line_scatter_text.png b/examples/output/png/line_scatter_text.png
new file mode 100644
index 00000000..2eeaef17
Binary files /dev/null and b/examples/output/png/line_scatter_text.png differ
diff --git a/examples/output/png/log_clipping.png b/examples/output/png/log_clipping.png
new file mode 100644
index 00000000..8cf7eed9
Binary files /dev/null and b/examples/output/png/log_clipping.png differ
diff --git a/examples/output/png/quiver3d.png b/examples/output/png/quiver3d.png
new file mode 100644
index 00000000..10c9475b
Binary files /dev/null and b/examples/output/png/quiver3d.png differ
diff --git a/examples/output/png/surface_wireframe.png b/examples/output/png/surface_wireframe.png
new file mode 100644
index 00000000..4ef0bb42
Binary files /dev/null and b/examples/output/png/surface_wireframe.png differ
diff --git a/examples/plot_3d_gallery.py b/examples/plot_3d_gallery.py
new file mode 100644
index 00000000..3e6c8664
--- /dev/null
+++ b/examples/plot_3d_gallery.py
@@ -0,0 +1,292 @@
+"""Generate the 3D example gallery used by the README."""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+from pathlib import Path
+from typing import TYPE_CHECKING, Literal, cast
+
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+import matplot2tikz
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
+ from matplotlib.figure import Figure
+ from mpl_toolkits.mplot3d import Axes3D
+
+mpl.use("Agg")
+
+ROOT = Path(__file__).resolve().parents[1]
+EXAMPLE_DIR = ROOT / "examples"
+TEX_DIR = EXAMPLE_DIR / "output" / "tex"
+PNG_DIR = EXAMPLE_DIR / "output" / "png"
+BUILD_DIR = EXAMPLE_DIR / "output" / "build"
+Clip3DMode = Literal["none", "hide", "clip"]
+
+
+def line_scatter_text() -> Figure:
+ """Return a 3D line, scatter, legend, and text example."""
+ fig = plt.figure(figsize=(4.2, 3.2))
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ theta = np.linspace(0.0, 2.6 * np.pi, 22)
+ radius = 0.18 + 0.035 * theta
+ ax.plot(
+ radius * np.cos(theta),
+ radius * np.sin(theta),
+ 0.12 * theta,
+ color="tab:red",
+ marker="o",
+ label="spiral",
+ )
+ x = np.linspace(-0.8, 0.8, 8)
+ ax.plot(x, 0.2 * np.sin(4 * x), 0.25 + 0.15 * np.cos(3 * x), "--", label="ridge")
+ xs, ys = np.meshgrid(np.linspace(-0.5, 0.5, 4), np.linspace(-0.4, 0.4, 3))
+ zs = 0.2 + xs**2 + 0.35 * ys**2
+ ax.scatter(xs.ravel(), ys.ravel(), zs.ravel(), c=zs.ravel(), s=32, cmap="viridis")
+ ax.text(0.0, -0.55, 0.78, "3D text")
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.legend(loc="upper left")
+ ax.view_init(elev=28.0, azim=38.0)
+ return fig
+
+
+def surface_wireframe() -> Figure:
+ """Return a surface with an overlaid wireframe."""
+ fig = plt.figure(figsize=(4.2, 3.2))
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.linspace(-2.5, 2.5, 11)
+ y = np.linspace(-2.5, 2.5, 11)
+ xx, yy = np.meshgrid(x, y)
+ zz = np.sin(np.hypot(xx, yy))
+ ax.plot_surface(xx, yy, zz, cmap="viridis", edgecolor="black", linewidth=0.2, alpha=0.9)
+ ax.plot_wireframe(xx, yy, zz + 0.75, color="black", linewidth=0.45, rstride=2, cstride=2)
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.view_init(elev=24.0, azim=-55.0)
+ return fig
+
+
+def contour_projection() -> Figure:
+ """Return 3D contour lines and filled contour patches."""
+ fig = plt.figure(figsize=(4.2, 3.2))
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.linspace(-2.0, 2.0, 10)
+ y = np.linspace(-2.0, 2.0, 10)
+ xx, yy = np.meshgrid(x, y)
+ zz = np.cos(xx) * np.sin(yy)
+ ax.contour(
+ xx,
+ yy,
+ zz,
+ levels=[-0.6, -0.2, 0.2, 0.6],
+ colors=["navy", "teal", "orange", "crimson"],
+ )
+ ax.contourf(xx, yy, zz, zdir="z", offset=-1.1, levels=5, cmap="cividis", alpha=0.65)
+ ax.set_zlim(-1.1, 1.0)
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.view_init(elev=25.0, azim=105.0)
+ return fig
+
+
+def bars() -> Figure:
+ """Return a 3D bar chart with custom tick labels."""
+ fig = plt.figure(figsize=(4.2, 3.2))
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ xpos, ypos = np.meshgrid([0.0, 1.0, 2.0], [0.0, 1.0])
+ heights = np.array([0.5, 1.2, 0.8, 1.4, 0.7, 1.6])
+ colors = plt.get_cmap("cividis")((heights - heights.min()) / np.ptp(heights))
+ ax.bar3d(
+ xpos.ravel(),
+ ypos.ravel(),
+ np.zeros(6),
+ 0.55,
+ 0.55,
+ heights,
+ color=colors,
+ edgecolor="black",
+ shade=False,
+ )
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.set_xticks([0.25, 1.25, 2.25], ["low", "mid", "high"])
+ ax.set_yticks([0.25, 1.25], ["front", "back"])
+ ax.view_init(elev=26.0, azim=35.0)
+ return fig
+
+
+def quiver() -> Figure:
+ """Return a semantic 3D quiver example."""
+ fig = plt.figure(figsize=(4.2, 3.2))
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.array([0.0, 0.9, 0.0, 0.9])
+ y = np.array([0.0, 0.0, 0.8, 0.8])
+ z = np.array([0.0, 0.2, 0.1, 0.3])
+ u = np.array([0.45, -0.2, 0.25, -0.3])
+ v = np.array([0.15, 0.35, -0.25, -0.2])
+ w = np.array([0.35, 0.25, 0.45, 0.2])
+ ax.quiver(x, y, z, u, v, w, length=0.8, arrow_length_ratio=0.25, color="tab:green")
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.view_init(elev=30.0, azim=-45.0)
+ return fig
+
+
+def clipping_surface() -> Figure:
+ """Return a surface and wireframe scene that crosses the 3D axis box."""
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.arange(-5, 5, 0.5)
+ y = np.arange(-5, 5, 0.5)
+ xx, yy = np.meshgrid(x, y)
+ zz = np.sin(np.sqrt(xx**2 + yy**2))
+ ax.plot_surface(
+ xx,
+ yy,
+ zz,
+ cmap=plt.get_cmap("viridis"),
+ edgecolor="black",
+ linewidth=0.25,
+ alpha=0.88,
+ )
+ ax.plot_wireframe(
+ xx,
+ yy,
+ zz + 0.75,
+ color="black",
+ linewidth=0.45,
+ rstride=2,
+ cstride=2,
+ )
+ ax.contour(
+ xx,
+ yy,
+ zz,
+ levels=[-0.2, 0.0, 0.2],
+ zdir="z",
+ offset=-0.85,
+ cmap="viridis",
+ linewidths=0.6,
+ )
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.set_xlim(-4, 4)
+ ax.set_ylim(-4, 4)
+ ax.set_zlim(-1, 0.5)
+ ax.view_init(elev=24.0, azim=-55.0)
+
+ return fig
+
+
+def clipping_surface_none() -> Figure:
+ """Return the clipping comparison scene without export clipping."""
+ return clipping_surface()
+
+
+def clipping_surface_hide() -> Figure:
+ """Return the clipping comparison scene for hide clipping."""
+ return clipping_surface()
+
+
+def clipping_surface_clip() -> Figure:
+ """Return the clipping comparison scene for geometric clipping."""
+ return clipping_surface()
+
+
+def log_clipping() -> Figure:
+ """Return a log-scaled 3D line clipped in axis-scale coordinates."""
+ fig = plt.figure(figsize=(4.2, 3.2))
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot([0.1, 10.0], [0.0, 2.0], [1.0, 1.0], marker="o", color="tab:blue")
+ ax.set_xlim(1.0, 10.0)
+ ax.set_xscale("log")
+ ax.set_ylim(0.0, 2.0)
+ ax.set_zlim(0.0, 2.0)
+ ax.set_xlabel(r"$\log_{10} \mathrm{X}$")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.view_init(elev=24.0, azim=-35.0)
+ return fig
+
+
+EXAMPLES: tuple[tuple[str, str, Callable[[], Figure], Clip3DMode], ...] = (
+ ("line_scatter_text", "Line, scatter, text", line_scatter_text, "none"),
+ ("surface_wireframe", "Surface and wireframe", surface_wireframe, "none"),
+ ("contour_projection", "Contours and filled contours", contour_projection, "none"),
+ ("bar3d", "3D bars", bars, "none"),
+ ("quiver3d", "Semantic quiver", quiver, "none"),
+ ("log_clipping", "Log-scale clipping", log_clipping, "clip"),
+ ("clipping_none", "No clipping", clipping_surface_none, "none"),
+ ("clipping_hide", "Hide outside", clipping_surface_hide, "hide"),
+ ("clipping_clip", "Clip to limits", clipping_surface_clip, "clip"),
+)
+
+
+def main() -> None:
+ """Generate TeX examples and PNG previews when the local TeX tools exist."""
+ TEX_DIR.mkdir(parents=True, exist_ok=True)
+ PNG_DIR.mkdir(parents=True, exist_ok=True)
+ BUILD_DIR.mkdir(parents=True, exist_ok=True)
+
+ expected_names = {name for name, _title, _plot, _clip_3d in EXAMPLES}
+ for output_dir, suffix in ((TEX_DIR, ".tex"), (PNG_DIR, ".png")):
+ for path in output_dir.glob(f"*{suffix}"):
+ if path.stem not in expected_names:
+ path.unlink()
+
+ pdflatex = shutil.which("pdflatex")
+ pdftoppm = shutil.which("pdftoppm")
+ for name, _title, plot, clip_3d in EXAMPLES:
+ fig = plot()
+ tex_path = TEX_DIR / f"{name}.tex"
+ fig.savefig(BUILD_DIR / f"{name}_reference.png", dpi=fig.dpi)
+ matplot2tikz.save(
+ tex_path,
+ figure=fig,
+ standalone=True,
+ include_disclaimer=False,
+ float_format=".8g",
+ clip_3d=clip_3d,
+ )
+ plt.close(fig)
+
+ if pdflatex is None or pdftoppm is None:
+ continue
+
+ subprocess.run( # noqa: S603
+ [
+ pdflatex,
+ "-interaction=nonstopmode",
+ "-halt-on-error",
+ "-output-directory",
+ str(BUILD_DIR),
+ str(tex_path),
+ ],
+ check=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT,
+ )
+ pdf_path = BUILD_DIR / f"{name}.pdf"
+ png_stem = PNG_DIR / name
+ subprocess.run( # noqa: S603
+ [pdftoppm, "-png", "-singlefile", "-r", "170", str(pdf_path), str(png_stem)],
+ check=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/matplot2tikz/_axes.py b/src/matplot2tikz/_axes.py
index 72d976b3..d592208e 100644
--- a/src/matplot2tikz/_axes.py
+++ b/src/matplot2tikz/_axes.py
@@ -1,13 +1,14 @@
from __future__ import annotations
import re
-from collections.abc import Iterable, Sized
-from typing import TYPE_CHECKING
+from collections.abc import Iterable, Sequence, Sized
+from typing import TYPE_CHECKING, cast
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Subplot
from matplotlib.colors import Colormap, LinearSegmentedColormap, ListedColormap
+from mpl_toolkits.mplot3d import Axes3D
from . import _color
from ._util import _common_texification
@@ -15,6 +16,8 @@
if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.colorbar import Colorbar
+ from matplotlib.lines import Line2D
+ from matplotlib.text import Text
from ._tikzdata import TikzData
@@ -32,6 +35,8 @@ def __init__(self, data: TikzData, obj: Axes) -> None:
if self.is_colorbar:
return
+ self.is_3d = isinstance(obj, Axes3D)
+
# instantiation
self.nsubplots = 1
self.subplot_index = 0
@@ -45,7 +50,8 @@ def __init__(self, data: TikzData, obj: Axes) -> None:
self._set_hide_axis()
self._set_plot_title()
self._set_axis_titles()
- xlim, ylim = self._set_axis_limits()
+ xlim, ylim, _ = self._set_axis_limits()
+ self._set_view()
self._set_axis_scaling()
self._set_axis_on_top()
self._set_axis_dimensions(self._get_aspect_ratio(), xlim, ylim)
@@ -59,7 +65,9 @@ def __init__(self, data: TikzData, obj: Axes) -> None:
def _set_hide_axis(self) -> None:
# check if axes need to be displayed at all
- if not self.obj.axison:
+ if self.is_3d and not self.obj._axis3don: # type: ignore[attr-defined] # noqa: SLF001
+ self.data.current_axis_options.add("hide axis")
+ elif not self.is_3d and not self.obj.axison:
self.data.current_axis_options.add("hide x axis")
self.data.current_axis_options.add("hide y axis")
@@ -87,7 +95,7 @@ def _set_axis_titles(self) -> None:
self.data.current_axis_options.add(f"xlabel={{{xlabel}}}")
xrotation = self.obj.xaxis.get_label().get_rotation()
- if xrotation != 0:
+ if not self.is_3d and xrotation != 0:
self.data.current_axis_options.add(f"xlabel style={{rotate={xrotation - 90}}}")
ylabel = self.obj.get_ylabel()
@@ -102,10 +110,22 @@ def _set_axis_titles(self) -> None:
self.data.current_axis_options.add(f"ylabel={{{ylabel}}}")
yrotation = self.obj.yaxis.get_label().get_rotation()
- if yrotation != 90: # noqa: PLR2004
+ if not self.is_3d and yrotation != 90: # noqa: PLR2004
self.data.current_axis_options.add(f"ylabel style={{rotate={yrotation - 90}}}")
- def _set_axis_limits(self) -> tuple[list[float], list[float]]:
+ if self.is_3d:
+ zlabel = self.obj.get_zlabel() # type: ignore[attr-defined]
+ if zlabel:
+ zlabel = _common_texification(zlabel)
+
+ labelcolor = self.obj.zaxis.label.get_color() # type: ignore[attr-defined]
+ if labelcolor != "black":
+ col, _ = _color.mpl_color2xcolor(self.data, labelcolor)
+ self.data.current_axis_options.add(f"zlabel=\\textcolor{{{col}}}{{{zlabel}}}")
+ else:
+ self.data.current_axis_options.add(f"zlabel={{{zlabel}}}")
+
+ def _set_axis_limits(self) -> tuple[list[float], list[float], list[float] | None]:
ff = self.data.float_format
xlim = list(self.obj.get_xlim())
xlim0, xlim1 = sorted(xlim)
@@ -119,7 +139,24 @@ def _set_axis_limits(self) -> tuple[list[float], list[float]]:
self.data.current_axis_options.add("x dir=reverse")
if ylim != sorted(ylim):
self.data.current_axis_options.add("y dir=reverse")
- return xlim, ylim
+ # For 3D axes, also set zlim.
+ if self.is_3d:
+ zlim = list(self.obj.get_zlim()) # type: ignore[attr-defined]
+ zlim0, zlim1 = sorted(zlim)
+ self.data.current_axis_options.add(f"zmin={zlim0:{ff}}, zmax={zlim1:{ff}}")
+ if zlim != sorted(zlim):
+ self.data.current_axis_options.add("z dir=reverse")
+ return xlim, ylim, zlim
+ return xlim, ylim, None
+
+ def _set_view(self) -> None:
+ if not self.is_3d:
+ return
+ ff = self.data.float_format
+ # PGFPlots' 3D azimuth is rotated 90 degrees relative to mplot3d.
+ azim = self.obj.azim + 90 # type: ignore[attr-defined]
+ elev = self.obj.elev # type: ignore[attr-defined]
+ self.data.current_axis_options.add("view={" + f"{azim:{ff}}" + "}{" + f"{elev:{ff}}" + "}")
def _set_axis_scaling(self) -> None:
if self.obj.get_xscale() == "log":
@@ -132,6 +169,11 @@ def _set_axis_scaling(self) -> None:
self.data.current_axis_options.add(
f"log basis y={{{_try_f2i(self.obj.yaxis._scale.base)}}}" # type: ignore[attr-defined] # noqa: SLF001
)
+ if self.is_3d and self.obj.get_zscale() == "log": # type: ignore[attr-defined]
+ self.data.current_axis_options.add("zmode=log")
+ self.data.current_axis_options.add(
+ f"log basis z={{{_try_f2i(self.obj.zaxis._scale.base)}}}" # type: ignore[attr-defined] # noqa: SLF001
+ )
def _set_axis_on_top(self) -> None:
# Possible values for get_axisbelow():
@@ -197,6 +239,8 @@ def _set_axis_positions(self) -> None:
def _set_ticks(self) -> None:
self._get_ticks()
self._get_tick_colors()
+ if self.is_3d:
+ return
self._get_tick_direction()
self._set_tick_rotation()
self._set_tick_positions()
@@ -205,6 +249,35 @@ def _set_grid(self) -> None:
# Don't use get_{x,y}gridlines for gridlines; see discussion on
# Coordinate of
# the lines are entirely meaningless, but styles (colors,...) are respected.
+ (
+ has_major_xgrid,
+ has_minor_xgrid,
+ has_major_ygrid,
+ has_minor_ygrid,
+ has_major_zgrid,
+ has_minor_zgrid,
+ ) = self._get_grid_visibility()
+
+ self._set_grid_options("x", (has_major_xgrid, has_minor_xgrid), self.obj.get_xgridlines())
+ self._set_grid_options("y", (has_major_ygrid, has_minor_ygrid), self.obj.get_ygridlines())
+ if self.is_3d:
+ self._set_grid_options(
+ "z",
+ (has_major_zgrid, has_minor_zgrid),
+ self.obj.get_zgridlines(), # type: ignore[attr-defined]
+ )
+
+ def _get_grid_visibility(self) -> tuple[bool, bool, bool, bool, bool, bool]:
+ if self.is_3d:
+ has_major_grid = bool(self.obj._draw_grid) # type: ignore[attr-defined] # noqa: SLF001
+ return (
+ has_major_grid,
+ False,
+ has_major_grid,
+ False,
+ has_major_grid,
+ False,
+ )
try:
# mpl 3.3.3+
@@ -219,29 +292,29 @@ def _set_grid(self) -> None:
has_major_ygrid = self.obj.yaxis._gridOnMajor # type: ignore[attr-defined] # noqa: SLF001
has_minor_ygrid = self.obj.yaxis._gridOnMinor # type: ignore[attr-defined] # noqa: SLF001
- if has_major_xgrid:
- self.data.current_axis_options.add("xmajorgrids")
- if has_minor_xgrid:
- self.data.current_axis_options.add("xminorgrids")
-
- xlines = self.obj.get_xgridlines()
- if xlines:
- xgridcolor = xlines[0].get_color()
- col, _ = _color.mpl_color2xcolor(self.data, xgridcolor)
- if col != "black":
- self.data.current_axis_options.add(f"x grid style={{{col}}}")
-
- if has_major_ygrid:
- self.data.current_axis_options.add("ymajorgrids")
- if has_minor_ygrid:
- self.data.current_axis_options.add("yminorgrids")
-
- ylines = self.obj.get_ygridlines()
- if ylines:
- ygridcolor = ylines[0].get_color()
- col, _ = _color.mpl_color2xcolor(self.data, ygridcolor)
- if col != "black":
- self.data.current_axis_options.add(f"y grid style={{{col}}}")
+ return (
+ has_major_xgrid,
+ has_minor_xgrid,
+ has_major_ygrid,
+ has_minor_ygrid,
+ False,
+ False,
+ )
+
+ def _set_grid_options(
+ self, axis_name: str, grid_state: tuple[bool, bool], gridlines: Sequence[Line2D]
+ ) -> None:
+ has_major_grid, has_minor_grid = grid_state
+ if has_major_grid:
+ self.data.current_axis_options.add(f"{axis_name}majorgrids")
+ if has_minor_grid:
+ self.data.current_axis_options.add(f"{axis_name}minorgrids")
+ if not gridlines:
+ return
+ gridcolor = gridlines[0].get_color()
+ col, _ = _color.mpl_color2xcolor(self.data, gridcolor)
+ if col != "black":
+ self.data.current_axis_options.add(f"{axis_name} grid style={{{col}}}")
def _set_axis_line_styles(self) -> None:
# Assume that the bottom edge color is the color of the entire box.
@@ -362,47 +435,40 @@ def get_end_code(self) -> str:
return ""
def _get_ticks(self) -> None:
- self.data.current_axis_options.update(
- _get_ticks(self.data, "x", self.obj.get_xticks(), self.obj.get_xticklabels())
- )
- self.data.current_axis_options.update(
- _get_ticks(self.data, "y", self.obj.get_yticks(), self.obj.get_yticklabels())
- )
- self.data.current_axis_options.update(
- _get_ticks(
- self.data,
- "minor x",
- self.obj.get_xticks(minor=True),
- self.obj.get_xticklabels(minor=True),
- )
- )
- self.data.current_axis_options.update(
- _get_ticks(
- self.data,
- "minor y",
- self.obj.get_yticks(minor=True),
- self.obj.get_yticklabels(minor=True),
- )
- )
+ tick_iterators = [
+ ("x", self.obj.get_xticks(), self.obj.get_xticklabels()),
+ ("y", self.obj.get_yticks(), self.obj.get_yticklabels()),
+ ("minor x", self.obj.get_xticks(minor=True), self.obj.get_xticklabels(minor=True)),
+ ("minor y", self.obj.get_yticks(minor=True), self.obj.get_yticklabels(minor=True)),
+ ]
+
+ if self.is_3d:
+ axes3d = cast("Axes3D", self.obj)
+ tick_iterators += [
+ ("z", axes3d.get_zticks(), axes3d.get_zticklabels()),
+ ("minor z", axes3d.get_zticks(minor=True), axes3d.get_zticklabels(minor=True)),
+ ]
+
+ for axis_name, ticks, ticklabels in tick_iterators:
+ self._add_tick_options(axis_name, ticks, ticklabels)
+
+ def _add_tick_options(
+ self, axis_name: str, ticks: Sequence[float] | np.ndarray, ticklabels: Sequence[Text]
+ ) -> None:
+ self.data.current_axis_options.update(_get_ticks(self.data, axis_name, ticks, ticklabels))
def _get_tick_colors(self) -> None:
- try:
- l0 = self.obj.get_xticklines()[0]
- except IndexError:
- pass
- else:
- c0 = l0.get_color()
- xtickcolor, _ = _color.mpl_color2xcolor(self.data, c0)
- self.data.current_axis_options.add(f"xtick style={{color={xtickcolor}}}")
+ self._add_tick_color_option("x", self.obj.get_xticklines())
+ self._add_tick_color_option("y", self.obj.get_yticklines())
+ if self.is_3d:
+ self._add_tick_color_option("z", cast("Axes3D", self.obj).get_zticklines())
- try:
- l0 = self.obj.get_yticklines()[0]
- except IndexError:
- pass
- else:
- c0 = l0.get_color()
- ytickcolor, _ = _color.mpl_color2xcolor(self.data, c0)
- self.data.current_axis_options.add(f"ytick style={{color={ytickcolor}}}")
+ def _add_tick_color_option(self, axis_name: str, ticklines: Sequence[Line2D]) -> None:
+ if not ticklines:
+ return
+ first_tickline = ticklines[0]
+ tickcolor, _ = _color.mpl_color2xcolor(self.data, first_tickline.get_color())
+ self.data.current_axis_options.add(f"{axis_name}tick style={{color={tickcolor}}}")
def _get_tick_direction(self) -> None:
# For new matplotlib versions, we could replace the direction getter by
@@ -564,7 +630,12 @@ def _get_tick_position(obj: Axes, x_or_y: str) -> tuple[str | None, str | None]:
return position_string, major_ticks_position
-def _get_ticks(data: TikzData, xy: str, ticks: list | np.ndarray, ticklabels: list) -> list[str]:
+def _get_ticks(
+ data: TikzData,
+ xy: str,
+ ticks: Sequence[float] | np.ndarray,
+ ticklabels: Sequence[Text],
+) -> list[str]:
"""Gets a {'x','y'}, a number of ticks and ticks labels.
Returns the necessary axis options for the given configuration.
@@ -603,7 +674,7 @@ def _get_ticks(data: TikzData, xy: str, ticks: list | np.ndarray, ticklabels: li
return axis_options
-def _is_label_required(ticks: list | np.ndarray, ticklabels: list) -> bool:
+def _is_label_required(ticks: Sequence[float] | np.ndarray, ticklabels: Sequence[Text]) -> bool:
"""Check if the label is necessary.
If one of the labels is, then all of them must appear in the TikZ plot.
@@ -631,7 +702,7 @@ def _is_label_required(ticks: list | np.ndarray, ticklabels: list) -> bool:
return False
-def _get_pgfplots_ticklabels(ticklabels: list) -> list[str]:
+def _get_pgfplots_ticklabels(ticklabels: Sequence[Text]) -> list[str]:
pgfplots_ticklabels = []
for ticklabel in ticklabels:
label = ticklabel.get_text()
diff --git a/src/matplot2tikz/_cleanfigure.py b/src/matplot2tikz/_cleanfigure.py
index 4f578ebd..c3f8291f 100644
--- a/src/matplot2tikz/_cleanfigure.py
+++ b/src/matplot2tikz/_cleanfigure.py
@@ -251,7 +251,7 @@ def _clean_collections(
cfd.has_lines = False
data = _simplify_line(cfd)
data = _limit_precision(cfd.axes, data, cfd.scale_precision)
- collection.set_offsets(data)
+ _update_collection_data(collection, data)
def _is_step(linehandle: Line2D | art3d.Line3D) -> bool:
@@ -343,6 +343,17 @@ def _update_line_data(linehandle: Line2D | art3d.Line3D, data: np.ndarray) -> No
linehandle.set_ydata(y_data)
+def _update_collection_data(
+ collection: PathCollection | art3d.Path3DCollection, data: np.ndarray
+) -> None:
+ if isinstance(collection, art3d.Path3DCollection):
+ x_data, y_data, z_data = _split_data_3d(data)
+ collection._offsets3d = (x_data, y_data, z_data) # noqa: SLF001
+ collection.set_offsets(_stack_data_2d(x_data, y_data))
+ else:
+ collection.set_offsets(data)
+
+
def _split_data_2d(data: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Convert data to 2 different arrays."""
x_data, y_data = np.split(data, 2, axis=1)
diff --git a/src/matplot2tikz/_clip3d.py b/src/matplot2tikz/_clip3d.py
new file mode 100644
index 00000000..00796105
--- /dev/null
+++ b/src/matplot2tikz/_clip3d.py
@@ -0,0 +1,247 @@
+"""Clip exported 3D artists to Matplotlib axis limits."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from itertools import pairwise
+from typing import TYPE_CHECKING, Literal
+
+import numpy as np
+
+if TYPE_CHECKING:
+ from matplotlib.axes import Axes
+ from matplotlib.transforms import Transform
+
+Clip3DMode = Literal["none", "hide", "clip"]
+POLYGON_RANK = 2
+
+
+@dataclass
+class ClipBox3D:
+ xlim: tuple[float, float]
+ ylim: tuple[float, float]
+ zlim: tuple[float, float]
+ xtransform: Transform
+ ytransform: Transform
+ ztransform: Transform
+
+
+def clip_box_from_axes(axes: Axes) -> ClipBox3D:
+ """Return the 3D clipping box in transformed axis coordinates."""
+ xlim = _transformed_limits(axes.xaxis.get_transform(), axes.get_xlim())
+ ylim = _transformed_limits(axes.yaxis.get_transform(), axes.get_ylim())
+ zlim = _transformed_limits(axes.zaxis.get_transform(), axes.get_zlim()) # type: ignore[attr-defined]
+ return ClipBox3D(
+ xlim=xlim,
+ ylim=ylim,
+ zlim=zlim,
+ xtransform=axes.xaxis.get_transform(),
+ ytransform=axes.yaxis.get_transform(),
+ ztransform=axes.zaxis.get_transform(), # type: ignore[attr-defined]
+ )
+
+
+def points_inside(points: np.ndarray, box: ClipBox3D) -> np.ndarray:
+ """Return a mask for points inside the 3D clipping box."""
+ transformed = transform_points(points, box)
+ return _points_inside_transformed(transformed, box)
+
+
+def clip_line_to_box(points: np.ndarray, box: ClipBox3D, mode: Clip3DMode) -> list[np.ndarray]:
+ """Return line segments hidden or clipped to the 3D clipping box.
+
+ Line clipping uses a Liang-Barsky style parametric interval against the six
+ axis-aligned box planes after applying the Matplotlib axis transforms.
+ """
+ transformed = transform_points(points, box)
+ segments = _line_segments(transformed, box, mode)
+ return [inverse_transform_points(segment, box) for segment in segments]
+
+
+def clip_polygon_to_box(poly: np.ndarray, box: ClipBox3D, mode: Clip3DMode) -> np.ndarray:
+ """Return a polygon hidden or clipped to the 3D clipping box.
+
+ Polygon clipping uses Sutherland-Hodgman clipping against each box plane
+ after applying the Matplotlib axis transforms.
+ """
+ transformed = transform_points(poly, box)
+ if mode == "hide":
+ if np.all(_points_inside_transformed(transformed, box)):
+ return poly
+ return np.empty((0, 3), dtype=float)
+ if mode == "clip":
+ clipped = _clip_polygon_transformed(transformed, box)
+ return inverse_transform_points(clipped, box) if len(clipped) else clipped
+ return poly
+
+
+def transform_points(points: np.ndarray, box: ClipBox3D) -> np.ndarray:
+ """Transform data coordinates into axis-scale coordinates for clipping."""
+ points = np.asarray(points, dtype=float)
+ return np.column_stack(
+ [
+ box.xtransform.transform(points[:, 0]),
+ box.ytransform.transform(points[:, 1]),
+ box.ztransform.transform(points[:, 2]),
+ ]
+ )
+
+
+def inverse_transform_points(points: np.ndarray, box: ClipBox3D) -> np.ndarray:
+ """Transform clipped axis-scale coordinates back into data coordinates."""
+ points = np.asarray(points, dtype=float)
+ return np.column_stack(
+ [
+ box.xtransform.inverted().transform(points[:, 0]),
+ box.ytransform.inverted().transform(points[:, 1]),
+ box.ztransform.inverted().transform(points[:, 2]),
+ ]
+ )
+
+
+def _transformed_limits(transform: Transform, limits: tuple[float, float]) -> tuple[float, float]:
+ transformed = np.asarray(transform.transform(limits), dtype=float)
+ finite = transformed[np.isfinite(transformed)]
+ if len(finite) != 2: # noqa: PLR2004
+ return (np.nan, np.nan)
+ return (float(np.min(finite)), float(np.max(finite)))
+
+
+def _points_inside_transformed(points: np.ndarray, box: ClipBox3D) -> np.ndarray:
+ return np.logical_and.reduce(
+ (
+ np.isfinite(points).all(axis=1),
+ points[:, 0] >= box.xlim[0],
+ points[:, 0] <= box.xlim[1],
+ points[:, 1] >= box.ylim[0],
+ points[:, 1] <= box.ylim[1],
+ points[:, 2] >= box.zlim[0],
+ points[:, 2] <= box.zlim[1],
+ )
+ )
+
+
+def _line_segments(points: np.ndarray, box: ClipBox3D, mode: Clip3DMode) -> list[np.ndarray]:
+ if len(points) < 2: # noqa: PLR2004
+ return []
+
+ inside = _points_inside_transformed(points, box)
+ segments: list[np.ndarray] = []
+ previous_edge_visible = False
+ for i, (p0, p1) in enumerate(pairwise(points)):
+ clipped = np.empty((0, 3), dtype=float)
+ if mode == "hide":
+ if inside[i] and inside[i + 1]:
+ clipped = np.asarray([p0, p1], dtype=float)
+ elif mode == "clip":
+ clipped = _clip_line_segment_transformed(p0, p1, box)
+ if len(clipped):
+ _append_line_segment(segments, clipped, merge=previous_edge_visible)
+ previous_edge_visible = True
+ else:
+ previous_edge_visible = False
+ return segments
+
+
+def _append_line_segment(segments: list[np.ndarray], segment: np.ndarray, *, merge: bool) -> None:
+ if len(segment) < 2: # noqa: PLR2004
+ return
+ if merge and segments and np.allclose(segments[-1][-1], segment[0]):
+ segments[-1] = np.vstack([segments[-1], segment[1:]])
+ return
+ segments.append(segment)
+
+
+def _clip_line_segment_transformed(p0: np.ndarray, p1: np.ndarray, box: ClipBox3D) -> np.ndarray:
+ if not np.isfinite(p0).all() or not np.isfinite(p1).all():
+ return np.empty((0, 3), dtype=float)
+
+ direction = p1 - p0
+ t0 = 0.0
+ t1 = 1.0
+ for axis, (lower, upper) in enumerate((box.xlim, box.ylim, box.zlim)):
+ if not np.isfinite([lower, upper]).all():
+ return np.empty((0, 3), dtype=float)
+ if direction[axis] == 0:
+ if p0[axis] < lower or p0[axis] > upper:
+ return np.empty((0, 3), dtype=float)
+ continue
+
+ t_lower = (lower - p0[axis]) / direction[axis]
+ t_upper = (upper - p0[axis]) / direction[axis]
+ t_enter = min(t_lower, t_upper)
+ t_exit = max(t_lower, t_upper)
+ t0 = max(t0, t_enter)
+ t1 = min(t1, t_exit)
+ if t0 > t1:
+ return np.empty((0, 3), dtype=float)
+
+ start = p0 + t0 * direction
+ end = p0 + t1 * direction
+ if np.allclose(start, end):
+ return np.empty((0, 3), dtype=float)
+ return np.asarray([start, end], dtype=float)
+
+
+def _clip_polygon_transformed(poly: np.ndarray, box: ClipBox3D) -> np.ndarray:
+ if _is_degenerate_polygon(poly):
+ return np.empty((0, 3), dtype=float)
+
+ for axis, value, keep_greater in (
+ (0, box.xlim[0], True),
+ (0, box.xlim[1], False),
+ (1, box.ylim[0], True),
+ (1, box.ylim[1], False),
+ (2, box.zlim[0], True),
+ (2, box.zlim[1], False),
+ ):
+ if not np.isfinite(value):
+ return np.empty((0, 3), dtype=float)
+ poly = _clip_polygon_against_plane(poly, axis, value, keep_greater=keep_greater)
+ if len(poly) < 3: # noqa: PLR2004
+ return np.empty((0, 3), dtype=float)
+
+ if _is_degenerate_polygon(poly):
+ return np.empty((0, 3), dtype=float)
+ return poly
+
+
+def _clip_polygon_against_plane(
+ poly: np.ndarray, axis: int, value: float, *, keep_greater: bool
+) -> np.ndarray:
+ clipped = []
+ previous = poly[-1]
+ previous_inside = _inside_plane(previous, axis, value, keep_greater=keep_greater)
+
+ for current in poly:
+ current_inside = _inside_plane(current, axis, value, keep_greater=keep_greater)
+ if current_inside != previous_inside:
+ denominator = current[axis] - previous[axis]
+ if not np.isclose(denominator, 0.0):
+ t = np.clip((value - previous[axis]) / denominator, 0.0, 1.0)
+ clipped.append(previous + t * (current - previous))
+ if current_inside:
+ clipped.append(current)
+
+ previous = current
+ previous_inside = current_inside
+
+ return np.asarray(clipped, dtype=float) if clipped else np.empty((0, 3), dtype=float)
+
+
+def _inside_plane(point: np.ndarray, axis: int, value: float, *, keep_greater: bool) -> bool:
+ return bool(point[axis] >= value if keep_greater else point[axis] <= value)
+
+
+def _is_degenerate_polygon(poly: np.ndarray, tol: float = 1e-12) -> bool:
+ poly = np.asarray(poly, dtype=float)
+ if poly.ndim != 2 or poly.shape[1] != 3 or len(poly) < 3: # noqa: PLR2004
+ return True
+ if not np.isfinite(poly).all():
+ return True
+
+ centered = poly - poly.mean(axis=0)
+ scale = np.linalg.norm(centered, axis=1).max(initial=0)
+ if scale == 0:
+ return True
+ return bool(np.linalg.matrix_rank(centered, tol=tol * scale) < POLYGON_RANK)
diff --git a/src/matplot2tikz/_line2d.py b/src/matplot2tikz/_line2d.py
index 7f675451..39624279 100644
--- a/src/matplot2tikz/_line2d.py
+++ b/src/matplot2tikz/_line2d.py
@@ -316,7 +316,7 @@ def _table(data: TikzData, obj: Line2D) -> list[str]:
opts_str = ("[" + ",".join(opts) + "] ") if len(opts) > 0 else ""
posix_filepath = rel_filepath.as_posix()
- content.append(f"table {{{opts_str}}}{{{posix_filepath}}};\n")
+ content.append(f"table {opts_str}{{{posix_filepath}}};\n")
else:
if len(opts) > 0:
opts_str = ",".join(opts)
diff --git a/src/matplot2tikz/_mplot3d.py b/src/matplot2tikz/_mplot3d.py
new file mode 100644
index 00000000..d67ffbf6
--- /dev/null
+++ b/src/matplot2tikz/_mplot3d.py
@@ -0,0 +1,1035 @@
+from __future__ import annotations
+
+from collections.abc import Iterable, Sequence
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, TypeVar, cast
+
+import numpy as np
+from matplotlib.path import Path
+from mpl_toolkits.mplot3d.art3d import (
+ Line3D,
+ Line3DCollection,
+ Path3DCollection,
+ Poly3DCollection,
+ Text3D,
+)
+
+from . import _files, _line2d
+from . import _path as mypath
+from ._axes import _mpl_cmap2pgf_cmap
+from ._clip3d import (
+ ClipBox3D,
+ clip_box_from_axes,
+ clip_line_to_box,
+ clip_polygon_to_box,
+ points_inside,
+)
+from ._util import get_legend_text, has_legend
+
+if TYPE_CHECKING:
+ from matplotlib.collections import Collection, LineCollection, PathCollection
+ from matplotlib.lines import Line2D
+ from matplotlib.text import Text
+
+ from ._tikzdata import TikzData
+
+MIN_POLYGON_VERTICES = 3
+RGBA_LENGTH = 4
+HashableColor = tuple[float, ...]
+LineStyle = str | tuple[float, Sequence[float] | None] | None
+T = TypeVar("T")
+
+
+@dataclass
+class Poly3DStyle:
+ facecolor: np.ndarray | None
+ edgecolor: np.ndarray | None
+ linewidth: float | None
+ linestyle: LineStyle
+
+
+@dataclass
+class Poly3DStyleArrays:
+ facecolors: np.ndarray | None
+ edgecolors: np.ndarray
+ linewidths: Sequence[float | None] | np.ndarray
+ linestyles: Sequence[LineStyle]
+
+
+@dataclass
+class Poly3DColorGroup:
+ segments: list[np.ndarray]
+ colors: list[np.ndarray | None]
+ style_options: list[str]
+ vertex_count: int
+
+
+@dataclass
+class Quiver3DData:
+ coordinates: np.ndarray
+ options: list[str]
+
+
+def is_quiver3d_collection(obj: Collection) -> bool:
+ # Since a quiver3d collection is represented as a Line3DCollection with a
+ # specific structure of segments and uniform style, both must be checked.
+ if not isinstance(obj, Line3DCollection):
+ return False
+ style_arrays = _poly3d_style_arrays(obj)
+ if not _quiver3d_has_uniform_style(style_arrays):
+ return False
+ return _quiver3d_coordinates(get_segments3d(obj)) is not None
+
+
+def is_contour3d_collection(obj: Collection) -> bool:
+ return hasattr(obj, "_3dverts_codes")
+
+
+def get_text3d_position(obj: Text) -> tuple[float, float, float]:
+ if not isinstance(obj, Text3D):
+ msg = f"Expected Text3D, got {type(obj)}."
+ raise TypeError(msg)
+ return obj.get_position_3d()
+
+
+def get_line3d_data(obj: Line2D) -> np.ndarray:
+ if not isinstance(obj, Line3D):
+ msg = f"Expected Line3D, got {type(obj)}."
+ raise TypeError(msg)
+ xdata, ydata, zdata = obj.get_data_3d()
+ return np.column_stack([xdata, ydata, zdata])
+
+
+def get_offsets3d(obj: PathCollection) -> np.ndarray:
+ if not isinstance(obj, Path3DCollection):
+ msg = f"Expected Path3DCollection, got {type(obj)}."
+ raise TypeError(msg)
+ offsets = obj._offsets3d # noqa: SLF001
+ xdata, ydata, zdata = (np.ma.getdata(offset) for offset in offsets)
+ return np.column_stack([xdata, ydata, zdata])
+
+
+def get_segments3d(obj: LineCollection) -> list[np.ndarray]:
+ if not isinstance(obj, Line3DCollection):
+ msg = f"Expected Line3DCollection, got {type(obj)}."
+ raise TypeError(msg)
+ segments = obj._segments3d # noqa: SLF001
+ return [np.asarray(segment, dtype=float) for segment in segments]
+
+
+def get_poly3d_segments(obj: Collection) -> list[np.ndarray]:
+ if not isinstance(obj, Poly3DCollection):
+ msg = f"Expected Poly3DCollection, got {type(obj)}."
+ raise TypeError(msg)
+
+ # Matplotlib <= 3.10 stores Poly3DCollection vertices in _vec/_segslices.
+ if hasattr(obj, "_vec") and hasattr(obj, "_segslices"):
+ vec = obj._vec # noqa: SLF001
+ segslices = obj._segslices # noqa: SLF001
+ return [np.asarray(vec[:3, segslice].T, dtype=float) for segslice in segslices]
+
+ # Matplotlib 3.11.0rc1 switched to padded _faces plus _invalid_vertices.
+ faces = getattr(obj, "_faces", None)
+ if faces is None:
+ msg = "Poly3DCollection has neither _vec/_segslices nor _faces."
+ raise AttributeError(msg)
+
+ faces = np.asarray(faces, dtype=float)
+ if faces.ndim != 3 or faces.shape[-1] != 3: # noqa: PLR2004
+ msg = f"Expected Poly3DCollection._faces with shape (n, m, 3), got {faces.shape}."
+ raise ValueError(msg)
+
+ invalid_vertices = np.asarray(getattr(obj, "_invalid_vertices", False), dtype=bool)
+ if invalid_vertices.ndim == 0:
+ if bool(invalid_vertices):
+ return []
+ return [np.asarray(face, dtype=float) for face in faces]
+
+ return [
+ np.asarray(face[~invalid_mask], dtype=float)
+ for face, invalid_mask in zip(faces, invalid_vertices, strict=False)
+ ]
+
+
+def get_poly3d_facecolors(obj: Collection) -> np.ndarray:
+ facecolors = obj._facecolor3d # type: ignore[attr-defined] # noqa: SLF001
+ return np.asarray(facecolors)
+
+
+def get_poly3d_edgecolors(obj: Collection) -> np.ndarray:
+ edgecolors = obj._edgecolor3d # type: ignore[attr-defined] # noqa: SLF001
+ return np.asarray(edgecolors)
+
+
+def get_contour3d_verts_and_codes(obj: Collection) -> list[tuple[np.ndarray, np.ndarray | None]]:
+ verts_codes = obj._3dverts_codes # type: ignore[attr-defined] # noqa: SLF001
+ return [
+ (
+ np.asarray(vertices, dtype=float),
+ None if codes is None else np.asarray(codes),
+ )
+ for vertices, codes in verts_codes
+ ]
+
+
+def draw_line3d(data: TikzData, obj: Line2D) -> list[str]:
+ """Return PGFPlots code for a 3D line."""
+ coordinates = get_line3d_data(obj)
+ segments = _line_segments_for_export(data, coordinates)
+ if not segments:
+ return []
+
+ addplot_options = _line2d._get_line2d_options(data, obj) # noqa: SLF001
+ legend_text = get_legend_text(obj)
+ if legend_text is None and obj.axes is not None and has_legend(obj.axes):
+ addplot_options.append("forget plot")
+
+ content = []
+ for segment in segments:
+ content.append("\\addplot3 ")
+ if addplot_options:
+ content.append("[{}]\n".format(", ".join(addplot_options)))
+ content.extend(table(data, segment))
+
+ if legend_text is not None:
+ content.append(f"\\addlegendentry{{{legend_text}}}\n")
+
+ return content
+
+
+def draw_quiver3d(data: TikzData, obj: Line3DCollection) -> list[str]:
+ """Return PGFPlots code for a 3D quiver plot."""
+ if data.clip_3d == "clip":
+ return draw_line3dcollection(data, obj)
+
+ style_arrays = _poly3d_style_arrays(obj)
+ segments = get_segments3d(obj)
+ quiver_data = _quiver3d_data(data, obj, segments, style_arrays)
+ if quiver_data is not None:
+ return addplot_table(
+ data,
+ quiver_data.coordinates,
+ command="addplot3",
+ options=quiver_data.options,
+ table_options=["x=x", "y=y", "z=z"],
+ column_names=["x", "y", "z", "u", "v", "w"],
+ )
+ return []
+
+
+def draw_line3dcollection(data: TikzData, obj: Line3DCollection) -> list[str]:
+ """Return PGFPlots code for 3D line collections such as wireframes and quivers."""
+ style_arrays = _poly3d_style_arrays(obj)
+ segments = get_segments3d(obj)
+
+ content = []
+ for i, segment in enumerate(segments):
+ color = _cycle_array(style_arrays.edgecolors, i)
+ style = _cycle_array(style_arrays.linestyles, i)
+ if isinstance(style, tuple):
+ style = (float(style[0]), style[1])
+ linewidth = _cycle_array(style_arrays.linewidths, i)
+ width = None if linewidth is None else float(linewidth)
+
+ options = mypath.get_draw_options(
+ data, mypath.LineData(obj=obj, ec=color, ls=style, lw=width)
+ )
+ for export_segment in _line_segments_for_export(data, segment):
+ content.extend(
+ addplot_table(
+ data,
+ export_segment,
+ command="addplot3",
+ options=options,
+ externalize_min_rows=3,
+ )
+ )
+
+ return content
+
+
+def _line_segments_for_export(data: TikzData, points: np.ndarray) -> list[np.ndarray]:
+ if len(points) == 0:
+ return []
+ clip_box = _clip_box(data)
+ if clip_box is None:
+ return [points]
+ return clip_line_to_box(points, clip_box, data.clip_3d)
+
+
+def _clip_box(data: TikzData) -> ClipBox3D | None:
+ if data.clip_3d == "none" or data.current_mpl_axes is None:
+ return None
+ return clip_box_from_axes(data.current_mpl_axes)
+
+
+def _quiver3d_data(
+ data: TikzData,
+ obj: LineCollection,
+ segments: list[np.ndarray],
+ style_arrays: Poly3DStyleArrays,
+) -> Quiver3DData | None:
+ quiver_coordinates = _quiver3d_coordinates(segments)
+ if quiver_coordinates is None:
+ return None
+ quiver_coordinates = _clip_quiver_coordinates(data, quiver_coordinates)
+ if len(quiver_coordinates) == 0:
+ return None
+
+ style = _cycle_array(style_arrays.linestyles, 0)
+ if isinstance(style, tuple):
+ style = (float(style[0]), style[1])
+ linewidth = _cycle_array(style_arrays.linewidths, 0)
+ width = None if linewidth is None else float(linewidth)
+ options = mypath.get_draw_options(
+ data,
+ mypath.LineData(obj=obj, ec=_cycle_array(style_arrays.edgecolors, 0), ls=style, lw=width),
+ )
+ options.extend(
+ [
+ "-latex",
+ "mark=none",
+ r"quiver={u=\thisrow{u}, v=\thisrow{v}, w=\thisrow{w}}",
+ ]
+ )
+ return Quiver3DData(quiver_coordinates, options)
+
+
+def _quiver3d_coordinates(segments: list[np.ndarray]) -> np.ndarray | None:
+ if not segments or len(segments) % 3 != 0:
+ return None
+
+ arrow_count = len(segments) // 3
+ shafts = segments[:arrow_count]
+ first_heads = segments[arrow_count : 2 * arrow_count]
+ second_heads = segments[2 * arrow_count :]
+ rows = []
+ for shaft, first_head, second_head in zip(shafts, first_heads, second_heads, strict=True):
+ if shaft.shape != (2, 3) or first_head.shape != (2, 3) or second_head.shape != (2, 3):
+ return None
+ tip = shaft[0]
+ tail = shaft[1]
+ if not (np.allclose(first_head[0], tip) and np.allclose(second_head[0], tip)):
+ return None
+ vector = tip - tail
+ if np.allclose(vector, 0.0):
+ return None
+ rows.append([*tail, *vector])
+
+ return np.asarray(rows, dtype=float)
+
+
+def _quiver3d_has_uniform_style(style_arrays: Poly3DStyleArrays) -> bool:
+ return (
+ _array_len(style_arrays.edgecolors) <= 1
+ and _array_len(style_arrays.linewidths) <= 1
+ and len(style_arrays.linestyles) <= 1
+ )
+
+
+def _array_len(values: Sequence[object] | np.ndarray | None) -> int:
+ return 0 if values is None else len(values)
+
+
+def _clip_quiver_coordinates(data: TikzData, coordinates: np.ndarray) -> np.ndarray:
+ clip_box = _clip_box(data)
+ if clip_box is None:
+ return coordinates
+ tails = coordinates[:, :3]
+ tips = coordinates[:, :3] + coordinates[:, 3:6]
+ mask = points_inside(tails, clip_box) & points_inside(tips, clip_box)
+ return coordinates[mask]
+
+
+def draw_path3dcollection(data: TikzData, obj: PathCollection) -> list[str]:
+ """Return PGFPlots code for 3D scatter/path collections."""
+ offsets = get_offsets3d(obj)
+ mask = _scatter_mask_for_export(data, offsets)
+ if mask is None:
+ return _draw_path3dcollection(data, obj, offsets)
+ if not np.any(mask):
+ return []
+ return _draw_clipped_path3dcollection(data, obj, offsets, mask)
+
+
+def _draw_path3dcollection(data: TikzData, obj: PathCollection, offsets: np.ndarray) -> list[str]:
+ pcd = mypath.make_pathcollection_data(
+ data,
+ obj,
+ mypath.PathCollectionCoordinates(
+ offsets=offsets,
+ labels=["x", "y", "z"],
+ table_options=["x=x", "y=y", "z=z"],
+ is_contour=False,
+ command="\\addplot3",
+ ),
+ )
+ return mypath.draw_pathcollection_data(data, pcd)
+
+
+def _scatter_mask_for_export(data: TikzData, offsets: np.ndarray) -> np.ndarray | None:
+ clip_box = _clip_box(data)
+ if clip_box is None:
+ return None
+ return points_inside(offsets, clip_box)
+
+
+def _draw_clipped_path3dcollection(
+ data: TikzData, obj: PathCollection, offsets: np.ndarray, mask: np.ndarray
+) -> list[str]:
+ obj3d = cast("Path3DCollection", obj)
+ old_offsets = obj3d._offsets3d # noqa: SLF001
+ old_array = obj.get_array()
+ old_sizes = obj.get_sizes()
+ old_edgecolors = obj.get_edgecolors() # type: ignore[attr-defined]
+ old_facecolors = obj.get_facecolors() # type: ignore[attr-defined]
+ try:
+ obj3d._offsets3d = tuple(offsets[mask].T) # noqa: SLF001
+ if old_array is not None and len(old_array) == len(mask):
+ obj.set_array(np.asarray(old_array)[mask])
+ if len(old_sizes) == len(mask):
+ obj.set_sizes(old_sizes[mask])
+ if len(old_edgecolors) == len(mask):
+ obj.set_edgecolors(old_edgecolors[mask]) # type: ignore[attr-defined]
+ if len(old_facecolors) == len(mask):
+ obj.set_facecolors(old_facecolors[mask]) # type: ignore[attr-defined]
+ return _draw_path3dcollection(data, obj, offsets[mask])
+ finally:
+ obj3d._offsets3d = old_offsets # noqa: SLF001
+ obj.set_array(old_array)
+ obj.set_sizes(old_sizes)
+ obj.set_edgecolors(old_edgecolors) # type: ignore[attr-defined]
+ obj.set_facecolors(old_facecolors) # type: ignore[attr-defined]
+
+
+def draw_poly3dcollection(data: TikzData, obj: Collection) -> list[str]:
+ """Returns PGFPlots patch-plot code for 3D polygon collections."""
+ indexed_segments = _poly_segments_for_export(data, get_poly3d_segments(obj))
+ if not indexed_segments:
+ return []
+
+ data.pgfplots_libs.add("patchplots")
+ array = obj.get_array()
+ if array is not None:
+ color_data = np.ma.getdata(array)
+ if len(color_data) > max(index for index, _ in indexed_segments):
+ segment_colors = [
+ (vertices, float(color_data[index])) for index, vertices in indexed_segments
+ ]
+ return _draw_poly3dcollection_colormapped(data, obj, segment_colors)
+
+ return _draw_poly3dcollection_explicit_colors(data, obj, indexed_segments)
+
+
+def _poly_segments_for_export(
+ data: TikzData, segments: list[np.ndarray]
+) -> list[tuple[int, np.ndarray]]:
+ clip_box = _clip_box(data)
+ if clip_box is None:
+ return [
+ (index, vertices)
+ for index, vertices in enumerate(segments)
+ if len(vertices) >= MIN_POLYGON_VERTICES
+ ]
+
+ indexed_segments: list[tuple[int, np.ndarray]] = []
+ for index, vertices in enumerate(segments):
+ if len(vertices) < MIN_POLYGON_VERTICES:
+ continue
+ if data.clip_3d == "clip" and np.all(points_inside(vertices, clip_box)):
+ indexed_segments.append((index, vertices))
+ continue
+
+ clipped = clip_polygon_to_box(vertices, clip_box, data.clip_3d)
+ if len(clipped) >= MIN_POLYGON_VERTICES:
+ indexed_segments.append((index, clipped))
+ return indexed_segments
+
+
+def draw_contour3d(data: TikzData, obj: Collection) -> list[str]:
+ """Returns PGFPlots code for 3D contour collections."""
+ facecolors = np.asarray(obj.get_facecolor())
+ if len(facecolors):
+ return _draw_contour3d_filled(data, obj, facecolors)
+ return _draw_contour3d_lines(data, obj)
+
+
+def _draw_contour3d_lines(data: TikzData, obj: Collection) -> list[str]:
+ content: list[str] = []
+ style_arrays = _poly3d_style_arrays(obj)
+ for i, (vertices, codes) in enumerate(get_contour3d_verts_and_codes(obj)):
+ options = _poly3d_style_options(
+ data, obj, _poly3d_style_at(style_arrays, i, include_facecolor=False)
+ )
+ for segment in _split_contour3d_vertices_for_export(data, vertices, codes):
+ content.extend(addplot_table(data, segment, command="addplot3", options=options))
+ return content
+
+
+def _draw_contour3d_filled(data: TikzData, obj: Collection, facecolors: np.ndarray) -> list[str]:
+ data.pgfplots_libs.add("patchplots")
+ content: list[str] = []
+ style_arrays = _poly3d_style_arrays(obj, facecolors=facecolors)
+ for i, (vertices, codes) in enumerate(get_contour3d_verts_and_codes(obj)):
+ segments = _contourf_segments_for_export(data, vertices, codes)
+ if not segments:
+ continue
+ style = _poly3d_style_at(style_arrays, i)
+ for vertex_count, grouped_segments in _group_segments_only_by_vertex_count(segments):
+ options = _poly3d_base_options(data, vertex_count)
+ options.extend(_poly3d_style_options(data, obj, style))
+ options.append(_patch_table(data, grouped_segments))
+ content.extend(_poly3d_addplot(data, grouped_segments, options))
+ return content
+
+
+def _split_contour3d_vertices_for_export(
+ data: TikzData, vertices: np.ndarray, codes: np.ndarray | None
+) -> list[np.ndarray]:
+ segments = _split_contour3d_vertices(vertices, codes)
+ clip_box = _clip_box(data)
+ if clip_box is None:
+ return [segment for segment in segments if len(segment) >= 2] # noqa: PLR2004
+
+ export_segments = []
+ for segment in segments:
+ export_segments.extend(clip_line_to_box(segment, clip_box, data.clip_3d))
+ return export_segments
+
+
+def _contourf_segments_for_export(
+ data: TikzData, vertices: np.ndarray, codes: np.ndarray | None
+) -> list[np.ndarray]:
+ segments = _split_contour3d_vertices(vertices, codes)
+ clip_box = _clip_box(data)
+ if clip_box is None:
+ return [segment for segment in segments if len(segment) >= MIN_POLYGON_VERTICES]
+
+ export_segments = []
+ for segment in segments:
+ clipped = clip_polygon_to_box(segment, clip_box, data.clip_3d)
+ if len(clipped) >= MIN_POLYGON_VERTICES:
+ export_segments.append(clipped)
+ return export_segments
+
+
+def _split_contour3d_vertices(vertices: np.ndarray, codes: np.ndarray | None) -> list[np.ndarray]:
+ if len(vertices) == 0:
+ return []
+ if codes is None:
+ return [vertices]
+
+ segments: list[np.ndarray] = []
+ current_segment: list[np.ndarray] = []
+ for vertex, code in zip(vertices, codes, strict=False):
+ if code == Path.MOVETO:
+ if current_segment:
+ segments.append(np.asarray(current_segment, dtype=float))
+ current_segment = [vertex]
+ elif code == Path.CLOSEPOLY:
+ if current_segment:
+ segments.append(np.asarray(current_segment, dtype=float))
+ current_segment = []
+ else:
+ current_segment.append(vertex)
+ if current_segment:
+ segments.append(np.asarray(current_segment, dtype=float))
+ return segments
+
+
+def _draw_poly3dcollection_colormapped(
+ data: TikzData, obj: Collection, segment_colors: list[tuple[np.ndarray, float]]
+) -> list[str]:
+ content: list[str] = []
+ cmap = obj.get_cmap()
+ if cmap is not None:
+ mycolormap, is_custom_cmap = _mpl_cmap2pgf_cmap(cmap, data)
+ colormap_option = "colormap" + ("=" if is_custom_cmap else "/") + mycolormap
+ data.current_axis_options.add(colormap_option)
+
+ # Triangulate if shading is enabled and any polygon has more than 4 vertices, since PGFPlots
+ # only supports shading for triangles and bilinear shading for quads.
+ if data.shader != "none" and any(len(vertices) > 4 for vertices, _ in segment_colors): # noqa: PLR2004
+ segment_colors = [
+ (segment, color_value)
+ for vertices, color_value in segment_colors
+ for segment in _triangulate_polygon(vertices)
+ ]
+
+ for vertex_count, group in _group_segments_by_vertex_count(segment_colors):
+ grouped_segments = [segment for segment, _ in group]
+ grouped_color_data = [color_value for _, color_value in group]
+ options = _poly3d_base_options(data, vertex_count, use_shader=data.shader != "none")
+ options.extend(_poly3d_collection_options(data, obj))
+
+ if data.shader != "none":
+ options.append(r"point meta=\thisrow{meta}")
+ options.append(_patch_table(data, grouped_segments))
+ content.extend(
+ _poly3d_addplot(
+ data,
+ grouped_segments,
+ options,
+ point_meta_segments=_poly3d_z_point_meta(grouped_segments),
+ )
+ )
+ else:
+ options.append(_patch_table(data, grouped_segments, grouped_color_data))
+ content.extend(_poly3d_addplot(data, grouped_segments, options))
+
+ return content
+
+
+def _triangulate_polygon(vertices: np.ndarray) -> list[np.ndarray]:
+ if len(vertices) < MIN_POLYGON_VERTICES:
+ return []
+ if len(vertices) == MIN_POLYGON_VERTICES:
+ return [vertices]
+ return [
+ np.asarray([vertices[0], vertices[i], vertices[i + 1]], dtype=float)
+ for i in range(1, len(vertices) - 1)
+ ]
+
+
+def _draw_poly3dcollection_explicit_colors(
+ data: TikzData, obj: Collection, segments: list[tuple[int, np.ndarray]]
+) -> list[str]:
+ facecolors = get_poly3d_facecolors(obj)
+ edgecolors = get_poly3d_edgecolors(obj)
+ style_arrays = _poly3d_style_arrays(obj, facecolors=facecolors, edgecolors=edgecolors)
+
+ group_key: tuple[int, HashableColor | None, float | None, str, float | None]
+ grouped: dict[
+ tuple[int, HashableColor | None, float | None, str, float | None], Poly3DColorGroup
+ ] = {}
+ for index, vertices in segments:
+ style = _poly3d_style_at(style_arrays, index)
+ fc = style.facecolor
+ ec = style.edgecolor
+ lw = style.linewidth
+ ls = style.linestyle
+ face_alpha = _color_alpha(fc)
+ group_key = (
+ len(vertices),
+ _hashable_color(ec),
+ float(lw) if lw is not None else None,
+ repr(ls),
+ face_alpha,
+ )
+ if group_key not in grouped:
+ grouped[group_key] = Poly3DColorGroup(
+ segments=[],
+ colors=[],
+ style_options=_poly3d_edge_options(data, obj, ec, lw, ls)
+ + _poly3d_fill_opacity_options(data, face_alpha),
+ vertex_count=len(vertices),
+ )
+ grouped[group_key].segments.append(vertices)
+ grouped[group_key].colors.append(_rgb_color(fc))
+
+ content: list[str] = []
+ for group in grouped.values():
+ options = _poly3d_base_options(data, group.vertex_count)
+ options.extend(group.style_options)
+ color_indices = _poly3d_explicit_color_indices(group.colors)
+ if not color_indices.unique_colors:
+ options.append(_patch_table(data, group.segments))
+ elif len(color_indices.unique_colors) > 1:
+ options.extend(_poly3d_colormap_options(data, color_indices.unique_colors))
+ options.append(_patch_table(data, group.segments, color_indices.indices))
+ else:
+ color = color_indices.unique_colors[0]
+ options.extend(
+ _poly3d_style_options(
+ data,
+ obj,
+ Poly3DStyle(color, None, None, None),
+ )
+ )
+ options.append(_patch_table(data, group.segments))
+ content.extend(_poly3d_addplot(data, group.segments, options))
+ return content
+
+
+@dataclass
+class Poly3DColorIndices:
+ unique_colors: list[np.ndarray]
+ indices: list[float]
+
+
+def _poly3d_base_options(
+ data: TikzData, vertex_count: int, *, use_shader: bool = False
+) -> list[str]:
+ options = [
+ "patch",
+ *_poly3d_patch_type_options(use_shader, vertex_count),
+ "table/row sep=\\\\",
+ "z buffer=sort",
+ ]
+ if use_shader:
+ options.append(_shader_option(data.shader))
+ return options
+
+
+def _poly3d_patch_type_options(use_shader: bool, vertex_count: int) -> list[str]: # noqa: FBT001
+ if use_shader and vertex_count == 3: # noqa: PLR2004
+ return ["patch type=triangle"]
+ if use_shader and vertex_count == 4: # noqa: PLR2004
+ return ["patch type=bilinear"]
+ return ["patch type=polygon", f"vertex count={vertex_count}"]
+
+
+def _shader_option(shader: str) -> str:
+ shader = shader.removeprefix(",").strip()
+ return shader if shader.startswith("shader=") else f"shader={shader}"
+
+
+def _poly3d_collection_options(data: TikzData, obj: Collection) -> list[str]:
+ facecolors = get_poly3d_facecolors(obj)
+ edgecolors = get_poly3d_edgecolors(obj)
+ style = _poly3d_style_at(
+ _poly3d_style_arrays(obj, facecolors=facecolors, edgecolors=edgecolors), 0
+ )
+ face_alpha = _color_alpha(style.facecolor)
+ return _poly3d_edge_options(
+ data,
+ obj,
+ style.edgecolor,
+ style.linewidth,
+ style.linestyle,
+ ) + _poly3d_fill_opacity_options(data, face_alpha)
+
+
+def _poly3d_edge_options(
+ data: TikzData,
+ obj: Collection,
+ edgecolor: np.ndarray | None,
+ linewidth: float | None,
+ linestyle: LineStyle,
+) -> list[str]:
+ return _poly3d_style_options(
+ data,
+ obj,
+ Poly3DStyle(
+ facecolor=None,
+ edgecolor=edgecolor,
+ linewidth=linewidth,
+ linestyle=linestyle,
+ ),
+ )
+
+
+def _poly3d_style_arrays(
+ obj: Collection,
+ *,
+ facecolors: np.ndarray | None = None,
+ edgecolors: np.ndarray | None = None,
+) -> Poly3DStyleArrays:
+ return Poly3DStyleArrays(
+ facecolors=facecolors,
+ edgecolors=np.asarray(obj.get_edgecolor()) if edgecolors is None else edgecolors,
+ linewidths=np.atleast_1d(obj.get_linewidth()),
+ linestyles=cast("Sequence[LineStyle]", obj.get_linestyle()),
+ )
+
+
+def _poly3d_style_at(
+ style_arrays: Poly3DStyleArrays,
+ index: int,
+ *,
+ include_facecolor: bool = True,
+) -> Poly3DStyle:
+ return Poly3DStyle(
+ facecolor=_cycle_array(style_arrays.facecolors, index) if include_facecolor else None,
+ edgecolor=_cycle_array(style_arrays.edgecolors, index),
+ linewidth=_cycle_array(style_arrays.linewidths, index),
+ linestyle=_cycle_array(style_arrays.linestyles, index),
+ )
+
+
+def _poly3d_style_options(
+ data: TikzData,
+ obj: Collection,
+ style: Poly3DStyle,
+) -> list[str]:
+ line_data = mypath.LineData(
+ obj=obj,
+ ec=style.edgecolor,
+ fc=style.facecolor,
+ ls=style.linestyle if isinstance(style.linestyle, (str, tuple)) else None,
+ lw=style.linewidth,
+ )
+ draw_options = mypath.get_draw_options(data, line_data)
+ if (
+ (style.edgecolor is None or style.linewidth == 0)
+ and "draw=none" not in draw_options
+ and not any(option.startswith("draw=") for option in draw_options)
+ ):
+ draw_options.append("draw=none")
+ if (
+ style.facecolor is not None
+ and len(style.facecolor) == RGBA_LENGTH
+ and style.facecolor[3] == 0
+ ):
+ draw_options.append("fill opacity=0")
+ return draw_options
+
+
+def _poly3d_explicit_color_indices(colors: list[np.ndarray | None]) -> Poly3DColorIndices:
+ non_null_colors = [color for color in colors if color is not None]
+ if len(non_null_colors) != len(colors):
+ return Poly3DColorIndices([], [])
+
+ unique_colors: list[np.ndarray] = []
+ indices: list[float] = []
+ for color in non_null_colors:
+ color_index = _matching_color_index(unique_colors, color)
+ if color_index is None:
+ color_index = len(unique_colors)
+ unique_colors.append(color)
+ indices.append(float(color_index))
+ return Poly3DColorIndices(unique_colors, indices)
+
+
+def _matching_color_index(colors: list[np.ndarray], color: np.ndarray) -> int | None:
+ for i, unique_color in enumerate(colors):
+ if np.allclose(color, unique_color):
+ return i
+ return None
+
+
+def _poly3d_colormap_options(data: TikzData, colors: list[np.ndarray]) -> list[str]:
+ name = f"matplot2tikzpoly{data.custom_colormap_id}"
+ data.custom_colormap_id += 1
+
+ color_changes: list[str] = []
+ for i, color in enumerate(colors):
+ red, green, blue = color[:3]
+ ff = data.float_format
+ color_changes.append(f"rgb({i}pt)=({red:{ff}},{green:{ff}},{blue:{ff}})")
+
+ return [
+ "colormap={" + name + "}{[1pt]\n " + ";\n ".join(color_changes) + "\n}",
+ "point meta min=0",
+ f"point meta max={len(colors) - 1}",
+ ]
+
+
+def _poly3d_z_point_meta(segments: list[np.ndarray]) -> list[np.ndarray]:
+ return [np.asarray(segment[:, 2], dtype=float) for segment in segments]
+
+
+def _poly3d_addplot(
+ data: TikzData,
+ segments: list[np.ndarray],
+ options: list[str],
+ *,
+ point_meta_segments: list[np.ndarray] | None = None,
+) -> list[str]:
+ column_names = "x y z meta" if point_meta_segments is not None else "x y z"
+ content = [
+ "\\addplot3 [\n",
+ ",\n".join(options),
+ "\n]\n",
+ "table [row sep=\\\\] {%\n",
+ column_names + "\\\\\n",
+ ]
+
+ ff = data.float_format
+ if point_meta_segments is None:
+ for segment in segments:
+ for x, y, z in segment:
+ content.append(f"{x:{ff}} {y:{ff}} {z:{ff}}\\\\\n")
+ else:
+ for segment, point_meta in zip(segments, point_meta_segments, strict=True):
+ for (x, y, z), meta in zip(segment, point_meta, strict=True):
+ content.append(f"{x:{ff}} {y:{ff}} {z:{ff}} {meta:{ff}}\\\\\n")
+ content.append("};\n")
+ return content
+
+
+def _patch_table(
+ data: TikzData, segments: list[np.ndarray], color_data: list[float] | None = None
+) -> str:
+ rows: list[str] = []
+ first_index = 0
+ for i, segment in enumerate(segments):
+ indices = [str(index) for index in range(first_index, first_index + len(segment))]
+ first_index += len(segment)
+ if color_data is not None:
+ indices.append(_format_value(color_data[i], data.float_format))
+ rows.append(" ".join(indices) + r"\\")
+
+ key = "patch table with point meta" if color_data is not None else "patch table"
+ return key + "={%\n" + "\n".join(rows) + "\n}"
+
+
+def _group_segments_by_vertex_count(
+ segment_colors: list[tuple[np.ndarray, float]],
+) -> Iterable[tuple[int, list[tuple[np.ndarray, float]]]]:
+ grouped: dict[int, list[tuple[np.ndarray, float]]] = {}
+ for segment, color_value in segment_colors:
+ grouped.setdefault(len(segment), []).append((segment, float(color_value)))
+ return grouped.items()
+
+
+def _group_segments_only_by_vertex_count(
+ segments: list[np.ndarray],
+) -> Iterable[tuple[int, list[np.ndarray]]]:
+ grouped: dict[int, list[np.ndarray]] = {}
+ for segment in segments:
+ grouped.setdefault(len(segment), []).append(segment)
+ return grouped.items()
+
+
+def _cycle_array(values: Sequence[T] | np.ndarray | None, index: int) -> T | None:
+ if values is None:
+ return None
+ length = len(values)
+ if length == 0:
+ return None
+ return values[index % length]
+
+
+def _hashable_color(color: np.ndarray | None) -> HashableColor | None:
+ if color is None:
+ return None
+ return tuple(float(value) for value in np.asarray(color).reshape(-1))
+
+
+def _rgb_color(color: np.ndarray | None) -> np.ndarray | None:
+ if color is None:
+ return None
+ color_array = np.asarray(color, dtype=float).reshape(-1)
+ if len(color_array) < 3: # noqa: PLR2004
+ return None
+ return color_array[:3]
+
+
+def _color_alpha(color: np.ndarray | None) -> float | None:
+ if color is None:
+ return None
+ color_array = np.asarray(color, dtype=float).reshape(-1)
+ if len(color_array) < RGBA_LENGTH:
+ return None
+ return float(color_array[3])
+
+
+def _poly3d_fill_opacity_options(data: TikzData, alpha: float | None) -> list[str]:
+ if alpha is None or alpha == 1.0:
+ return []
+ return [f"fill opacity={alpha:{data.float_format}}"]
+
+
+def addplot_table( # noqa: PLR0913
+ data: TikzData,
+ coordinates: np.ndarray,
+ *,
+ command: str = "addplot",
+ options: Sequence[str] | None = None,
+ table_options: Sequence[str] | None = None,
+ column_names: Sequence[str] | None = None,
+ externalize_min_rows: int = 3,
+) -> list[str]:
+ """Return a PGFPlots addplot command with inline or external table data."""
+ coordinates = _as_2d_array(coordinates)
+ if len(coordinates) == 0:
+ return []
+
+ content = [f"\\{command}"]
+ if options:
+ content.append(" [" + ", ".join(options) + "]")
+ content.append("\n")
+ content.extend(
+ table(
+ data,
+ coordinates,
+ table_options=table_options,
+ column_names=column_names,
+ externalize_min_rows=externalize_min_rows,
+ )
+ )
+ return content
+
+
+def table(
+ data: TikzData,
+ coordinates: np.ndarray,
+ *,
+ table_options: Sequence[str] | None = None,
+ column_names: Sequence[str] | None = None,
+ externalize_min_rows: int = 3,
+) -> list[str]:
+ """Return PGFPlots table code for numeric coordinate data."""
+ coordinates = _as_2d_array(coordinates)
+ if not np.all(np.isfinite(coordinates)) and "unbounded coords=jump" not in (
+ data.current_axis_options
+ ):
+ data.current_axis_options.add("unbounded coords=jump")
+
+ opts = list(table_options or [])
+ if data.table_row_sep != "\n":
+ opts.append("row sep=" + data.table_row_sep.strip())
+
+ plot_table = table_rows(data, coordinates, column_names=column_names)
+ content = []
+ if data.externalize_tables and len(coordinates) >= externalize_min_rows:
+ filepath, rel_filepath = _files.new_filepath(data, "table", ".dat")
+ with filepath.open("w") as f:
+ f.write("".join(plot_table))
+
+ if data.externals_search_path is not None:
+ opts.append(f"search path={{{data.externals_search_path}}}")
+
+ opts_str = ("[" + ",".join(opts) + "] ") if opts else ""
+ content.append(f"table {opts_str}{{{rel_filepath.as_posix()}}};\n")
+ return content
+
+ if opts:
+ content.append("table [" + ",".join(opts) + "] {%\n")
+ else:
+ content.append("table {%\n")
+ content.extend(plot_table)
+ content.append("};\n")
+ return content
+
+
+def table_rows(
+ data: TikzData,
+ coordinates: np.ndarray,
+ *,
+ column_names: Sequence[str] | None = None,
+) -> list[str]:
+ coordinates = _as_2d_array(coordinates)
+ ff = data.float_format
+ table_row_sep = data.table_row_sep
+ rows = []
+ if column_names:
+ rows.append(" ".join(column_names) + table_row_sep)
+ rows.extend(
+ " ".join(_format_value(value, ff) for value in row) + table_row_sep for row in coordinates
+ )
+ return rows
+
+
+def _as_2d_array(coordinates: np.ndarray) -> np.ndarray:
+ coordinates = np.asarray(coordinates)
+ if coordinates.ndim == 1:
+ return coordinates.reshape((-1, 1))
+ if coordinates.ndim != 2: # noqa: PLR2004
+ msg = f"Expected 2D coordinate array, got shape {coordinates.shape}."
+ raise ValueError(msg)
+ return coordinates
+
+
+def _format_value(value: str | float | np.number, float_format: str) -> str:
+ if isinstance(value, str):
+ return value
+ if isinstance(value, Iterable):
+ msg = f"Unexpected nested table value {value!r}."
+ raise TypeError(msg)
+ numeric_value = value
+ if not isinstance(numeric_value, (int, float, np.number)):
+ msg = f"Unexpected table value {value!r}."
+ raise TypeError(msg)
+ return f"{numeric_value:{float_format}}"
diff --git a/src/matplot2tikz/_path.py b/src/matplot2tikz/_path.py
index e1da1cd0..80a9a185 100644
--- a/src/matplot2tikz/_path.py
+++ b/src/matplot2tikz/_path.py
@@ -27,10 +27,10 @@
@dataclass
class LineData:
obj: Collection | Patch
- ec: str | tuple | None = None # edgecolor
+ ec: str | tuple | np.ndarray | None = None # edgecolor
ec_name: str | None = None
ec_rgba: np.ndarray | None = None
- fc: str | tuple | None = None # facecolor
+ fc: str | tuple | np.ndarray | None = None # facecolor
fc_name: str | None = None
fc_rgba: np.ndarray | None = None
ls: str | tuple[float, Sequence[float] | None] | None = None # linestyle
@@ -42,16 +42,26 @@ class LineData:
class PathCollectionData:
obj: PathCollection
dd_strings: np.ndarray
- draw_options: list
- labels: list
- table_options: list
+ draw_options: list[str]
+ labels: list[str]
+ table_options: list[str]
is_contour: bool
+ command: str = "\\addplot"
marker: str | None = None
is_filled: bool = False
add_individual_color_code: bool | None = False
legend_text: str | None = None
+@dataclass
+class PathCollectionCoordinates:
+ offsets: np.ndarray
+ labels: list[str]
+ table_options: list[str]
+ is_contour: bool
+ command: str
+
+
def draw_path(
data: TikzData,
path: Path,
@@ -162,31 +172,32 @@ def _check_x_is_date(data: TikzData) -> bool:
def draw_pathcollection(data: TikzData, obj: PathCollection) -> list[str]:
"""Returns PGFPlots code for a number of patch objects."""
- content = []
- # gather data
- dd = obj.get_offsets()
- if not isinstance(dd, Iterable):
- # No idea what to draw.
- return []
-
- path_collection_data = PathCollectionData(
- obj=obj,
- dd_strings=np.array(
- [
- [f"{val:{data.float_format}}" for val in row] # type: ignore[str-bytes-safe]
- for row in dd
- if isinstance(row, Iterable)
- ]
+ offsets = np.asarray(obj.get_offsets())
+ path_collection_data = make_pathcollection_data(
+ data,
+ obj,
+ PathCollectionCoordinates(
+ offsets=offsets,
+ labels=["x", "y"],
+ table_options=[],
+ is_contour=len(offsets) == 1,
+ command="\\addplot",
),
- draw_options=["only marks"],
- labels=["x", "y"],
- table_options=[],
- is_contour=isinstance(dd, Sized) and len(dd) == 1,
)
+ return draw_pathcollection_data(data, path_collection_data)
+
+
+def draw_pathcollection_data(data: TikzData, path_collection_data: PathCollectionData) -> list[str]:
+ content = []
+ obj = path_collection_data.obj
line_data = LineData(obj=obj)
if obj.get_array() is not None:
_draw_pathcollection_scatter_colormap(data, path_collection_data)
+ _draw_pathcollection_get_edgecolors(data, path_collection_data, line_data)
+ _draw_pathcollection_get_marker(path_collection_data)
+ _draw_pathcollection_get_linewidth(path_collection_data, line_data)
+ path_collection_data.is_filled = True
else:
# gather the draw options
_draw_pathcollection_get_edgecolors(data, path_collection_data, line_data)
@@ -202,7 +213,7 @@ def draw_pathcollection(data: TikzData, obj: PathCollection) -> list[str]:
for path in obj.get_paths():
_draw_pathcollection_draw_contour(path, data, path_collection_data)
- _draw_pathcollection_scatter_sizes(path_collection_data)
+ _draw_pathcollection_scatter_sizes(data, path_collection_data)
# remove duplicates
draw_options = sorted(set(path_collection_data.draw_options))
@@ -211,7 +222,7 @@ def draw_pathcollection(data: TikzData, obj: PathCollection) -> list[str]:
len_row = sum(len(item) for item in draw_options)
j0, j1, j2 = ("", ", ", "") if len_row < max_row_length else ("\n ", ",\n ", "\n")
do = f" [{j0}{{}}{j2}]".format(j1.join(draw_options)) if draw_options else ""
- content.append(f"\\addplot{do}\n")
+ content.append(f"{path_collection_data.command}{do}\n")
if data.externals_search_path is not None:
esp = data.externals_search_path
@@ -245,13 +256,46 @@ def draw_pathcollection(data: TikzData, obj: PathCollection) -> list[str]:
return content
+def make_pathcollection_data(
+ data: TikzData,
+ obj: PathCollection,
+ coordinates: PathCollectionCoordinates,
+) -> PathCollectionData:
+ dd = coordinates.offsets
+ if not isinstance(dd, Iterable):
+ msg = f"Expected iterable path collection offsets, got {type(dd)}."
+ raise TypeError(msg)
+
+ dd_strings = []
+ for row in dd:
+ if not isinstance(row, Iterable):
+ msg = f"Expected iterable path collection offset row, got {row!r}."
+ raise TypeError(msg)
+ dd_strings.append([f"{val:{data.float_format}}" for val in row])
+
+ return PathCollectionData(
+ obj=obj,
+ dd_strings=np.array(dd_strings),
+ draw_options=["only marks"],
+ labels=coordinates.labels,
+ table_options=coordinates.table_options,
+ is_contour=coordinates.is_contour,
+ command=coordinates.command,
+ )
+
+
def _draw_pathcollection_scatter_colormap(data: TikzData, pcd: PathCollectionData) -> None:
obj_array = pcd.obj.get_array()
if obj_array is not None:
- pcd.dd_strings = np.column_stack([pcd.dd_strings, obj_array])
+ colordata = [f"{value:{data.float_format}}" for value in np.ma.getdata(obj_array)]
+ pcd.dd_strings = np.column_stack([pcd.dd_strings, colordata])
pcd.labels.append("colordata")
pcd.draw_options.append("scatter src=explicit")
- pcd.table_options.extend(["x=x", "y=y", "meta=colordata"])
+ coordinate_options = [f"{label}={label}" for label in pcd.labels if label in {"x", "y", "z"}]
+ for option in coordinate_options:
+ if option not in pcd.table_options:
+ pcd.table_options.append(option)
+ pcd.table_options.append("meta=colordata")
if pcd.obj.get_cmap():
mycolormap, is_custom_cmap = _mpl_cmap2pgf_cmap(pcd.obj.get_cmap(), data)
pcd.draw_options.append("scatter")
@@ -268,7 +312,7 @@ def _draw_pathcollection_get_edgecolors(
else:
if len(edgecolors) == 1:
line_data.ec = edgecolors[0]
- elif len(edgecolors) > 1:
+ elif len(edgecolors) == len(pcd.dd_strings):
pcd.labels.append("draw")
ec_strings = [
@@ -290,7 +334,7 @@ def _draw_pathcollection_get_facecolors(
if len(facecolors) == 1:
line_data.fc = facecolors[0]
pcd.is_filled = True
- elif len(facecolors) > 1:
+ elif len(facecolors) == len(pcd.dd_strings):
pcd.labels.append("fill")
fc_strings = [
",".join(f"{item:{data.float_format}}" for item in row)
@@ -301,6 +345,12 @@ def _draw_pathcollection_get_facecolors(
pcd.is_filled = True
+def _draw_pathcollection_get_linewidth(pcd: PathCollectionData, line_data: LineData) -> None:
+ linewidths = np.atleast_1d(pcd.obj.get_linewidth())
+ if len(linewidths) == 1:
+ line_data.lw = float(linewidths[0])
+
+
def _draw_pathcollection_add_individual_color(pcd: PathCollectionData) -> None:
if pcd.add_individual_color_code:
pcd.draw_options.extend(
@@ -387,11 +437,11 @@ def _draw_pathcollection_draw_contour(path: Path, data: TikzData, pcd: PathColle
pcd.dd_strings = np.array(dd_strings[1:], dtype=object)
-def _draw_pathcollection_scatter_sizes(pcd: PathCollectionData) -> None:
+def _draw_pathcollection_scatter_sizes(data: TikzData, pcd: PathCollectionData) -> None:
if len(pcd.obj.get_sizes()) == len(pcd.dd_strings):
# See Pgfplots manual, chapter 4.25.
# In Pgfplots, \mark size specifies radii, in matplotlib circle areas.
- radii = np.sqrt(pcd.obj.get_sizes() / np.pi)
+ radii = [f"{radius:{data.float_format}}" for radius in np.sqrt(pcd.obj.get_sizes() / np.pi)]
pcd.dd_strings = np.column_stack([pcd.dd_strings, radii])
pcd.labels.append("sizedata")
pcd.draw_options.extend(
diff --git a/src/matplot2tikz/_save.py b/src/matplot2tikz/_save.py
index c3763c3c..aefd988d 100644
--- a/src/matplot2tikz/_save.py
+++ b/src/matplot2tikz/_save.py
@@ -5,7 +5,7 @@
import tempfile
import warnings
from pathlib import Path
-from typing import TYPE_CHECKING, TypedDict
+from typing import TYPE_CHECKING, Literal, TypedDict, cast
import matplotlib as mpl
import matplotlib.pyplot as plt
@@ -20,17 +20,26 @@
from matplotlib.patches import Patch
from matplotlib.spines import Spine
from matplotlib.text import Text
+from mpl_toolkits.mplot3d import Axes3D
+from mpl_toolkits.mplot3d.art3d import Line3D, Line3DCollection, Path3DCollection, Poly3DCollection
from typing_extensions import NotRequired, Unpack
if TYPE_CHECKING:
+ from collections.abc import Sequence
+
from matplotlib.artist import Artist
-from . import _axes, _legend, _line2d, _patch, _path, _text, _util
+ from ._clip3d import Clip3DMode
+
+
+from . import _axes, _legend, _line2d, _mplot3d, _patch, _path, _text, _util
from . import _image as img
from . import _quadmesh as qmsh
from .__about__ import __version__
from ._tikzdata import Flavors, TikzData
+ShaderMode = Literal["none", "interp"]
+
# Set logger to be used to print some info
LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)
@@ -61,11 +70,19 @@ class TikzArgs(TypedDict):
show_info: NotRequired[bool]
include_disclaimer: NotRequired[bool]
standalone: NotRequired[bool]
+ clip_3d: NotRequired[Clip3DMode]
+ shader: NotRequired[ShaderMode]
float_format: NotRequired[str]
table_row_sep: NotRequired[str]
flavor: NotRequired[str]
+def _validate_clip_3d(clip_3d: str) -> None:
+ if clip_3d not in ("none", "hide", "clip"):
+ msg = 'clip_3d must be one of "none", "hide", or "clip".'
+ raise ValueError(msg)
+
+
def get_tikz_code( # noqa: PLR0913
figure: str | Figure = "gcf",
filepath: str | Path | None = None,
@@ -87,6 +104,8 @@ def get_tikz_code( # noqa: PLR0913
show_info: bool = False, # noqa: FBT001, FBT002
include_disclaimer: bool = True, # noqa: FBT001, FBT002
standalone: bool = False, # noqa: FBT001, FBT002
+ clip_3d: Clip3DMode = "none",
+ shader: ShaderMode = "none",
float_format: str = ".15g",
table_row_sep: str = "\n",
flavor: str = "latex",
@@ -174,6 +193,16 @@ def get_tikz_code( # noqa: PLR0913
:param standalone: Include wrapper code for a standalone LaTeX file.
:type standalone: bool
+ :param clip_3d: How 3D artists outside the 3D axis limits are handled.
+ ``"none"`` preserves the current export behavior,
+ ``"hide"`` removes artists outside the limits, and
+ ``"clip"`` clips supported lines and polygons to the limits.
+ :type clip_3d: str
+
+ :param shader: Optional PGFPlots shader value for exported 3D patch plots,
+ for example ``"interp"`` to emit ``shader=interp``.
+ :type shader: str
+
:param float_format: Format for float entities. Default is ```".15g"```.
:type float_format: str
@@ -202,6 +231,8 @@ def get_tikz_code( # noqa: PLR0913
f"Unsupported TeX flavor {flavor!r}. Please choose from {', '.join(map(repr, Flavors))}"
)
raise ValueError(msg) from None
+ _validate_clip_3d(clip_3d)
+
data = TikzData(flavor=flavor_object)
data.externalize_tables = externalize_tables
@@ -212,6 +243,8 @@ def get_tikz_code( # noqa: PLR0913
data.show_info = show_info
data.strict = strict
data.standalone = standalone
+ data.clip_3d = clip_3d
+ data.shader = shader
data.axis_width, data.axis_height = axis_width, axis_height
if tex_relative_path_to_data is not None:
@@ -242,8 +275,11 @@ def get_tikz_code( # noqa: PLR0913
if show_info:
_print_pgfplot_libs_message(data)
+ mpl_figure = _get_figure(figure)
+ _finalize_figure_for_export(mpl_figure)
+
# gather the file content
- content = _recurse(data, _get_figure(figure))
+ content = _recurse(data, mpl_figure)
# Check if there is still an open groupplot environment. This occurs if not
# all of the group plot slots are used.
@@ -262,6 +298,21 @@ def _get_figure(figure: str | Figure) -> Figure:
raise ValueError(msg)
+def _finalize_figure_for_export(figure: Figure) -> None:
+ axes3d = [ax for ax in figure.axes if isinstance(ax, Axes3D)]
+ if not axes3d:
+ return
+
+ renderer = figure.canvas.get_renderer() # type: ignore[attr-defined]
+
+ for ax in axes3d:
+ # Axes3D updates aspect-dependent locator state during draw. Do the same
+ # narrowly, without running collection projection/sorting side effects.
+ ax._unstale_viewLim() # noqa: SLF001
+ locator = ax.get_axes_locator()
+ ax.apply_aspect(locator(ax, renderer) if locator else None)
+
+
def _set_filepath(data: TikzData, filepath: str | Path | None) -> None:
if filepath:
filepath = Path(filepath)
@@ -294,7 +345,7 @@ def save(
f.write(code)
-def _generate_code(data: TikzData, content: list) -> str:
+def _generate_code(data: TikzData, content: Sequence[str]) -> str:
# write disclaimer to the file header
code = """"""
@@ -323,7 +374,7 @@ def _generate_code(data: TikzData, content: list) -> str:
if data.standalone:
# When using pdflatex, \\DeclareUnicodeCharacter is necessary.
- code = data.flavor.standalone(code)
+ code = data.flavor.standalone(code, data)
return code
@@ -332,7 +383,7 @@ def _tex_comment(comment: str) -> str:
return "% " + str.replace(comment, "\n", "\n% ") + "\n"
-def _get_color_definitions(data: TikzData) -> list:
+def _get_color_definitions(data: TikzData) -> list[str]:
"""Returns the list of custom color definitions for the TikZ file."""
# sort by key
sorted_keys = sorted(data.custom_colors.keys(), key=lambda x: x.lower())
@@ -356,14 +407,14 @@ class _ContentManager:
def __init__(self) -> None:
self._content: dict[float, list[str]] = {}
- def extend(self, content: list, zorder: float) -> None:
+ def extend(self, content: Sequence[str], zorder: float) -> None:
"""Extends with a list and a z-order."""
if zorder not in self._content:
self._content[zorder] = []
self._content[zorder].extend(content)
- def flatten(self) -> list:
- content_out = []
+ def flatten(self) -> list[str]:
+ content_out: list[str] = []
all_z = sorted(self._content.keys())
for z in all_z:
content_out.extend(self._content[z])
@@ -371,16 +422,28 @@ def flatten(self) -> list:
def _draw_collection(data: TikzData, child: Collection) -> list[str]:
- if isinstance(child, PathCollection):
- return _path.draw_pathcollection(data, child)
- if isinstance(child, LineCollection):
- return _line2d.draw_linecollection(data, child)
- if isinstance(child, QuadMesh):
- return qmsh.draw_quadmesh(data, child)
- return _patch.draw_patchcollection(data, child)
+ if _mplot3d.is_quiver3d_collection(child):
+ content = _mplot3d.draw_quiver3d(data, cast("Line3DCollection", child))
+ elif _mplot3d.is_contour3d_collection(child):
+ content = _mplot3d.draw_contour3d(data, child)
+ elif isinstance(child, Poly3DCollection):
+ content = _mplot3d.draw_poly3dcollection(data, child)
+ elif isinstance(child, Path3DCollection):
+ content = _mplot3d.draw_path3dcollection(data, child)
+ elif isinstance(child, Line3DCollection):
+ content = _mplot3d.draw_line3dcollection(data, child)
+ elif isinstance(child, PathCollection):
+ content = _path.draw_pathcollection(data, child)
+ elif isinstance(child, LineCollection):
+ content = _line2d.draw_linecollection(data, child)
+ elif isinstance(child, QuadMesh):
+ content = qmsh.draw_quadmesh(data, child)
+ else:
+ content = _patch.draw_patchcollection(data, child)
+ return content
-def _recurse(data: TikzData, obj: Artist) -> list:
+def _recurse(data: TikzData, obj: Artist) -> list[str]:
"""Iterates over all children of the current object and gathers the contents.
Content is returned.
@@ -418,6 +481,7 @@ def _recurse(data: TikzData, obj: Artist) -> list:
else:
for child_type, process_func in (
+ (Line3D, _mplot3d.draw_line3d),
(Line2D, _line2d.draw_line2d),
(AxesImage, img.draw_image),
(Patch, _patch.draw_patch),
@@ -437,6 +501,7 @@ def _recurse(data: TikzData, obj: Artist) -> list:
def _process_axes(data: TikzData, obj: Axes, content: _ContentManager) -> None:
+ data.current_mpl_axes = obj
ax = _axes.MyAxes(data, obj)
if ax.is_colorbar:
@@ -446,8 +511,6 @@ def _process_axes(data: TikzData, obj: Axes, content: _ContentManager) -> None:
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)
diff --git a/src/matplot2tikz/_text.py b/src/matplot2tikz/_text.py
index 7d46f0ae..a19818c7 100644
--- a/src/matplot2tikz/_text.py
+++ b/src/matplot2tikz/_text.py
@@ -5,11 +5,19 @@
from typing import TYPE_CHECKING
import matplotlib as mpl
+import numpy as np
from matplotlib.font_manager import font_scalings
-from matplotlib.patches import ArrowStyle, BoxStyle, FancyArrowPatch, FancyBboxPatch
+from matplotlib.patches import (
+ ArrowStyle,
+ BoxStyle,
+ FancyArrowPatch,
+ FancyBboxPatch,
+)
from matplotlib.text import Annotation, Text
+from mpl_toolkits.mplot3d.art3d import Text3D
-from . import _color
+from . import _color, _mplot3d
+from ._clip3d import clip_box_from_axes, points_inside
if TYPE_CHECKING:
from matplotlib.offsetbox import AnchoredText
@@ -30,9 +38,7 @@ def draw_text(data: TikzData, obj: Text) -> list[str]:
text = obj.get_text()
- if text in ["", data.current_axis_title]:
- # Text nodes which are direct children of Axes are typically titles. They are
- # already captured by the `title` property of pgfplots axes, so skip them here.
+ if _is_skipped_text(data, obj, text):
return content
size = obj.get_fontsize()
@@ -179,6 +185,20 @@ def draw_anchored_text(data: TikzData, obj: AnchoredText) -> list[str]:
return content
+def _is_skipped_text(data: TikzData, obj: Text, text: str) -> bool:
+ if text in ["", data.current_axis_title]:
+ return True
+ return _is_clipped_text3d(data, obj)
+
+
+def _is_clipped_text3d(data: TikzData, obj: Text) -> bool:
+ if data.clip_3d == "none" or not isinstance(obj, Text3D) or obj.axes is None:
+ return False
+ x, y, z = _mplot3d.get_text3d_position(obj)
+ mask = points_inside(np.asarray([[x, y, z]], dtype=float), clip_box_from_axes(obj.axes))
+ return not bool(mask[0])
+
+
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()
@@ -186,6 +206,12 @@ def _get_tikz_pos(data: TikzData, obj: Text, content: list[str]) -> str:
if isinstance(pos, str):
return pos
if obj.axes:
+ if isinstance(obj, Text3D):
+ x, y, z = _mplot3d.get_text3d_position(obj)
+ return (
+ f"(axis cs:{x:{data.float_format}},{y:{data.float_format}},{z:{data.float_format}})"
+ )
+
# Check if the text uses axes-relative coordinates (transform=ax.transAxes).
# In that case, use `rel axis cs` instead of `axis cs`.
transform = obj.get_transform()
diff --git a/src/matplot2tikz/_tikzdata.py b/src/matplot2tikz/_tikzdata.py
index c1313ec4..1b9eddb2 100644
--- a/src/matplot2tikz/_tikzdata.py
+++ b/src/matplot2tikz/_tikzdata.py
@@ -8,6 +8,8 @@
if TYPE_CHECKING:
from matplotlib.axes import Axes
+ from ._clip3d import Clip3DMode
+
@dataclass
class TikzData:
@@ -22,6 +24,8 @@ class TikzData:
strict: bool = False
standalone: bool = False
is_in_groupplot_env: bool = False
+ clip_3d: Clip3DMode = "none"
+ shader: str = "none"
dpi: int = 100
font_size: float = 10.0
@@ -52,6 +56,7 @@ class TikzData:
nb_keys: dict = field(default_factory=dict)
current_mpl_axes: Axes | None = None
+ custom_colormap_id: int = 0
class Flavors(enum.Enum):
@@ -101,6 +106,6 @@ def preamble(self, data: TikzData | None = None) -> str:
tikzlibs = ",".join(data.tikz_libs)
return self.value[3].format(pgfplotslibs=pgfplotslibs, tikzlibs=tikzlibs)
- def standalone(self, code: str) -> str:
+ def standalone(self, code: str, data: TikzData | None = None) -> str:
docenv = self.value[2]
- return f"{self.preamble()}{self.start(docenv)}\n{code}\n{self.end(docenv)}"
+ return f"{self.preamble(data)}{self.start(docenv)}\n{code}\n{self.end(docenv)}"
diff --git a/src/matplot2tikz/_util.py b/src/matplot2tikz/_util.py
index 2b233373..a6053bec 100644
--- a/src/matplot2tikz/_util.py
+++ b/src/matplot2tikz/_util.py
@@ -100,13 +100,12 @@ def _tex_escape(text: str) -> str:
# Not using \mathnormal instead since this looks odd for the latex cm font.
text = _replace_mathdefault(text)
text = text.replace("\N{MINUS SIGN}", r"\ensuremath{-}")
- # Work around
- text = text.replace("&", r"\&")
- text = text.replace("_", r"\_")
- text = text.replace("%", r"\%")
- # split text into normaltext and inline math parts
+ # Split text into normaltext and inline math parts
parts = _split_math(text)
for i, s in enumerate(parts):
if i % 2: # mathmode replacements
parts[i] = rf"\(\displaystyle {s}\)"
+ else:
+ # Work around
+ parts[i] = s.replace("&", r"\&").replace("_", r"\_").replace("%", r"\%")
return "".join(parts)
diff --git a/tests/test_3d.py b/tests/test_3d.py
new file mode 100644
index 00000000..6d193542
--- /dev/null
+++ b/tests/test_3d.py
@@ -0,0 +1,724 @@
+"""Test 3D plot export."""
+
+import tempfile
+from collections.abc import Callable
+from pathlib import Path
+from typing import TYPE_CHECKING, Literal, cast
+
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+import pytest
+from matplotlib.figure import Figure
+from mpl_toolkits.mplot3d.art3d import Poly3DCollection
+from typing_extensions import NotRequired, TypedDict, Unpack
+
+import matplot2tikz
+
+from .helpers import assert_equality
+
+mpl.use("Agg")
+
+if TYPE_CHECKING:
+ from mpl_toolkits.mplot3d import Axes3D
+
+Clip3DMode = Literal["none", "hide", "clip"]
+ShaderMode = Literal["none", "interp"]
+
+
+class _TikzCodeOptions(TypedDict):
+ axis_width: NotRequired[str | None]
+ axis_height: NotRequired[str | None]
+ extra_axis_parameters: NotRequired[list[str] | None]
+ strict: NotRequired[bool]
+ add_axis_environment: NotRequired[bool]
+ standalone: NotRequired[bool]
+ externalize_tables: NotRequired[bool]
+ clip_3d: NotRequired[Clip3DMode]
+ shader: NotRequired[ShaderMode]
+
+
+def plot_line_and_scatter() -> Figure:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+
+ theta = np.linspace(0.0, 2.5 * np.pi, 18)
+ radius = 0.25 + 0.07 * theta
+ ax.plot(
+ radius * np.cos(theta),
+ radius * np.sin(theta),
+ 0.18 * theta,
+ color="tab:red",
+ marker="o",
+ label="spiral",
+ )
+ ax.plot(
+ np.linspace(-0.8, 0.8, 9),
+ 0.25 * np.sin(np.linspace(-2.0, 2.0, 9)),
+ 0.35 + 0.15 * np.cos(np.linspace(-2.0, 2.0, 9)),
+ color="tab:blue",
+ linestyle="--",
+ marker="^",
+ label="ridge",
+ )
+
+ xs = np.linspace(-0.7, 0.7, 4)
+ ys = np.linspace(-0.6, 0.6, 3)
+ xx, yy = np.meshgrid(xs, ys)
+ zz = 0.3 + 0.45 * xx**2 + 0.25 * np.cos(np.pi * yy)
+ ax.scatter(
+ xx.ravel(),
+ yy.ravel(),
+ zz.ravel(),
+ c=zz.ravel(),
+ s=np.linspace(12.0, 52.0, zz.size),
+ cmap="viridis",
+ marker="D",
+ edgecolors="black",
+ linewidths=0.35,
+ label="samples",
+ )
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.set_xlim(-1.05, 1.05)
+ ax.set_ylim(-0.9, 0.9)
+ ax.set_zlim(0.0, 1.6)
+ ax.view_init(elev=28.0, azim=38.0)
+ ax.text(0.28, -0.72, 1.18, "3D text")
+ ax.legend(loc="upper left")
+ return fig
+
+
+def plot_surface_and_wireframe() -> Figure:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.arange(-5, 5, 0.5)
+ y = np.arange(-5, 5, 0.5)
+ xx, yy = np.meshgrid(x, y)
+ zz = np.sin(np.sqrt(xx**2 + yy**2))
+ ax.plot_surface(
+ xx,
+ yy,
+ zz,
+ cmap=plt.get_cmap("viridis"),
+ edgecolor="black",
+ linewidth=0.25,
+ alpha=0.88,
+ )
+ ax.plot_wireframe(
+ xx,
+ yy,
+ zz + 0.75,
+ color="black",
+ linewidth=0.45,
+ rstride=2,
+ cstride=2,
+ )
+ ax.contour(
+ xx,
+ yy,
+ zz,
+ levels=[-0.2, 0.0, 0.2],
+ colors=["navy", "darkorange", "crimson"],
+ linewidths=0.9,
+ )
+ ax.contour(
+ xx,
+ yy,
+ zz,
+ levels=[-0.2, 0.0, 0.2],
+ zdir="z",
+ offset=-0.85,
+ cmap="viridis",
+ linewidths=0.6,
+ )
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.set_xlim(-5, 5)
+ ax.set_ylim(-5, 5)
+ ax.set_zlim(-1, 1)
+ ax.view_init(elev=24.0, azim=-55.0)
+ return fig
+
+
+def plot_bar3d() -> Figure:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ xpos, ypos = np.meshgrid([0.0, 1.1, 2.2], [0.0, 1.0])
+ xpos = xpos.ravel()
+ ypos = ypos.ravel()
+ zpos = np.zeros_like(xpos)
+ dx = np.array([0.55, 0.75, 0.65, 0.55, 0.75, 0.65])
+ dy = np.array([0.55, 0.45, 0.65, 0.75, 0.5, 0.6])
+ dz = np.array([0.5, 1.15, 0.8, 1.35, 0.65, 1.55])
+ colors = plt.get_cmap("cividis")((dz - dz.min()) / (dz.max() - dz.min()))
+ ax.bar3d(
+ xpos,
+ ypos,
+ zpos,
+ dx,
+ dy,
+ dz,
+ color=colors,
+ edgecolor="black",
+ linewidth=0.35,
+ alpha=0.82,
+ shade=False,
+ )
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.set_xlim(-0.2, 3.05)
+ ax.set_ylim(-0.2, 1.9)
+ ax.set_zlim(0.0, 1.7)
+ ax.view_init(elev=26.0, azim=35.0)
+ return fig
+
+
+def plot_quiver3d() -> Figure:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.array([0.0, 0.9, 0.0, 0.9])
+ y = np.array([0.0, 0.0, 0.8, 0.8])
+ z = np.array([0.0, 0.2, 0.1, 0.3])
+ u = np.array([0.45, -0.2, 0.25, -0.3])
+ v = np.array([0.15, 0.35, -0.25, -0.2])
+ w = np.array([0.35, 0.25, 0.45, 0.2])
+ ax.quiver(
+ x,
+ y,
+ z,
+ u,
+ v,
+ w,
+ length=0.8,
+ arrow_length_ratio=0.25,
+ color="tab:green",
+ linewidth=1.1,
+ )
+ ax.set_xlabel("X")
+ ax.set_ylabel("Y")
+ ax.set_zlabel("Z")
+ ax.set_xlim(-0.1, 1.1)
+ ax.set_ylim(-0.1, 1.1)
+ ax.set_zlim(0.0, 0.8)
+ ax.view_init(elev=30.0, azim=-45.0)
+ return fig
+
+
+def test_line_and_scatter() -> None:
+ assert_equality(plot_line_and_scatter, "test_3d_line_and_scatter_reference.tex")
+
+
+def test_colormapped_3d_scatter_preserves_marker_and_edge() -> None:
+ code = _tikz_code(plot_line_and_scatter)
+
+ assert "mark=diamond*" in code
+ assert "draw=black" in code
+ assert "line width=0.14pt" in code
+
+
+def test_surface_and_wireframe() -> None:
+ assert_equality(plot_surface_and_wireframe, "test_3d_surface_and_wireframe_reference.tex")
+
+
+def test_3d_surface_shader_option() -> None:
+ code = _tikz_code(plot_surface_and_wireframe, shader="interp")
+
+ assert "shader=interp" in code
+ assert "patch type=bilinear" in code
+ assert r"point meta=\thisrow{meta}" in code
+ assert "x y z meta\\\\" in code
+ assert "patch table with point meta" not in code
+ assert "patch type=polygon,\nvertex count=4" not in code
+
+
+def test_3d_axis_uses_native_label_and_tick_layout() -> None:
+ code = _tikz_code(plot_surface_and_wireframe)
+
+ assert "ylabel style={rotate=-90.0}" not in code
+ assert "yticklabel style={anchor=center}" not in code
+ assert "tick pos=left" not in code
+ assert "tick align=outside" not in code
+
+
+def test_3d_grid_matches_mplot3d_default() -> None:
+ code = _tikz_code(plot_surface_and_wireframe)
+
+ assert "xmajorgrids" in code
+ assert "ymajorgrids" in code
+ assert "zmajorgrids" in code
+
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot([0.0, 1.0], [0.0, 1.0], [0.0, 1.0])
+ ax.grid(visible=False)
+ code = matplot2tikz.get_tikz_code(fig, include_disclaimer=False, float_format=".8g")
+ plt.close("all")
+
+ assert "xmajorgrids" not in code
+ assert "ymajorgrids" not in code
+ assert "zmajorgrids" not in code
+
+
+def test_3d_view_uses_pgfplots_azimuth_convention() -> None:
+ code = _tikz_code(plot_line_and_scatter)
+
+ assert "view={128}{28}" in code
+
+
+def test_strict_3d_ticks_use_matplotlib_locations() -> None:
+ code = _tikz_code(plot_surface_and_wireframe, strict=True)
+
+ assert "xtick={-6,-4,-2,0,2,4,6}" in code
+ assert "ytick={-6,-4,-2,0,2,4,6}" in code
+ assert "ztick={-1,-0.75,-0.5,-0.25,0,0.25,0.5,0.75,1}" in code
+
+
+def test_3d_custom_tick_labels_are_exported_without_strict() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot([0.0, 1.0], [0.0, 1.0], [0.0, 1.0])
+ ax.set_xticks([0.0, 0.5, 1.0], ["left", "middle", "right"])
+ ax.set_yticks([0.0, 1.0], ["front", "back"])
+ ax.set_zticks([0.0, 1.0], ["low", "high"])
+
+ code = matplot2tikz.get_tikz_code(fig, include_disclaimer=False, float_format=".8g")
+ plt.close("all")
+
+ assert "xtick={0,0.5,1}" in code
+ assert "xticklabels={left,middle,right}" in code
+ assert "ytick={0,1}" in code
+ assert "yticklabels={front,back}" in code
+ assert "ztick={0,1}" in code
+ assert "zticklabels={low,high}" in code
+
+
+def test_strict_3d_custom_tick_locations_are_exported() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot([0.0, 1.0], [0.0, 1.0], [0.0, 1.0])
+ ax.set_xticks([0.0, 0.3, 0.9])
+ ax.set_yticks([0.1, 0.4, 1.0])
+ ax.set_zticks([0.2, 0.6, 1.0])
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", strict=True
+ )
+ plt.close("all")
+
+ assert "xtick={0,0.3,0.9}" in code
+ assert "ytick={0.1,0.4,1}" in code
+ assert "ztick={0.2,0.6,1}" in code
+
+
+def test_bar3d() -> None:
+ assert_equality(plot_bar3d, "test_3d_bar3d_reference.tex", strict=True)
+
+
+def test_bar3d_sorts_faces_in_one_patch_plot() -> None:
+ code = _tikz_code(plot_bar3d, strict=True)
+
+ assert code.count("\\addplot3 [") == 1
+ assert "patch table with point meta" in code
+ assert "colormap={matplot2tikzpoly0}" in code
+ assert "ztick={0,0.25,0.5,0.75,1,1.25,1.5,1.75}" in code
+
+
+def test_poly3d_colormap_keeps_colors_aligned_after_filtering() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ collection = Poly3DCollection(
+ [
+ np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]),
+ np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]),
+ np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]]),
+ ],
+ edgecolor="black",
+ )
+ collection.set_array(np.array([0.0, 1.0, 2.0]))
+ ax.add_collection3d(collection)
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(0.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(fig, include_disclaimer=False, float_format=".8g")
+ plt.close("all")
+
+ assert "0 1 2 1\\\\" in code
+ assert "3 4 5 2\\\\" in code
+ assert "0 1 2 0\\\\" not in code
+
+
+def test_clip3d_line_clips_to_axis_limits() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot([-1.0, 2.0], [0.5, 0.5], [0.0, 0.0])
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(-1.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="clip"
+ )
+ plt.close("all")
+
+ assert "0 0.5 0" in code
+ assert "1 0.5 0" in code
+ assert "-1 0.5 0" not in code
+
+
+def test_clip3d_line_preserves_contiguous_polyline() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot(
+ [-1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 2.0],
+ [0.5] * 7,
+ [0.5] * 7,
+ )
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(0.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="clip"
+ )
+ plt.close("all")
+
+ assert code.count("\\addplot3 [") == 1
+ assert "0 0.5 0.5" in code
+ assert "0.25 0.5 0.5" in code
+ assert "1 0.5 0.5" in code
+ assert "-1 0.5 0.5" not in code
+ assert "2 0.5 0.5" not in code
+
+
+def test_clip3d_line_preserves_disconnected_polyline_runs() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot(
+ [0.0, 1.0, np.nan, 1.0, 0.0],
+ [0.5, 0.5, np.nan, 0.5, 0.5],
+ [0.5, 0.5, np.nan, 0.5, 0.5],
+ )
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(0.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="clip"
+ )
+ plt.close("all")
+
+ assert code.count("\\addplot3 [") == 2 # noqa: PLR2004
+
+
+def test_clip3d_line_uses_log_axis_coordinates() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot([0.1, 10.0], [0.0, 2.0], [1.0, 1.0])
+ ax.set_xlim(1.0, 10.0)
+ ax.set_xscale("log")
+ ax.set_ylim(0.0, 2.0)
+ ax.set_zlim(0.1, 10.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="clip"
+ )
+ plt.close("all")
+
+ assert "1 1 1" in code
+ assert "10 2 1" in code
+ assert "0.1 0 1" not in code
+
+
+def test_clip3d_scatter_hides_points_outside_limits() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.scatter(
+ [0.2, 1.5, 0.8],
+ [0.2, 0.5, 1.8],
+ [0.2, 0.5, 0.8],
+ c=[1.0, 2.0, 3.0],
+ s=[10.0, 20.0, 30.0],
+ )
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(0.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="hide"
+ )
+ plt.close("all")
+
+ assert "0.2 0.2 0.2 1" in code
+ assert "1.5 0.5 0.5 2" not in code
+ assert "0.8 1.8 0.8 3" not in code
+
+
+def test_clip3d_poly_collection_clips_polygon_vertices() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ collection = Poly3DCollection(
+ [np.array([[-0.5, 0.2, 0.2], [0.5, 0.2, 0.2], [0.5, 0.8, 0.2]])],
+ facecolor="tab:red",
+ )
+ ax.add_collection3d(collection)
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(0.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="clip"
+ )
+ plt.close("all")
+
+ assert "0 0.2 0.2" in code
+ assert "-0.5 0.2 0.2" not in code
+
+
+def test_clip3d_surface_keeps_fully_inside_quad_patches() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.array([[0.0, 1.0], [0.0, 1.0]])
+ y = np.array([[0.0, 0.0], [1.0, 1.0]])
+ z = np.array([[0.0, 0.0], [0.0, 0.0]])
+ ax.plot_surface(x, y, z, linewidth=0.0, edgecolor="none")
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(-1.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="clip"
+ )
+ plt.close("all")
+
+ assert "vertex count=4" in code
+ assert "vertex count=3" not in code
+
+
+def test_clip3d_surface_keeps_clipped_quad_patches() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ collection = Poly3DCollection(
+ [
+ np.array(
+ [
+ [-1.0, 0.0, 0.0],
+ [1.0, 0.0, 0.0],
+ [1.0, 1.0, 0.0],
+ [-1.0, 1.0, 0.0],
+ ]
+ )
+ ],
+ facecolor="tab:blue",
+ edgecolor="none",
+ )
+ ax.add_collection3d(collection)
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(-1.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="clip"
+ )
+ plt.close("all")
+
+ assert "vertex count=4" in code
+ assert "vertex count=3" not in code
+ assert "0 0 0" in code
+ assert "-1 0 0" not in code
+
+
+def test_clip3d_shader_triangulates_collection_when_a_clipped_polygon_breaks() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ collection = Poly3DCollection(
+ [
+ np.array(
+ [
+ [0.1, 0.1, 0.0],
+ [0.4, 0.1, 1.0],
+ [0.4, 0.4, 2.0],
+ [0.1, 0.4, 3.0],
+ ]
+ ),
+ np.array(
+ [
+ [-0.5, 0.2, 0.0],
+ [0.8, 0.2, 2.0],
+ [0.8, 0.8, 4.0],
+ [0.2, 0.8, 6.0],
+ ]
+ ),
+ ],
+ edgecolor="none",
+ )
+ collection.set_array(np.array([0.0, 1.0]))
+ collection.set_cmap(plt.get_cmap("viridis"))
+ ax.add_collection3d(collection)
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(0.0, 6.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig,
+ include_disclaimer=False,
+ float_format=".8g",
+ clip_3d="clip",
+ shader="interp",
+ )
+ plt.close("all")
+
+ assert "patch type=triangle" in code
+ assert "patch type=bilinear" not in code
+ assert "patch type=polygon" not in code
+ assert "shader=interp" in code
+ assert r"point meta=\thisrow{meta}" in code
+
+
+def test_clip3d_quiver_hide_keeps_semantic_quiver_for_inside_arrows() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.quiver(
+ [0.2, 1.2],
+ [0.2, 0.2],
+ [0.2, 0.2],
+ [0.2, 0.2],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ )
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(0.0, 1.0)
+ ax.set_zlim(0.0, 1.0)
+
+ code = matplot2tikz.get_tikz_code(
+ fig, include_disclaimer=False, float_format=".8g", clip_3d="hide"
+ )
+ plt.close("all")
+
+ assert r"quiver={u=\thisrow{u}, v=\thisrow{v}, w=\thisrow{w}}" in code
+ assert "0.2 0.2 0.2 0.2 0 0" in code
+ assert "1.2 0.2 0.2 0.2 0 0" not in code
+
+
+def test_clip3d_rejects_unknown_mode() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ ax.plot([0.0, 1.0], [0.0, 1.0], [0.0, 1.0])
+
+ with pytest.raises(ValueError, match="clip_3d"):
+ matplot2tikz.get_tikz_code(
+ fig,
+ include_disclaimer=False,
+ clip_3d=cast("Clip3DMode", "invalid"),
+ )
+ plt.close("all")
+
+
+def test_quiver3d() -> None:
+ assert_equality(plot_quiver3d, "test_3d_quiver_reference.tex")
+
+
+def test_quiver3d_uses_semantic_pgfplots_quiver() -> None:
+ code = _tikz_code(plot_quiver3d)
+
+ assert code.count("\\addplot3 [") == 1
+ assert r"quiver={u=\thisrow{u}, v=\thisrow{v}, w=\thisrow{w}}" in code
+ assert "x y z u v w" in code
+ assert "-latex" in code
+ assert "mark=none" in code
+
+
+def test_3d_axis_size_and_extra_parameters() -> None:
+ code = _tikz_code(
+ plot_line_and_scatter,
+ axis_width="7cm",
+ axis_height="5cm",
+ extra_axis_parameters=["name=threeDaxis"],
+ )
+
+ assert "width=7cm" in code
+ assert "height=5cm" in code
+ assert "name=threeDaxis" in code
+
+
+def test_3d_without_axis_environment() -> None:
+ code = _tikz_code(plot_line_and_scatter, add_axis_environment=False)
+
+ assert "\\begin{axis}" not in code
+ assert "\\end{axis}" not in code
+ assert "\\addplot3" in code
+
+
+def test_3d_standalone_includes_dynamic_libraries() -> None:
+ code = _tikz_code(plot_surface_and_wireframe, standalone=True)
+
+ assert "\\documentclass{standalone}" in code
+ assert "\\usepgfplotslibrary{patchplots}" in code
+ assert "\\begin{document}" in code
+
+
+def test_3d_externalize_tables() -> None:
+ fig = plot_line_and_scatter()
+ with tempfile.TemporaryDirectory() as tmpdir:
+ filepath = Path(tmpdir) / "plot.tex"
+ matplot2tikz.save(
+ filepath,
+ figure=fig,
+ include_disclaimer=False,
+ externalize_tables=True,
+ float_format=".8g",
+ )
+
+ code = filepath.read_text(encoding="utf-8")
+ table_files = list(Path(tmpdir).glob("plot-*.dat"))
+
+ plt.close("all")
+
+ assert "\\addplot3" in code
+ assert table_files
+ assert "plot-000.dat" in code
+
+
+def test_contour3d_uses_native_coordinates() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.array([0.0, 1.0, 2.0])
+ y = np.array([0.0, 1.0, 2.0])
+ xx, yy = np.meshgrid(x, y)
+ zz = xx + yy
+ ax.contour(xx, yy, zz, levels=[1.0, 2.0, 3.0])
+
+ code = matplot2tikz.get_tikz_code(fig, include_disclaimer=False, float_format=".8g")
+ plt.close("all")
+
+ assert "\\addplot3" in code
+ assert "zmin=" in code
+ assert "0 1 1" in code
+ assert "0 2 2" in code
+
+
+def test_contourf3d_uses_patchplots() -> None:
+ fig = plt.figure()
+ ax = cast("Axes3D", fig.add_subplot(111, projection="3d"))
+ x = np.array([0.0, 1.0, 2.0])
+ y = np.array([0.0, 1.0, 2.0])
+ xx, yy = np.meshgrid(x, y)
+ zz = xx + yy
+ ax.contourf(xx, yy, zz, levels=[1.0, 2.0, 3.0])
+
+ code = matplot2tikz.get_tikz_code(fig, include_disclaimer=False, float_format=".8g")
+ plt.close("all")
+
+ assert "\\addplot3" in code
+ assert "patch table" in code
+
+
+def _tikz_code(plot: Callable[[], Figure], **kwargs: Unpack[_TikzCodeOptions]) -> str:
+ fig = plot()
+ code = matplot2tikz.get_tikz_code(fig, include_disclaimer=False, float_format=".8g", **kwargs)
+ plt.close("all")
+ return code
diff --git a/tests/test_3d_bar3d_reference.tex b/tests/test_3d_bar3d_reference.tex
new file mode 100644
index 00000000..b595d3db
--- /dev/null
+++ b/tests/test_3d_bar3d_reference.tex
@@ -0,0 +1,236 @@
+\begin{tikzpicture}
+
+\definecolor{darkgray176}{RGB}{176,176,176}
+
+\begin{axis}[
+minor xtick={},
+minor ytick={},
+minor ztick={},
+view={125}{26},
+x grid style={darkgray176},
+xlabel={X},
+xmajorgrids,
+xmin=-0.2, xmax=3.05,
+xtick style={color=black},
+xtick={-0.5,0,0.5,1,1.5,2,2.5,3,3.5},
+y grid style={darkgray176},
+ylabel={Y},
+ymajorgrids,
+ymin=-0.2, ymax=1.9,
+ytick style={color=black},
+ytick={-0.5,0,0.5,1,1.5,2},
+z grid style={darkgray176},
+zlabel={Z},
+zmajorgrids,
+zmin=0, zmax=1.7,
+ztick style={color=black},
+ztick={0,0.25,0.5,0.75,1,1.25,1.5,1.75}
+]
+\addplot3 [
+patch,
+patch type=polygon,
+vertex count=4,
+table/row sep=\\,
+z buffer=sort,
+draw=black,
+line width=0.14pt,
+fill opacity=0.82,
+colormap={matplot2tikzpoly0}{[1pt]
+ rgb(0pt)=(0,0.135112,0.304751);
+ rgb(1pt)=(0.601354,0.573743,0.465074);
+ rgb(2pt)=(0.298421,0.332247,0.423973);
+ rgb(3pt)=(0.79848,0.73058,0.392597);
+ rgb(4pt)=(0.130669,0.231458,0.43284);
+ rgb(5pt)=(0.995737,0.909344,0.217772)
+},
+point meta min=0,
+point meta max=5,
+patch table with point meta={%
+0 1 2 3 0\\
+4 5 6 7 0\\
+8 9 10 11 0\\
+12 13 14 15 0\\
+16 17 18 19 0\\
+20 21 22 23 0\\
+24 25 26 27 1\\
+28 29 30 31 1\\
+32 33 34 35 1\\
+36 37 38 39 1\\
+40 41 42 43 1\\
+44 45 46 47 1\\
+48 49 50 51 2\\
+52 53 54 55 2\\
+56 57 58 59 2\\
+60 61 62 63 2\\
+64 65 66 67 2\\
+68 69 70 71 2\\
+72 73 74 75 3\\
+76 77 78 79 3\\
+80 81 82 83 3\\
+84 85 86 87 3\\
+88 89 90 91 3\\
+92 93 94 95 3\\
+96 97 98 99 4\\
+100 101 102 103 4\\
+104 105 106 107 4\\
+108 109 110 111 4\\
+112 113 114 115 4\\
+116 117 118 119 4\\
+120 121 122 123 5\\
+124 125 126 127 5\\
+128 129 130 131 5\\
+132 133 134 135 5\\
+136 137 138 139 5\\
+140 141 142 143 5\\
+}
+]
+table [row sep=\\] {%
+x y z\\
+0 0 0\\
+0 0.55 0\\
+0.55 0.55 0\\
+0.55 0 0\\
+0 0 0.5\\
+0.55 0 0.5\\
+0.55 0.55 0.5\\
+0 0.55 0.5\\
+0 0 0\\
+0.55 0 0\\
+0.55 0 0.5\\
+0 0 0.5\\
+0 0.55 0\\
+0 0.55 0.5\\
+0.55 0.55 0.5\\
+0.55 0.55 0\\
+0 0 0\\
+0 0 0.5\\
+0 0.55 0.5\\
+0 0.55 0\\
+0.55 0 0\\
+0.55 0.55 0\\
+0.55 0.55 0.5\\
+0.55 0 0.5\\
+1.1 0 0\\
+1.1 0.45 0\\
+1.85 0.45 0\\
+1.85 0 0\\
+1.1 0 1.15\\
+1.85 0 1.15\\
+1.85 0.45 1.15\\
+1.1 0.45 1.15\\
+1.1 0 0\\
+1.85 0 0\\
+1.85 0 1.15\\
+1.1 0 1.15\\
+1.1 0.45 0\\
+1.1 0.45 1.15\\
+1.85 0.45 1.15\\
+1.85 0.45 0\\
+1.1 0 0\\
+1.1 0 1.15\\
+1.1 0.45 1.15\\
+1.1 0.45 0\\
+1.85 0 0\\
+1.85 0.45 0\\
+1.85 0.45 1.15\\
+1.85 0 1.15\\
+2.2 0 0\\
+2.2 0.65 0\\
+2.85 0.65 0\\
+2.85 0 0\\
+2.2 0 0.8\\
+2.85 0 0.8\\
+2.85 0.65 0.8\\
+2.2 0.65 0.8\\
+2.2 0 0\\
+2.85 0 0\\
+2.85 0 0.8\\
+2.2 0 0.8\\
+2.2 0.65 0\\
+2.2 0.65 0.8\\
+2.85 0.65 0.8\\
+2.85 0.65 0\\
+2.2 0 0\\
+2.2 0 0.8\\
+2.2 0.65 0.8\\
+2.2 0.65 0\\
+2.85 0 0\\
+2.85 0.65 0\\
+2.85 0.65 0.8\\
+2.85 0 0.8\\
+0 1 0\\
+0 1.75 0\\
+0.55 1.75 0\\
+0.55 1 0\\
+0 1 1.35\\
+0.55 1 1.35\\
+0.55 1.75 1.35\\
+0 1.75 1.35\\
+0 1 0\\
+0.55 1 0\\
+0.55 1 1.35\\
+0 1 1.35\\
+0 1.75 0\\
+0 1.75 1.35\\
+0.55 1.75 1.35\\
+0.55 1.75 0\\
+0 1 0\\
+0 1 1.35\\
+0 1.75 1.35\\
+0 1.75 0\\
+0.55 1 0\\
+0.55 1.75 0\\
+0.55 1.75 1.35\\
+0.55 1 1.35\\
+1.1 1 0\\
+1.1 1.5 0\\
+1.85 1.5 0\\
+1.85 1 0\\
+1.1 1 0.65\\
+1.85 1 0.65\\
+1.85 1.5 0.65\\
+1.1 1.5 0.65\\
+1.1 1 0\\
+1.85 1 0\\
+1.85 1 0.65\\
+1.1 1 0.65\\
+1.1 1.5 0\\
+1.1 1.5 0.65\\
+1.85 1.5 0.65\\
+1.85 1.5 0\\
+1.1 1 0\\
+1.1 1 0.65\\
+1.1 1.5 0.65\\
+1.1 1.5 0\\
+1.85 1 0\\
+1.85 1.5 0\\
+1.85 1.5 0.65\\
+1.85 1 0.65\\
+2.2 1 0\\
+2.2 1.6 0\\
+2.85 1.6 0\\
+2.85 1 0\\
+2.2 1 1.55\\
+2.85 1 1.55\\
+2.85 1.6 1.55\\
+2.2 1.6 1.55\\
+2.2 1 0\\
+2.85 1 0\\
+2.85 1 1.55\\
+2.2 1 1.55\\
+2.2 1.6 0\\
+2.2 1.6 1.55\\
+2.85 1.6 1.55\\
+2.85 1.6 0\\
+2.2 1 0\\
+2.2 1 1.55\\
+2.2 1.6 1.55\\
+2.2 1.6 0\\
+2.85 1 0\\
+2.85 1.6 0\\
+2.85 1.6 1.55\\
+2.85 1 1.55\\
+};
+\end{axis}
+
+\end{tikzpicture}
diff --git a/tests/test_3d_line_and_scatter_reference.tex b/tests/test_3d_line_and_scatter_reference.tex
new file mode 100644
index 00000000..3ee466c1
--- /dev/null
+++ b/tests/test_3d_line_and_scatter_reference.tex
@@ -0,0 +1,105 @@
+\begin{tikzpicture}
+
+\definecolor{crimson2143940}{RGB}{214,39,40}
+\definecolor{darkgray176}{RGB}{176,176,176}
+\definecolor{lightgray204}{RGB}{204,204,204}
+\definecolor{steelblue31119180}{RGB}{31,119,180}
+
+\begin{axis}[
+legend cell align={left},
+legend style={
+ fill opacity=0.8,
+ draw opacity=1,
+ text opacity=1,
+ at={(0.03,0.97)},
+ anchor=north west,
+ draw=lightgray204
+},
+view={128}{28},
+x grid style={darkgray176},
+xlabel={X},
+xmajorgrids,
+xmin=-1.05, xmax=1.05,
+xtick style={color=black},
+y grid style={darkgray176},
+ylabel={Y},
+ymajorgrids,
+ymin=-0.9, ymax=0.9,
+ytick style={color=black},
+z grid style={darkgray176},
+zlabel={Z},
+zmajorgrids,
+zmin=0, zmax=1.6,
+ztick style={color=black}
+]
+\addplot3 [
+ colormap/viridis,
+ draw=black,
+ line width=0.14pt,
+ mark=diamond*,
+ only marks,
+ scatter,
+ scatter src=explicit,
+ scatter/@pre marker code/.append style={/tikz/mark size=\perpointmarksize},
+ visualization depends on={\thisrow{sizedata} \as\perpointmarksize}
+]
+table [x=x, y=y, z=z, meta=colordata]{%
+x y z colordata sizedata
+-0.7 -0.6 0.44324575 0.44324575 1.95441
+-0.23333333 -0.6 0.24724575 0.24724575 2.230966
+0.23333333 -0.6 0.24724575 0.24724575 2.4768326
+0.7 -0.6 0.44324575 0.44324575 2.7004055
+-0.7 0 0.7705 0.7705 2.9068334
+-0.23333333 0 0.5745 0.5745 3.0995437
+0.23333333 0 0.5745 0.5745 3.2809544
+0.7 0 0.7705 0.7705 3.452847
+-0.7 0.6 0.44324575 0.44324575 3.6165789
+-0.23333333 0.6 0.24724575 0.24724575 3.7732126
+0.23333333 0.6 0.24724575 0.24724575 3.9235983
+0.7 0.6 0.44324575 0.44324575 4.0684289
+};
+\addlegendentry{samples}
+\addplot3 [semithick, crimson2143940, mark=*, mark size=3, mark options={solid}]
+table {%
+0.25 0 0
+0.25274034 0.12584973 0.083159806
+0.18963698 0.25111994 0.16631961
+0.063764716 0.3411111 0.24947942
+-0.10381671 0.36487789 0.33263922
+-0.27736024 0.30424969 0.41579903
+-0.41405455 0.16040559 0.49895883
+-0.47434732 -0.043954752 0.58211864
+-0.43252195 -0.26780625 0.66527844
+-0.28483103 -0.4600179 0.74843825
+-0.052906608 -0.57095322 0.83159806
+0.21881823 -0.56483495 0.91475786
+0.47154614 -0.4298711 0.99791767
+0.6448262 -0.18346887 1.0810775
+0.69079313 0.12913162 1.1642373
+0.58662156 0.44299604 1.2473971
+0.3420769 0.68698303 1.3305569
+2.4486161e-16 0.79977871 1.4137167
+};
+\addlegendentry{spiral}
+\addplot3 [semithick, steelblue31119180, dashed, mark=triangle*, mark size=3, mark options={solid}]
+table {%
+-0.8 -0.22732436 0.28757797
+-0.6 -0.24937375 0.36061058
+-0.4 -0.21036775 0.43104535
+-0.2 -0.11985638 0.48163738
+0 0 0.5
+0.2 0.11985638 0.48163738
+0.4 0.21036775 0.43104535
+0.6 0.24937375 0.36061058
+0.8 0.22732436 0.28757797
+};
+\addlegendentry{ridge}
+\draw (axis cs:0.28,-0.72,1.18) node[
+ scale=0.5,
+ anchor=base west,
+ text=black,
+ rotate=0.0
+]{3D text};
+\end{axis}
+
+\end{tikzpicture}
diff --git a/tests/test_3d_quiver_reference.tex b/tests/test_3d_quiver_reference.tex
new file mode 100644
index 00000000..25479a39
--- /dev/null
+++ b/tests/test_3d_quiver_reference.tex
@@ -0,0 +1,34 @@
+\begin{tikzpicture}
+
+\definecolor{darkgray176}{RGB}{176,176,176}
+\definecolor{forestgreen4416044}{RGB}{44,160,44}
+
+\begin{axis}[
+view={45}{30},
+x grid style={darkgray176},
+xlabel={X},
+xmajorgrids,
+xmin=-0.1, xmax=1.1,
+xtick style={color=black},
+y grid style={darkgray176},
+ylabel={Y},
+ymajorgrids,
+ymin=-0.1, ymax=1.1,
+ytick style={color=black},
+z grid style={darkgray176},
+zlabel={Z},
+zmajorgrids,
+zmin=0, zmax=0.8,
+ztick style={color=black}
+]
+\addplot3 [draw=forestgreen4416044, line width=0.44pt, -latex, mark=none, quiver={u=\thisrow{u}, v=\thisrow{v}, w=\thisrow{w}}]
+table [x=x,y=y,z=z] {%
+x y z u v w
+0 0 0 0.36 0.12 0.28
+0.9 0 0.2 -0.16 0.28 0.2
+0 0.8 0.1 0.2 -0.2 0.36
+0.9 0.8 0.3 -0.24 -0.16 0.16
+};
+\end{axis}
+
+\end{tikzpicture}
diff --git a/tests/test_3d_surface_and_wireframe_reference.tex b/tests/test_3d_surface_and_wireframe_reference.tex
new file mode 100644
index 00000000..fd50cba8
--- /dev/null
+++ b/tests/test_3d_surface_and_wireframe_reference.tex
@@ -0,0 +1,2854 @@
+\begin{tikzpicture}
+
+\definecolor{crimson}{RGB}{220,20,60}
+\definecolor{darkcyan32144140}{RGB}{32,144,140}
+\definecolor{darkgray176}{RGB}{176,176,176}
+\definecolor{darkorange}{RGB}{255,140,0}
+\definecolor{gold25323136}{RGB}{253,231,36}
+\definecolor{indigo68184}{RGB}{68,1,84}
+\definecolor{navy}{RGB}{0,0,128}
+
+\begin{axis}[
+colormap/viridis,
+view={35}{24},
+x grid style={darkgray176},
+xlabel={X},
+xmajorgrids,
+xmin=-5, xmax=5,
+xtick style={color=black},
+y grid style={darkgray176},
+ylabel={Y},
+ymajorgrids,
+ymin=-5, ymax=5,
+ytick style={color=black},
+z grid style={darkgray176},
+zlabel={Z},
+zmajorgrids,
+zmin=-1, zmax=1,
+ztick style={color=black}
+]
+\addplot3 [
+patch,
+patch type=polygon,
+vertex count=4,
+table/row sep=\\,
+z buffer=sort,
+draw=black,
+ultra thin,
+fill opacity=0.88,
+patch table with point meta={%
+0 1 2 3 0.41199627\\
+4 5 6 7 0.092542463\\
+8 9 10 11 -0.21715694\\
+12 13 14 15 -0.48333\\
+16 17 18 19 -0.68749463\\
+20 21 22 23 -0.8263122\\
+24 25 26 27 -0.90826274\\
+28 29 30 31 -0.9485692\\
+32 33 34 35 -0.96386734\\
+36 37 38 39 -0.96774984\\
+40 41 42 43 -0.96774984\\
+44 45 46 47 -0.96386734\\
+48 49 50 51 -0.9485692\\
+52 53 54 55 -0.90826274\\
+56 57 58 59 -0.8263122\\
+60 61 62 63 -0.68749463\\
+64 65 66 67 -0.48333\\
+68 69 70 71 -0.21715694\\
+72 73 74 75 0.092542463\\
+76 77 78 79 0.092542463\\
+80 81 82 83 -0.25606586\\
+84 85 86 87 -0.55483353\\
+88 89 90 91 -0.77503684\\
+92 93 94 95 -0.9082642\\
+96 97 98 99 -0.96389031\\
+100 101 102 103 -0.96324671\\
+104 105 106 107 -0.93254602\\
+108 109 110 111 -0.89646263\\
+112 113 114 115 -0.8735261\\
+116 117 118 119 -0.8735261\\
+120 121 122 123 -0.89646263\\
+124 125 126 127 -0.93254602\\
+128 129 130 131 -0.96324671\\
+132 133 134 135 -0.96389031\\
+136 137 138 139 -0.9082642\\
+140 141 142 143 -0.77503684\\
+144 145 146 147 -0.55483353\\
+148 149 150 151 -0.25606586\\
+152 153 154 155 -0.21715694\\
+156 157 158 159 -0.55483353\\
+160 161 162 163 -0.80144326\\
+164 165 166 167 -0.93736086\\
+168 169 170 171 -0.96757113\\
+172 173 174 175 -0.91616957\\
+176 177 178 179 -0.81768027\\
+180 181 182 183 -0.70801955\\
+184 185 186 187 -0.6175063\\
+188 189 190 191 -0.56704906\\
+192 193 194 195 -0.56704906\\
+196 197 198 199 -0.6175063\\
+200 201 202 203 -0.70801955\\
+204 205 206 207 -0.81768027\\
+208 209 210 211 -0.91616957\\
+212 213 214 215 -0.96757113\\
+216 217 218 219 -0.93736086\\
+220 221 222 223 -0.80144326\\
+224 225 226 227 -0.55483353\\
+228 229 230 231 -0.48333\\
+232 233 234 235 -0.77503684\\
+236 237 238 239 -0.93736086\\
+240 241 242 243 -0.96328094\\
+244 245 246 247 -0.87363262\\
+248 249 250 251 -0.70809505\\
+252 253 254 255 -0.51331544\\
+256 257 258 259 -0.33193732\\
+260 261 262 263 -0.19563651\\
+264 265 266 267 -0.12336256\\
+268 269 270 271 -0.12336256\\
+272 273 274 275 -0.19563651\\
+276 277 278 279 -0.33193732\\
+280 281 282 283 -0.51331544\\
+284 285 286 287 -0.70809505\\
+288 289 290 291 -0.87363262\\
+292 293 294 295 -0.96328094\\
+296 297 298 299 -0.93736086\\
+300 301 302 303 -0.77503684\\
+304 305 306 307 -0.68749463\\
+308 309 310 311 -0.9082642\\
+312 313 314 315 -0.96757113\\
+316 317 318 319 -0.87363262\\
+320 321 322 323 -0.66461695\\
+324 325 326 327 -0.39568336\\
+328 329 330 331 -0.12353598\\
+332 333 334 335 0.10666798\\
+336 337 338 339 0.26788122\\
+340 341 342 343 0.34943162\\
+344 345 346 347 0.34943162\\
+348 349 350 351 0.26788122\\
+352 353 354 355 0.10666798\\
+356 357 358 359 -0.12353598\\
+360 361 362 363 -0.39568336\\
+364 365 366 367 -0.66461695\\
+368 369 370 371 -0.87363262\\
+372 373 374 375 -0.96757113\\
+376 377 378 379 -0.9082642\\
+380 381 382 383 -0.8263122\\
+384 385 386 387 -0.96389031\\
+388 389 390 391 -0.91616957\\
+392 393 394 395 -0.70809505\\
+396 397 398 399 -0.39568336\\
+400 401 402 403 -0.048906516\\
+404 405 406 407 0.26770129\\
+408 409 410 411 0.51087254\\
+412 413 414 415 0.6652226\\
+416 417 418 419 0.73695918\\
+420 421 422 423 0.73695918\\
+424 425 426 427 0.6652226\\
+428 429 430 431 0.51087254\\
+432 433 434 435 0.26770129\\
+436 437 438 439 -0.048906516\\
+440 441 442 443 -0.39568336\\
+444 445 446 447 -0.70809505\\
+448 449 450 451 -0.91616957\\
+452 453 454 455 -0.96389031\\
+456 457 458 459 -0.90826274\\
+460 461 462 463 -0.96324671\\
+464 465 466 467 -0.81768027\\
+468 469 470 471 -0.51331544\\
+472 473 474 475 -0.12353598\\
+476 477 478 479 0.26770129\\
+480 481 482 483 0.58931663\\
+484 485 486 487 0.80267126\\
+488 489 490 491 0.91047137\\
+492 493 494 495 0.94717888\\
+496 497 498 499 0.94717888\\
+500 501 502 503 0.91047137\\
+504 505 506 507 0.80267126\\
+508 509 510 511 0.58931663\\
+512 513 514 515 0.26770129\\
+516 517 518 519 -0.12353598\\
+520 521 522 523 -0.51331544\\
+524 525 526 527 -0.81768027\\
+528 529 530 531 -0.96324671\\
+532 533 534 535 -0.9485692\\
+536 537 538 539 -0.93254602\\
+540 541 542 543 -0.70801955\\
+544 545 546 547 -0.33193732\\
+548 549 550 551 0.10666798\\
+552 553 554 555 0.51087254\\
+556 557 558 559 0.80267126\\
+560 561 562 563 0.94661074\\
+564 565 566 567 0.96504196\\
+568 569 570 571 0.93453866\\
+572 573 574 575 0.93453866\\
+576 577 578 579 0.96504196\\
+580 581 582 583 0.94661074\\
+584 585 586 587 0.80267126\\
+588 589 590 591 0.51087254\\
+592 593 594 595 0.10666798\\
+596 597 598 599 -0.33193732\\
+600 601 602 603 -0.70801955\\
+604 605 606 607 -0.93254602\\
+608 609 610 611 -0.96386734\\
+612 613 614 615 -0.89646263\\
+616 617 618 619 -0.6175063\\
+620 621 622 623 -0.19563651\\
+624 625 626 627 0.26788122\\
+628 629 630 631 0.6652226\\
+632 633 634 635 0.91047137\\
+636 637 638 639 0.96504196\\
+640 641 642 643 0.85897179\\
+644 645 646 647 0.7174439\\
+648 649 650 651 0.7174439\\
+652 653 654 655 0.85897179\\
+656 657 658 659 0.96504196\\
+660 661 662 663 0.91047137\\
+664 665 666 667 0.6652226\\
+668 669 670 671 0.26788122\\
+672 673 674 675 -0.19563651\\
+676 677 678 679 -0.6175063\\
+680 681 682 683 -0.89646263\\
+684 685 686 687 -0.96774984\\
+688 689 690 691 -0.8735261\\
+692 693 694 695 -0.56704906\\
+696 697 698 699 -0.12336256\\
+700 701 702 703 0.34943162\\
+704 705 706 707 0.73695918\\
+708 709 710 711 0.94717888\\
+712 713 714 715 0.93453866\\
+716 717 718 719 0.7174439\\
+720 721 722 723 0.402122\\
+724 725 726 727 0.402122\\
+728 729 730 731 0.7174439\\
+732 733 734 735 0.93453866\\
+736 737 738 739 0.94717888\\
+740 741 742 743 0.73695918\\
+744 745 746 747 0.34943162\\
+748 749 750 751 -0.12336256\\
+752 753 754 755 -0.56704906\\
+756 757 758 759 -0.8735261\\
+760 761 762 763 -0.96774984\\
+764 765 766 767 -0.8735261\\
+768 769 770 771 -0.56704906\\
+772 773 774 775 -0.12336256\\
+776 777 778 779 0.34943162\\
+780 781 782 783 0.73695918\\
+784 785 786 787 0.94717888\\
+788 789 790 791 0.93453866\\
+792 793 794 795 0.7174439\\
+796 797 798 799 0.402122\\
+800 801 802 803 0.402122\\
+804 805 806 807 0.7174439\\
+808 809 810 811 0.93453866\\
+812 813 814 815 0.94717888\\
+816 817 818 819 0.73695918\\
+820 821 822 823 0.34943162\\
+824 825 826 827 -0.12336256\\
+828 829 830 831 -0.56704906\\
+832 833 834 835 -0.8735261\\
+836 837 838 839 -0.96386734\\
+840 841 842 843 -0.89646263\\
+844 845 846 847 -0.6175063\\
+848 849 850 851 -0.19563651\\
+852 853 854 855 0.26788122\\
+856 857 858 859 0.6652226\\
+860 861 862 863 0.91047137\\
+864 865 866 867 0.96504196\\
+868 869 870 871 0.85897179\\
+872 873 874 875 0.7174439\\
+876 877 878 879 0.7174439\\
+880 881 882 883 0.85897179\\
+884 885 886 887 0.96504196\\
+888 889 890 891 0.91047137\\
+892 893 894 895 0.6652226\\
+896 897 898 899 0.26788122\\
+900 901 902 903 -0.19563651\\
+904 905 906 907 -0.6175063\\
+908 909 910 911 -0.89646263\\
+912 913 914 915 -0.9485692\\
+916 917 918 919 -0.93254602\\
+920 921 922 923 -0.70801955\\
+924 925 926 927 -0.33193732\\
+928 929 930 931 0.10666798\\
+932 933 934 935 0.51087254\\
+936 937 938 939 0.80267126\\
+940 941 942 943 0.94661074\\
+944 945 946 947 0.96504196\\
+948 949 950 951 0.93453866\\
+952 953 954 955 0.93453866\\
+956 957 958 959 0.96504196\\
+960 961 962 963 0.94661074\\
+964 965 966 967 0.80267126\\
+968 969 970 971 0.51087254\\
+972 973 974 975 0.10666798\\
+976 977 978 979 -0.33193732\\
+980 981 982 983 -0.70801955\\
+984 985 986 987 -0.93254602\\
+988 989 990 991 -0.90826274\\
+992 993 994 995 -0.96324671\\
+996 997 998 999 -0.81768027\\
+1000 1001 1002 1003 -0.51331544\\
+1004 1005 1006 1007 -0.12353598\\
+1008 1009 1010 1011 0.26770129\\
+1012 1013 1014 1015 0.58931663\\
+1016 1017 1018 1019 0.80267126\\
+1020 1021 1022 1023 0.91047137\\
+1024 1025 1026 1027 0.94717888\\
+1028 1029 1030 1031 0.94717888\\
+1032 1033 1034 1035 0.91047137\\
+1036 1037 1038 1039 0.80267126\\
+1040 1041 1042 1043 0.58931663\\
+1044 1045 1046 1047 0.26770129\\
+1048 1049 1050 1051 -0.12353598\\
+1052 1053 1054 1055 -0.51331544\\
+1056 1057 1058 1059 -0.81768027\\
+1060 1061 1062 1063 -0.96324671\\
+1064 1065 1066 1067 -0.8263122\\
+1068 1069 1070 1071 -0.96389031\\
+1072 1073 1074 1075 -0.91616957\\
+1076 1077 1078 1079 -0.70809505\\
+1080 1081 1082 1083 -0.39568336\\
+1084 1085 1086 1087 -0.048906516\\
+1088 1089 1090 1091 0.26770129\\
+1092 1093 1094 1095 0.51087254\\
+1096 1097 1098 1099 0.6652226\\
+1100 1101 1102 1103 0.73695918\\
+1104 1105 1106 1107 0.73695918\\
+1108 1109 1110 1111 0.6652226\\
+1112 1113 1114 1115 0.51087254\\
+1116 1117 1118 1119 0.26770129\\
+1120 1121 1122 1123 -0.048906516\\
+1124 1125 1126 1127 -0.39568336\\
+1128 1129 1130 1131 -0.70809505\\
+1132 1133 1134 1135 -0.91616957\\
+1136 1137 1138 1139 -0.96389031\\
+1140 1141 1142 1143 -0.68749463\\
+1144 1145 1146 1147 -0.9082642\\
+1148 1149 1150 1151 -0.96757113\\
+1152 1153 1154 1155 -0.87363262\\
+1156 1157 1158 1159 -0.66461695\\
+1160 1161 1162 1163 -0.39568336\\
+1164 1165 1166 1167 -0.12353598\\
+1168 1169 1170 1171 0.10666798\\
+1172 1173 1174 1175 0.26788122\\
+1176 1177 1178 1179 0.34943162\\
+1180 1181 1182 1183 0.34943162\\
+1184 1185 1186 1187 0.26788122\\
+1188 1189 1190 1191 0.10666798\\
+1192 1193 1194 1195 -0.12353598\\
+1196 1197 1198 1199 -0.39568336\\
+1200 1201 1202 1203 -0.66461695\\
+1204 1205 1206 1207 -0.87363262\\
+1208 1209 1210 1211 -0.96757113\\
+1212 1213 1214 1215 -0.9082642\\
+1216 1217 1218 1219 -0.48333\\
+1220 1221 1222 1223 -0.77503684\\
+1224 1225 1226 1227 -0.93736086\\
+1228 1229 1230 1231 -0.96328094\\
+1232 1233 1234 1235 -0.87363262\\
+1236 1237 1238 1239 -0.70809505\\
+1240 1241 1242 1243 -0.51331544\\
+1244 1245 1246 1247 -0.33193732\\
+1248 1249 1250 1251 -0.19563651\\
+1252 1253 1254 1255 -0.12336256\\
+1256 1257 1258 1259 -0.12336256\\
+1260 1261 1262 1263 -0.19563651\\
+1264 1265 1266 1267 -0.33193732\\
+1268 1269 1270 1271 -0.51331544\\
+1272 1273 1274 1275 -0.70809505\\
+1276 1277 1278 1279 -0.87363262\\
+1280 1281 1282 1283 -0.96328094\\
+1284 1285 1286 1287 -0.93736086\\
+1288 1289 1290 1291 -0.77503684\\
+1292 1293 1294 1295 -0.21715694\\
+1296 1297 1298 1299 -0.55483353\\
+1300 1301 1302 1303 -0.80144326\\
+1304 1305 1306 1307 -0.93736086\\
+1308 1309 1310 1311 -0.96757113\\
+1312 1313 1314 1315 -0.91616957\\
+1316 1317 1318 1319 -0.81768027\\
+1320 1321 1322 1323 -0.70801955\\
+1324 1325 1326 1327 -0.6175063\\
+1328 1329 1330 1331 -0.56704906\\
+1332 1333 1334 1335 -0.56704906\\
+1336 1337 1338 1339 -0.6175063\\
+1340 1341 1342 1343 -0.70801955\\
+1344 1345 1346 1347 -0.81768027\\
+1348 1349 1350 1351 -0.91616957\\
+1352 1353 1354 1355 -0.96757113\\
+1356 1357 1358 1359 -0.93736086\\
+1360 1361 1362 1363 -0.80144326\\
+1364 1365 1366 1367 -0.55483353\\
+1368 1369 1370 1371 0.092542463\\
+1372 1373 1374 1375 -0.25606586\\
+1376 1377 1378 1379 -0.55483353\\
+1380 1381 1382 1383 -0.77503684\\
+1384 1385 1386 1387 -0.9082642\\
+1388 1389 1390 1391 -0.96389031\\
+1392 1393 1394 1395 -0.96324671\\
+1396 1397 1398 1399 -0.93254602\\
+1400 1401 1402 1403 -0.89646263\\
+1404 1405 1406 1407 -0.8735261\\
+1408 1409 1410 1411 -0.8735261\\
+1412 1413 1414 1415 -0.89646263\\
+1416 1417 1418 1419 -0.93254602\\
+1420 1421 1422 1423 -0.96324671\\
+1424 1425 1426 1427 -0.96389031\\
+1428 1429 1430 1431 -0.9082642\\
+1432 1433 1434 1435 -0.77503684\\
+1436 1437 1438 1439 -0.55483353\\
+1440 1441 1442 1443 -0.25606586\\
+}
+]
+table [row sep=\\] {%
+x y z\\
+-5 -5 0.70886129\\
+-4.5 -5 0.42921793\\
+-4.5 -4.5 0.080687912\\
+-5 -4.5 0.42921793\\
+-4.5 -5 0.42921793\\
+-4 -5 0.11965158\\
+-4 -4.5 -0.25938757\\
+-4.5 -4.5 0.080687912\\
+-4 -5 0.11965158\\
+-3.5 -5 -0.17893857\\
+-3.5 -4.5 -0.54995318\\
+-4 -4.5 -0.25938757\\
+-3.5 -5 -0.17893857\\
+-3 -5 -0.43697552\\
+-3 -4.5 -0.76745273\\
+-3.5 -4.5 -0.54995318\\
+-3 -5 -0.43697552\\
+-2.5 -5 -0.63885987\\
+-2.5 -4.5 -0.9066904\\
+-3 -4.5 -0.76745273\\
+-2.5 -5 -0.63885987\\
+-2 -5 -0.7820949\\
+-2 -4.5 -0.97760364\\
+-2.5 -4.5 -0.9066904\\
+-2 -5 -0.7820949\\
+-1.5 -5 -0.87383376\\
+-1.5 -4.5 -0.99951869\\
+-2 -4.5 -0.97760364\\
+-1.5 -5 -0.87383376\\
+-1 -5 -0.92618484\\
+-1 -4.5 -0.99473952\\
+-1.5 -4.5 -0.99951869\\
+-1 -5 -0.92618484\\
+-0.5 -5 -0.95155293\\
+-0.5 -4.5 -0.98299205\\
+-1 -4.5 -0.99473952\\
+-0.5 -5 -0.95155293\\
+0 -5 -0.95892427\\
+0 -4.5 -0.97753012\\
+-0.5 -4.5 -0.98299205\\
+0 -5 -0.95892427\\
+0.5 -5 -0.95155293\\
+0.5 -4.5 -0.98299205\\
+0 -4.5 -0.97753012\\
+0.5 -5 -0.95155293\\
+1 -5 -0.92618484\\
+1 -4.5 -0.99473952\\
+0.5 -4.5 -0.98299205\\
+1 -5 -0.92618484\\
+1.5 -5 -0.87383376\\
+1.5 -4.5 -0.99951869\\
+1 -4.5 -0.99473952\\
+1.5 -5 -0.87383376\\
+2 -5 -0.7820949\\
+2 -4.5 -0.97760364\\
+1.5 -4.5 -0.99951869\\
+2 -5 -0.7820949\\
+2.5 -5 -0.63885987\\
+2.5 -4.5 -0.9066904\\
+2 -4.5 -0.97760364\\
+2.5 -5 -0.63885987\\
+3 -5 -0.43697552\\
+3 -4.5 -0.76745273\\
+2.5 -4.5 -0.9066904\\
+3 -5 -0.43697552\\
+3.5 -5 -0.17893857\\
+3.5 -4.5 -0.54995318\\
+3 -4.5 -0.76745273\\
+3.5 -5 -0.17893857\\
+4 -5 0.11965158\\
+4 -4.5 -0.25938757\\
+3.5 -4.5 -0.54995318\\
+4 -5 0.11965158\\
+4.5 -5 0.42921793\\
+4.5 -4.5 0.080687912\\
+4 -4.5 -0.25938757\\
+-5 -4.5 0.42921793\\
+-4.5 -4.5 0.080687912\\
+-4.5 -4 -0.25938757\\
+-5 -4 0.11965158\\
+-4.5 -4.5 0.080687912\\
+-4 -4.5 -0.25938757\\
+-4 -4 -0.58617619\\
+-4.5 -4 -0.25938757\\
+-4 -4.5 -0.25938757\\
+-3.5 -4.5 -0.54995318\\
+-3.5 -4 -0.82381719\\
+-4 -4 -0.58617619\\
+-3.5 -4.5 -0.54995318\\
+-3 -4.5 -0.76745273\\
+-3 -4 -0.95892427\\
+-3.5 -4 -0.82381719\\
+-3 -4.5 -0.76745273\\
+-2.5 -4.5 -0.9066904\\
+-2.5 -4 -0.99998941\\
+-3 -4 -0.95892427\\
+-2.5 -4.5 -0.9066904\\
+-2 -4.5 -0.97760364\\
+-2 -4 -0.9712778\\
+-2.5 -4 -0.99998941\\
+-2 -4.5 -0.97760364\\
+-1.5 -4.5 -0.99951869\\
+-1.5 -4 -0.90458671\\
+-2 -4 -0.9712778\\
+-1.5 -4.5 -0.99951869\\
+-1 -4.5 -0.99473952\\
+-1 -4 -0.83133918\\
+-1.5 -4 -0.90458671\\
+-1 -4.5 -0.99473952\\
+-0.5 -4.5 -0.98299205\\
+-0.5 -4 -0.77677976\\
+-1 -4 -0.83133918\\
+-0.5 -4.5 -0.98299205\\
+0 -4.5 -0.97753012\\
+0 -4 -0.7568025\\
+-0.5 -4 -0.77677976\\
+0 -4.5 -0.97753012\\
+0.5 -4.5 -0.98299205\\
+0.5 -4 -0.77677976\\
+0 -4 -0.7568025\\
+0.5 -4.5 -0.98299205\\
+1 -4.5 -0.99473952\\
+1 -4 -0.83133918\\
+0.5 -4 -0.77677976\\
+1 -4.5 -0.99473952\\
+1.5 -4.5 -0.99951869\\
+1.5 -4 -0.90458671\\
+1 -4 -0.83133918\\
+1.5 -4.5 -0.99951869\\
+2 -4.5 -0.97760364\\
+2 -4 -0.9712778\\
+1.5 -4 -0.90458671\\
+2 -4.5 -0.97760364\\
+2.5 -4.5 -0.9066904\\
+2.5 -4 -0.99998941\\
+2 -4 -0.9712778\\
+2.5 -4.5 -0.9066904\\
+3 -4.5 -0.76745273\\
+3 -4 -0.95892427\\
+2.5 -4 -0.99998941\\
+3 -4.5 -0.76745273\\
+3.5 -4.5 -0.54995318\\
+3.5 -4 -0.82381719\\
+3 -4 -0.95892427\\
+3.5 -4.5 -0.54995318\\
+4 -4.5 -0.25938757\\
+4 -4 -0.58617619\\
+3.5 -4 -0.82381719\\
+4 -4.5 -0.25938757\\
+4.5 -4.5 0.080687912\\
+4.5 -4 -0.25938757\\
+4 -4 -0.58617619\\
+-5 -4 0.11965158\\
+-4.5 -4 -0.25938757\\
+-4.5 -3.5 -0.54995318\\
+-5 -3.5 -0.17893857\\
+-4.5 -4 -0.25938757\\
+-4 -4 -0.58617619\\
+-4 -3.5 -0.82381719\\
+-4.5 -3.5 -0.54995318\\
+-4 -4 -0.58617619\\
+-3.5 -4 -0.82381719\\
+-3.5 -3.5 -0.97196248\\
+-4 -3.5 -0.82381719\\
+-3.5 -4 -0.82381719\\
+-3 -4 -0.95892427\\
+-3 -3.5 -0.99473952\\
+-3.5 -3.5 -0.97196248\\
+-3 -4 -0.95892427\\
+-2.5 -4 -0.99998941\\
+-2.5 -3.5 -0.9166313\\
+-3 -3.5 -0.99473952\\
+-2.5 -4 -0.99998941\\
+-2 -4 -0.9712778\\
+-2 -3.5 -0.77677976\\
+-2.5 -3.5 -0.9166313\\
+-2 -4 -0.9712778\\
+-1.5 -4 -0.90458671\\
+-1.5 -3.5 -0.61807681\\
+-2 -3.5 -0.77677976\\
+-1.5 -4 -0.90458671\\
+-1 -4 -0.83133918\\
+-1 -3.5 -0.47807551\\
+-1.5 -3.5 -0.61807681\\
+-1 -4 -0.83133918\\
+-0.5 -4 -0.77677976\\
+-0.5 -3.5 -0.38383075\\
+-1 -3.5 -0.47807551\\
+-0.5 -4 -0.77677976\\
+0 -4 -0.7568025\\
+0 -3.5 -0.35078323\\
+-0.5 -3.5 -0.38383075\\
+0 -4 -0.7568025\\
+0.5 -4 -0.77677976\\
+0.5 -3.5 -0.38383075\\
+0 -3.5 -0.35078323\\
+0.5 -4 -0.77677976\\
+1 -4 -0.83133918\\
+1 -3.5 -0.47807551\\
+0.5 -3.5 -0.38383075\\
+1 -4 -0.83133918\\
+1.5 -4 -0.90458671\\
+1.5 -3.5 -0.61807681\\
+1 -3.5 -0.47807551\\
+1.5 -4 -0.90458671\\
+2 -4 -0.9712778\\
+2 -3.5 -0.77677976\\
+1.5 -3.5 -0.61807681\\
+2 -4 -0.9712778\\
+2.5 -4 -0.99998941\\
+2.5 -3.5 -0.9166313\\
+2 -3.5 -0.77677976\\
+2.5 -4 -0.99998941\\
+3 -4 -0.95892427\\
+3 -3.5 -0.99473952\\
+2.5 -3.5 -0.9166313\\
+3 -4 -0.95892427\\
+3.5 -4 -0.82381719\\
+3.5 -3.5 -0.97196248\\
+3 -3.5 -0.99473952\\
+3.5 -4 -0.82381719\\
+4 -4 -0.58617619\\
+4 -3.5 -0.82381719\\
+3.5 -3.5 -0.97196248\\
+4 -4 -0.58617619\\
+4.5 -4 -0.25938757\\
+4.5 -3.5 -0.54995318\\
+4 -3.5 -0.82381719\\
+-5 -3.5 -0.17893857\\
+-4.5 -3.5 -0.54995318\\
+-4.5 -3 -0.76745273\\
+-5 -3 -0.43697552\\
+-4.5 -3.5 -0.54995318\\
+-4 -3.5 -0.82381719\\
+-4 -3 -0.95892427\\
+-4.5 -3 -0.76745273\\
+-4 -3.5 -0.82381719\\
+-3.5 -3.5 -0.97196248\\
+-3.5 -3 -0.99473952\\
+-4 -3 -0.95892427\\
+-3.5 -3.5 -0.97196248\\
+-3 -3.5 -0.99473952\\
+-3 -3 -0.89168225\\
+-3.5 -3 -0.99473952\\
+-3 -3.5 -0.99473952\\
+-2.5 -3.5 -0.9166313\\
+-2.5 -3 -0.6914774\\
+-3 -3 -0.89168225\\
+-2.5 -3.5 -0.9166313\\
+-2 -3.5 -0.77677976\\
+-2 -3 -0.44749175\\
+-2.5 -3 -0.6914774\\
+-2 -3.5 -0.77677976\\
+-1.5 -3.5 -0.61807681\\
+-1.5 -3 -0.21091343\\
+-2 -3 -0.44749175\\
+-1.5 -3.5 -0.61807681\\
+-1 -3.5 -0.47807551\\
+-1 -3 -0.020683532\\
+-1.5 -3 -0.21091343\\
+-1 -3.5 -0.47807551\\
+-0.5 -3.5 -0.38383075\\
+-0.5 -3 0.10004375\\
+-1 -3 -0.020683532\\
+-0.5 -3.5 -0.38383075\\
+0 -3.5 -0.35078323\\
+0 -3 0.14112001\\
+-0.5 -3 0.10004375\\
+0 -3.5 -0.35078323\\
+0.5 -3.5 -0.38383075\\
+0.5 -3 0.10004375\\
+0 -3 0.14112001\\
+0.5 -3.5 -0.38383075\\
+1 -3.5 -0.47807551\\
+1 -3 -0.020683532\\
+0.5 -3 0.10004375\\
+1 -3.5 -0.47807551\\
+1.5 -3.5 -0.61807681\\
+1.5 -3 -0.21091343\\
+1 -3 -0.020683532\\
+1.5 -3.5 -0.61807681\\
+2 -3.5 -0.77677976\\
+2 -3 -0.44749175\\
+1.5 -3 -0.21091343\\
+2 -3.5 -0.77677976\\
+2.5 -3.5 -0.9166313\\
+2.5 -3 -0.6914774\\
+2 -3 -0.44749175\\
+2.5 -3.5 -0.9166313\\
+3 -3.5 -0.99473952\\
+3 -3 -0.89168225\\
+2.5 -3 -0.6914774\\
+3 -3.5 -0.99473952\\
+3.5 -3.5 -0.97196248\\
+3.5 -3 -0.99473952\\
+3 -3 -0.89168225\\
+3.5 -3.5 -0.97196248\\
+4 -3.5 -0.82381719\\
+4 -3 -0.95892427\\
+3.5 -3 -0.99473952\\
+4 -3.5 -0.82381719\\
+4.5 -3.5 -0.54995318\\
+4.5 -3 -0.76745273\\
+4 -3 -0.95892427\\
+-5 -3 -0.43697552\\
+-4.5 -3 -0.76745273\\
+-4.5 -2.5 -0.9066904\\
+-5 -2.5 -0.63885987\\
+-4.5 -3 -0.76745273\\
+-4 -3 -0.95892427\\
+-4 -2.5 -0.99998941\\
+-4.5 -2.5 -0.9066904\\
+-4 -3 -0.95892427\\
+-3.5 -3 -0.99473952\\
+-3.5 -2.5 -0.9166313\\
+-4 -2.5 -0.99998941\\
+-3.5 -3 -0.99473952\\
+-3 -3 -0.89168225\\
+-3 -2.5 -0.6914774\\
+-3.5 -2.5 -0.9166313\\
+-3 -3 -0.89168225\\
+-2.5 -3 -0.6914774\\
+-2.5 -2.5 -0.38383075\\
+-3 -2.5 -0.6914774\\
+-2.5 -3 -0.6914774\\
+-2 -3 -0.44749175\\
+-2 -2.5 -0.059933527\\
+-2.5 -2.5 -0.38383075\\
+-2 -3 -0.44749175\\
+-1.5 -3 -0.21091343\\
+-1.5 -2.5 0.22419478\\
+-2 -2.5 -0.059933527\\
+-1.5 -3 -0.21091343\\
+-1 -3 -0.020683532\\
+-1 -2.5 0.4340741\\
+-1.5 -2.5 0.22419478\\
+-1 -3 -0.020683532\\
+-0.5 -3 0.10004375\\
+-0.5 -2.5 0.55809058\\
+-1 -2.5 0.4340741\\
+-0.5 -3 0.10004375\\
+0 -3 0.14112001\\
+0 -2.5 0.59847214\\
+-0.5 -2.5 0.55809058\\
+0 -3 0.14112001\\
+0.5 -3 0.10004375\\
+0.5 -2.5 0.55809058\\
+0 -2.5 0.59847214\\
+0.5 -3 0.10004375\\
+1 -3 -0.020683532\\
+1 -2.5 0.4340741\\
+0.5 -2.5 0.55809058\\
+1 -3 -0.020683532\\
+1.5 -3 -0.21091343\\
+1.5 -2.5 0.22419478\\
+1 -2.5 0.4340741\\
+1.5 -3 -0.21091343\\
+2 -3 -0.44749175\\
+2 -2.5 -0.059933527\\
+1.5 -2.5 0.22419478\\
+2 -3 -0.44749175\\
+2.5 -3 -0.6914774\\
+2.5 -2.5 -0.38383075\\
+2 -2.5 -0.059933527\\
+2.5 -3 -0.6914774\\
+3 -3 -0.89168225\\
+3 -2.5 -0.6914774\\
+2.5 -2.5 -0.38383075\\
+3 -3 -0.89168225\\
+3.5 -3 -0.99473952\\
+3.5 -2.5 -0.9166313\\
+3 -2.5 -0.6914774\\
+3.5 -3 -0.99473952\\
+4 -3 -0.95892427\\
+4 -2.5 -0.99998941\\
+3.5 -2.5 -0.9166313\\
+4 -3 -0.95892427\\
+4.5 -3 -0.76745273\\
+4.5 -2.5 -0.9066904\\
+4 -2.5 -0.99998941\\
+-5 -2.5 -0.63885987\\
+-4.5 -2.5 -0.9066904\\
+-4.5 -2 -0.97760364\\
+-5 -2 -0.7820949\\
+-4.5 -2.5 -0.9066904\\
+-4 -2.5 -0.99998941\\
+-4 -2 -0.9712778\\
+-4.5 -2 -0.97760364\\
+-4 -2.5 -0.99998941\\
+-3.5 -2.5 -0.9166313\\
+-3.5 -2 -0.77677976\\
+-4 -2 -0.9712778\\
+-3.5 -2.5 -0.9166313\\
+-3 -2.5 -0.6914774\\
+-3 -2 -0.44749175\\
+-3.5 -2 -0.77677976\\
+-3 -2.5 -0.6914774\\
+-2.5 -2.5 -0.38383075\\
+-2.5 -2 -0.059933527\\
+-3 -2 -0.44749175\\
+-2.5 -2.5 -0.38383075\\
+-2 -2.5 -0.059933527\\
+-2 -2 0.30807174\\
+-2.5 -2 -0.059933527\\
+-2 -2.5 -0.059933527\\
+-1.5 -2.5 0.22419478\\
+-1.5 -2 0.59847214\\
+-2 -2 0.30807174\\
+-1.5 -2.5 0.22419478\\
+-1 -2.5 0.4340741\\
+-1 -2 0.78674913\\
+-1.5 -2 0.59847214\\
+-1 -2.5 0.4340741\\
+-0.5 -2.5 0.55809058\\
+-0.5 -2 0.88197658\\
+-1 -2 0.78674913\\
+-0.5 -2.5 0.55809058\\
+0 -2.5 0.59847214\\
+0 -2 0.90929743\\
+-0.5 -2 0.88197658\\
+0 -2.5 0.59847214\\
+0.5 -2.5 0.55809058\\
+0.5 -2 0.88197658\\
+0 -2 0.90929743\\
+0.5 -2.5 0.55809058\\
+1 -2.5 0.4340741\\
+1 -2 0.78674913\\
+0.5 -2 0.88197658\\
+1 -2.5 0.4340741\\
+1.5 -2.5 0.22419478\\
+1.5 -2 0.59847214\\
+1 -2 0.78674913\\
+1.5 -2.5 0.22419478\\
+2 -2.5 -0.059933527\\
+2 -2 0.30807174\\
+1.5 -2 0.59847214\\
+2 -2.5 -0.059933527\\
+2.5 -2.5 -0.38383075\\
+2.5 -2 -0.059933527\\
+2 -2 0.30807174\\
+2.5 -2.5 -0.38383075\\
+3 -2.5 -0.6914774\\
+3 -2 -0.44749175\\
+2.5 -2 -0.059933527\\
+3 -2.5 -0.6914774\\
+3.5 -2.5 -0.9166313\\
+3.5 -2 -0.77677976\\
+3 -2 -0.44749175\\
+3.5 -2.5 -0.9166313\\
+4 -2.5 -0.99998941\\
+4 -2 -0.9712778\\
+3.5 -2 -0.77677976\\
+4 -2.5 -0.99998941\\
+4.5 -2.5 -0.9066904\\
+4.5 -2 -0.97760364\\
+4 -2 -0.9712778\\
+-5 -2 -0.7820949\\
+-4.5 -2 -0.97760364\\
+-4.5 -1.5 -0.99951869\\
+-5 -1.5 -0.87383376\\
+-4.5 -2 -0.97760364\\
+-4 -2 -0.9712778\\
+-4 -1.5 -0.90458671\\
+-4.5 -1.5 -0.99951869\\
+-4 -2 -0.9712778\\
+-3.5 -2 -0.77677976\\
+-3.5 -1.5 -0.61807681\\
+-4 -1.5 -0.90458671\\
+-3.5 -2 -0.77677976\\
+-3 -2 -0.44749175\\
+-3 -1.5 -0.21091343\\
+-3.5 -1.5 -0.61807681\\
+-3 -2 -0.44749175\\
+-2.5 -2 -0.059933527\\
+-2.5 -1.5 0.22419478\\
+-3 -1.5 -0.21091343\\
+-2.5 -2 -0.059933527\\
+-2 -2 0.30807174\\
+-2 -1.5 0.59847214\\
+-2.5 -1.5 0.22419478\\
+-2 -2 0.30807174\\
+-1.5 -2 0.59847214\\
+-1.5 -1.5 0.85225051\\
+-2 -1.5 0.59847214\\
+-1.5 -2 0.59847214\\
+-1 -2 0.78674913\\
+-1 -1.5 0.97321325\\
+-1.5 -1.5 0.85225051\\
+-1 -2 0.78674913\\
+-0.5 -2 0.88197658\\
+-0.5 -1.5 0.99994652\\
+-1 -1.5 0.97321325\\
+-0.5 -2 0.88197658\\
+0 -2 0.90929743\\
+0 -1.5 0.99749499\\
+-0.5 -1.5 0.99994652\\
+0 -2 0.90929743\\
+0.5 -2 0.88197658\\
+0.5 -1.5 0.99994652\\
+0 -1.5 0.99749499\\
+0.5 -2 0.88197658\\
+1 -2 0.78674913\\
+1 -1.5 0.97321325\\
+0.5 -1.5 0.99994652\\
+1 -2 0.78674913\\
+1.5 -2 0.59847214\\
+1.5 -1.5 0.85225051\\
+1 -1.5 0.97321325\\
+1.5 -2 0.59847214\\
+2 -2 0.30807174\\
+2 -1.5 0.59847214\\
+1.5 -1.5 0.85225051\\
+2 -2 0.30807174\\
+2.5 -2 -0.059933527\\
+2.5 -1.5 0.22419478\\
+2 -1.5 0.59847214\\
+2.5 -2 -0.059933527\\
+3 -2 -0.44749175\\
+3 -1.5 -0.21091343\\
+2.5 -1.5 0.22419478\\
+3 -2 -0.44749175\\
+3.5 -2 -0.77677976\\
+3.5 -1.5 -0.61807681\\
+3 -1.5 -0.21091343\\
+3.5 -2 -0.77677976\\
+4 -2 -0.9712778\\
+4 -1.5 -0.90458671\\
+3.5 -1.5 -0.61807681\\
+4 -2 -0.9712778\\
+4.5 -2 -0.97760364\\
+4.5 -1.5 -0.99951869\\
+4 -1.5 -0.90458671\\
+-5 -1.5 -0.87383376\\
+-4.5 -1.5 -0.99951869\\
+-4.5 -1 -0.99473952\\
+-5 -1 -0.92618484\\
+-4.5 -1.5 -0.99951869\\
+-4 -1.5 -0.90458671\\
+-4 -1 -0.83133918\\
+-4.5 -1 -0.99473952\\
+-4 -1.5 -0.90458671\\
+-3.5 -1.5 -0.61807681\\
+-3.5 -1 -0.47807551\\
+-4 -1 -0.83133918\\
+-3.5 -1.5 -0.61807681\\
+-3 -1.5 -0.21091343\\
+-3 -1 -0.020683532\\
+-3.5 -1 -0.47807551\\
+-3 -1.5 -0.21091343\\
+-2.5 -1.5 0.22419478\\
+-2.5 -1 0.4340741\\
+-3 -1 -0.020683532\\
+-2.5 -1.5 0.22419478\\
+-2 -1.5 0.59847214\\
+-2 -1 0.78674913\\
+-2.5 -1 0.4340741\\
+-2 -1.5 0.59847214\\
+-1.5 -1.5 0.85225051\\
+-1.5 -1 0.97321325\\
+-2 -1 0.78674913\\
+-1.5 -1.5 0.85225051\\
+-1 -1.5 0.97321325\\
+-1 -1 0.98776595\\
+-1.5 -1 0.97321325\\
+-1 -1.5 0.97321325\\
+-0.5 -1.5 0.99994652\\
+-0.5 -1 0.89924215\\
+-1 -1 0.98776595\\
+-0.5 -1.5 0.99994652\\
+0 -1.5 0.99749499\\
+0 -1 0.84147098\\
+-0.5 -1 0.89924215\\
+0 -1.5 0.99749499\\
+0.5 -1.5 0.99994652\\
+0.5 -1 0.89924215\\
+0 -1 0.84147098\\
+0.5 -1.5 0.99994652\\
+1 -1.5 0.97321325\\
+1 -1 0.98776595\\
+0.5 -1 0.89924215\\
+1 -1.5 0.97321325\\
+1.5 -1.5 0.85225051\\
+1.5 -1 0.97321325\\
+1 -1 0.98776595\\
+1.5 -1.5 0.85225051\\
+2 -1.5 0.59847214\\
+2 -1 0.78674913\\
+1.5 -1 0.97321325\\
+2 -1.5 0.59847214\\
+2.5 -1.5 0.22419478\\
+2.5 -1 0.4340741\\
+2 -1 0.78674913\\
+2.5 -1.5 0.22419478\\
+3 -1.5 -0.21091343\\
+3 -1 -0.020683532\\
+2.5 -1 0.4340741\\
+3 -1.5 -0.21091343\\
+3.5 -1.5 -0.61807681\\
+3.5 -1 -0.47807551\\
+3 -1 -0.020683532\\
+3.5 -1.5 -0.61807681\\
+4 -1.5 -0.90458671\\
+4 -1 -0.83133918\\
+3.5 -1 -0.47807551\\
+4 -1.5 -0.90458671\\
+4.5 -1.5 -0.99951869\\
+4.5 -1 -0.99473952\\
+4 -1 -0.83133918\\
+-5 -1 -0.92618484\\
+-4.5 -1 -0.99473952\\
+-4.5 -0.5 -0.98299205\\
+-5 -0.5 -0.95155293\\
+-4.5 -1 -0.99473952\\
+-4 -1 -0.83133918\\
+-4 -0.5 -0.77677976\\
+-4.5 -0.5 -0.98299205\\
+-4 -1 -0.83133918\\
+-3.5 -1 -0.47807551\\
+-3.5 -0.5 -0.38383075\\
+-4 -0.5 -0.77677976\\
+-3.5 -1 -0.47807551\\
+-3 -1 -0.020683532\\
+-3 -0.5 0.10004375\\
+-3.5 -0.5 -0.38383075\\
+-3 -1 -0.020683532\\
+-2.5 -1 0.4340741\\
+-2.5 -0.5 0.55809058\\
+-3 -0.5 0.10004375\\
+-2.5 -1 0.4340741\\
+-2 -1 0.78674913\\
+-2 -0.5 0.88197658\\
+-2.5 -0.5 0.55809058\\
+-2 -1 0.78674913\\
+-1.5 -1 0.97321325\\
+-1.5 -0.5 0.99994652\\
+-2 -0.5 0.88197658\\
+-1.5 -1 0.97321325\\
+-1 -1 0.98776595\\
+-1 -0.5 0.89924215\\
+-1.5 -0.5 0.99994652\\
+-1 -1 0.98776595\\
+-0.5 -1 0.89924215\\
+-0.5 -0.5 0.64963694\\
+-1 -0.5 0.89924215\\
+-0.5 -1 0.89924215\\
+0 -1 0.84147098\\
+0 -0.5 0.47942554\\
+-0.5 -0.5 0.64963694\\
+0 -1 0.84147098\\
+0.5 -1 0.89924215\\
+0.5 -0.5 0.64963694\\
+0 -0.5 0.47942554\\
+0.5 -1 0.89924215\\
+1 -1 0.98776595\\
+1 -0.5 0.89924215\\
+0.5 -0.5 0.64963694\\
+1 -1 0.98776595\\
+1.5 -1 0.97321325\\
+1.5 -0.5 0.99994652\\
+1 -0.5 0.89924215\\
+1.5 -1 0.97321325\\
+2 -1 0.78674913\\
+2 -0.5 0.88197658\\
+1.5 -0.5 0.99994652\\
+2 -1 0.78674913\\
+2.5 -1 0.4340741\\
+2.5 -0.5 0.55809058\\
+2 -0.5 0.88197658\\
+2.5 -1 0.4340741\\
+3 -1 -0.020683532\\
+3 -0.5 0.10004375\\
+2.5 -0.5 0.55809058\\
+3 -1 -0.020683532\\
+3.5 -1 -0.47807551\\
+3.5 -0.5 -0.38383075\\
+3 -0.5 0.10004375\\
+3.5 -1 -0.47807551\\
+4 -1 -0.83133918\\
+4 -0.5 -0.77677976\\
+3.5 -0.5 -0.38383075\\
+4 -1 -0.83133918\\
+4.5 -1 -0.99473952\\
+4.5 -0.5 -0.98299205\\
+4 -0.5 -0.77677976\\
+-5 -0.5 -0.95155293\\
+-4.5 -0.5 -0.98299205\\
+-4.5 0 -0.97753012\\
+-5 0 -0.95892427\\
+-4.5 -0.5 -0.98299205\\
+-4 -0.5 -0.77677976\\
+-4 0 -0.7568025\\
+-4.5 0 -0.97753012\\
+-4 -0.5 -0.77677976\\
+-3.5 -0.5 -0.38383075\\
+-3.5 0 -0.35078323\\
+-4 0 -0.7568025\\
+-3.5 -0.5 -0.38383075\\
+-3 -0.5 0.10004375\\
+-3 0 0.14112001\\
+-3.5 0 -0.35078323\\
+-3 -0.5 0.10004375\\
+-2.5 -0.5 0.55809058\\
+-2.5 0 0.59847214\\
+-3 0 0.14112001\\
+-2.5 -0.5 0.55809058\\
+-2 -0.5 0.88197658\\
+-2 0 0.90929743\\
+-2.5 0 0.59847214\\
+-2 -0.5 0.88197658\\
+-1.5 -0.5 0.99994652\\
+-1.5 0 0.99749499\\
+-2 0 0.90929743\\
+-1.5 -0.5 0.99994652\\
+-1 -0.5 0.89924215\\
+-1 0 0.84147098\\
+-1.5 0 0.99749499\\
+-1 -0.5 0.89924215\\
+-0.5 -0.5 0.64963694\\
+-0.5 0 0.47942554\\
+-1 0 0.84147098\\
+-0.5 -0.5 0.64963694\\
+0 -0.5 0.47942554\\
+0 0 0\\
+-0.5 0 0.47942554\\
+0 -0.5 0.47942554\\
+0.5 -0.5 0.64963694\\
+0.5 0 0.47942554\\
+0 0 0\\
+0.5 -0.5 0.64963694\\
+1 -0.5 0.89924215\\
+1 0 0.84147098\\
+0.5 0 0.47942554\\
+1 -0.5 0.89924215\\
+1.5 -0.5 0.99994652\\
+1.5 0 0.99749499\\
+1 0 0.84147098\\
+1.5 -0.5 0.99994652\\
+2 -0.5 0.88197658\\
+2 0 0.90929743\\
+1.5 0 0.99749499\\
+2 -0.5 0.88197658\\
+2.5 -0.5 0.55809058\\
+2.5 0 0.59847214\\
+2 0 0.90929743\\
+2.5 -0.5 0.55809058\\
+3 -0.5 0.10004375\\
+3 0 0.14112001\\
+2.5 0 0.59847214\\
+3 -0.5 0.10004375\\
+3.5 -0.5 -0.38383075\\
+3.5 0 -0.35078323\\
+3 0 0.14112001\\
+3.5 -0.5 -0.38383075\\
+4 -0.5 -0.77677976\\
+4 0 -0.7568025\\
+3.5 0 -0.35078323\\
+4 -0.5 -0.77677976\\
+4.5 -0.5 -0.98299205\\
+4.5 0 -0.97753012\\
+4 0 -0.7568025\\
+-5 0 -0.95892427\\
+-4.5 0 -0.97753012\\
+-4.5 0.5 -0.98299205\\
+-5 0.5 -0.95155293\\
+-4.5 0 -0.97753012\\
+-4 0 -0.7568025\\
+-4 0.5 -0.77677976\\
+-4.5 0.5 -0.98299205\\
+-4 0 -0.7568025\\
+-3.5 0 -0.35078323\\
+-3.5 0.5 -0.38383075\\
+-4 0.5 -0.77677976\\
+-3.5 0 -0.35078323\\
+-3 0 0.14112001\\
+-3 0.5 0.10004375\\
+-3.5 0.5 -0.38383075\\
+-3 0 0.14112001\\
+-2.5 0 0.59847214\\
+-2.5 0.5 0.55809058\\
+-3 0.5 0.10004375\\
+-2.5 0 0.59847214\\
+-2 0 0.90929743\\
+-2 0.5 0.88197658\\
+-2.5 0.5 0.55809058\\
+-2 0 0.90929743\\
+-1.5 0 0.99749499\\
+-1.5 0.5 0.99994652\\
+-2 0.5 0.88197658\\
+-1.5 0 0.99749499\\
+-1 0 0.84147098\\
+-1 0.5 0.89924215\\
+-1.5 0.5 0.99994652\\
+-1 0 0.84147098\\
+-0.5 0 0.47942554\\
+-0.5 0.5 0.64963694\\
+-1 0.5 0.89924215\\
+-0.5 0 0.47942554\\
+0 0 0\\
+0 0.5 0.47942554\\
+-0.5 0.5 0.64963694\\
+0 0 0\\
+0.5 0 0.47942554\\
+0.5 0.5 0.64963694\\
+0 0.5 0.47942554\\
+0.5 0 0.47942554\\
+1 0 0.84147098\\
+1 0.5 0.89924215\\
+0.5 0.5 0.64963694\\
+1 0 0.84147098\\
+1.5 0 0.99749499\\
+1.5 0.5 0.99994652\\
+1 0.5 0.89924215\\
+1.5 0 0.99749499\\
+2 0 0.90929743\\
+2 0.5 0.88197658\\
+1.5 0.5 0.99994652\\
+2 0 0.90929743\\
+2.5 0 0.59847214\\
+2.5 0.5 0.55809058\\
+2 0.5 0.88197658\\
+2.5 0 0.59847214\\
+3 0 0.14112001\\
+3 0.5 0.10004375\\
+2.5 0.5 0.55809058\\
+3 0 0.14112001\\
+3.5 0 -0.35078323\\
+3.5 0.5 -0.38383075\\
+3 0.5 0.10004375\\
+3.5 0 -0.35078323\\
+4 0 -0.7568025\\
+4 0.5 -0.77677976\\
+3.5 0.5 -0.38383075\\
+4 0 -0.7568025\\
+4.5 0 -0.97753012\\
+4.5 0.5 -0.98299205\\
+4 0.5 -0.77677976\\
+-5 0.5 -0.95155293\\
+-4.5 0.5 -0.98299205\\
+-4.5 1 -0.99473952\\
+-5 1 -0.92618484\\
+-4.5 0.5 -0.98299205\\
+-4 0.5 -0.77677976\\
+-4 1 -0.83133918\\
+-4.5 1 -0.99473952\\
+-4 0.5 -0.77677976\\
+-3.5 0.5 -0.38383075\\
+-3.5 1 -0.47807551\\
+-4 1 -0.83133918\\
+-3.5 0.5 -0.38383075\\
+-3 0.5 0.10004375\\
+-3 1 -0.020683532\\
+-3.5 1 -0.47807551\\
+-3 0.5 0.10004375\\
+-2.5 0.5 0.55809058\\
+-2.5 1 0.4340741\\
+-3 1 -0.020683532\\
+-2.5 0.5 0.55809058\\
+-2 0.5 0.88197658\\
+-2 1 0.78674913\\
+-2.5 1 0.4340741\\
+-2 0.5 0.88197658\\
+-1.5 0.5 0.99994652\\
+-1.5 1 0.97321325\\
+-2 1 0.78674913\\
+-1.5 0.5 0.99994652\\
+-1 0.5 0.89924215\\
+-1 1 0.98776595\\
+-1.5 1 0.97321325\\
+-1 0.5 0.89924215\\
+-0.5 0.5 0.64963694\\
+-0.5 1 0.89924215\\
+-1 1 0.98776595\\
+-0.5 0.5 0.64963694\\
+0 0.5 0.47942554\\
+0 1 0.84147098\\
+-0.5 1 0.89924215\\
+0 0.5 0.47942554\\
+0.5 0.5 0.64963694\\
+0.5 1 0.89924215\\
+0 1 0.84147098\\
+0.5 0.5 0.64963694\\
+1 0.5 0.89924215\\
+1 1 0.98776595\\
+0.5 1 0.89924215\\
+1 0.5 0.89924215\\
+1.5 0.5 0.99994652\\
+1.5 1 0.97321325\\
+1 1 0.98776595\\
+1.5 0.5 0.99994652\\
+2 0.5 0.88197658\\
+2 1 0.78674913\\
+1.5 1 0.97321325\\
+2 0.5 0.88197658\\
+2.5 0.5 0.55809058\\
+2.5 1 0.4340741\\
+2 1 0.78674913\\
+2.5 0.5 0.55809058\\
+3 0.5 0.10004375\\
+3 1 -0.020683532\\
+2.5 1 0.4340741\\
+3 0.5 0.10004375\\
+3.5 0.5 -0.38383075\\
+3.5 1 -0.47807551\\
+3 1 -0.020683532\\
+3.5 0.5 -0.38383075\\
+4 0.5 -0.77677976\\
+4 1 -0.83133918\\
+3.5 1 -0.47807551\\
+4 0.5 -0.77677976\\
+4.5 0.5 -0.98299205\\
+4.5 1 -0.99473952\\
+4 1 -0.83133918\\
+-5 1 -0.92618484\\
+-4.5 1 -0.99473952\\
+-4.5 1.5 -0.99951869\\
+-5 1.5 -0.87383376\\
+-4.5 1 -0.99473952\\
+-4 1 -0.83133918\\
+-4 1.5 -0.90458671\\
+-4.5 1.5 -0.99951869\\
+-4 1 -0.83133918\\
+-3.5 1 -0.47807551\\
+-3.5 1.5 -0.61807681\\
+-4 1.5 -0.90458671\\
+-3.5 1 -0.47807551\\
+-3 1 -0.020683532\\
+-3 1.5 -0.21091343\\
+-3.5 1.5 -0.61807681\\
+-3 1 -0.020683532\\
+-2.5 1 0.4340741\\
+-2.5 1.5 0.22419478\\
+-3 1.5 -0.21091343\\
+-2.5 1 0.4340741\\
+-2 1 0.78674913\\
+-2 1.5 0.59847214\\
+-2.5 1.5 0.22419478\\
+-2 1 0.78674913\\
+-1.5 1 0.97321325\\
+-1.5 1.5 0.85225051\\
+-2 1.5 0.59847214\\
+-1.5 1 0.97321325\\
+-1 1 0.98776595\\
+-1 1.5 0.97321325\\
+-1.5 1.5 0.85225051\\
+-1 1 0.98776595\\
+-0.5 1 0.89924215\\
+-0.5 1.5 0.99994652\\
+-1 1.5 0.97321325\\
+-0.5 1 0.89924215\\
+0 1 0.84147098\\
+0 1.5 0.99749499\\
+-0.5 1.5 0.99994652\\
+0 1 0.84147098\\
+0.5 1 0.89924215\\
+0.5 1.5 0.99994652\\
+0 1.5 0.99749499\\
+0.5 1 0.89924215\\
+1 1 0.98776595\\
+1 1.5 0.97321325\\
+0.5 1.5 0.99994652\\
+1 1 0.98776595\\
+1.5 1 0.97321325\\
+1.5 1.5 0.85225051\\
+1 1.5 0.97321325\\
+1.5 1 0.97321325\\
+2 1 0.78674913\\
+2 1.5 0.59847214\\
+1.5 1.5 0.85225051\\
+2 1 0.78674913\\
+2.5 1 0.4340741\\
+2.5 1.5 0.22419478\\
+2 1.5 0.59847214\\
+2.5 1 0.4340741\\
+3 1 -0.020683532\\
+3 1.5 -0.21091343\\
+2.5 1.5 0.22419478\\
+3 1 -0.020683532\\
+3.5 1 -0.47807551\\
+3.5 1.5 -0.61807681\\
+3 1.5 -0.21091343\\
+3.5 1 -0.47807551\\
+4 1 -0.83133918\\
+4 1.5 -0.90458671\\
+3.5 1.5 -0.61807681\\
+4 1 -0.83133918\\
+4.5 1 -0.99473952\\
+4.5 1.5 -0.99951869\\
+4 1.5 -0.90458671\\
+-5 1.5 -0.87383376\\
+-4.5 1.5 -0.99951869\\
+-4.5 2 -0.97760364\\
+-5 2 -0.7820949\\
+-4.5 1.5 -0.99951869\\
+-4 1.5 -0.90458671\\
+-4 2 -0.9712778\\
+-4.5 2 -0.97760364\\
+-4 1.5 -0.90458671\\
+-3.5 1.5 -0.61807681\\
+-3.5 2 -0.77677976\\
+-4 2 -0.9712778\\
+-3.5 1.5 -0.61807681\\
+-3 1.5 -0.21091343\\
+-3 2 -0.44749175\\
+-3.5 2 -0.77677976\\
+-3 1.5 -0.21091343\\
+-2.5 1.5 0.22419478\\
+-2.5 2 -0.059933527\\
+-3 2 -0.44749175\\
+-2.5 1.5 0.22419478\\
+-2 1.5 0.59847214\\
+-2 2 0.30807174\\
+-2.5 2 -0.059933527\\
+-2 1.5 0.59847214\\
+-1.5 1.5 0.85225051\\
+-1.5 2 0.59847214\\
+-2 2 0.30807174\\
+-1.5 1.5 0.85225051\\
+-1 1.5 0.97321325\\
+-1 2 0.78674913\\
+-1.5 2 0.59847214\\
+-1 1.5 0.97321325\\
+-0.5 1.5 0.99994652\\
+-0.5 2 0.88197658\\
+-1 2 0.78674913\\
+-0.5 1.5 0.99994652\\
+0 1.5 0.99749499\\
+0 2 0.90929743\\
+-0.5 2 0.88197658\\
+0 1.5 0.99749499\\
+0.5 1.5 0.99994652\\
+0.5 2 0.88197658\\
+0 2 0.90929743\\
+0.5 1.5 0.99994652\\
+1 1.5 0.97321325\\
+1 2 0.78674913\\
+0.5 2 0.88197658\\
+1 1.5 0.97321325\\
+1.5 1.5 0.85225051\\
+1.5 2 0.59847214\\
+1 2 0.78674913\\
+1.5 1.5 0.85225051\\
+2 1.5 0.59847214\\
+2 2 0.30807174\\
+1.5 2 0.59847214\\
+2 1.5 0.59847214\\
+2.5 1.5 0.22419478\\
+2.5 2 -0.059933527\\
+2 2 0.30807174\\
+2.5 1.5 0.22419478\\
+3 1.5 -0.21091343\\
+3 2 -0.44749175\\
+2.5 2 -0.059933527\\
+3 1.5 -0.21091343\\
+3.5 1.5 -0.61807681\\
+3.5 2 -0.77677976\\
+3 2 -0.44749175\\
+3.5 1.5 -0.61807681\\
+4 1.5 -0.90458671\\
+4 2 -0.9712778\\
+3.5 2 -0.77677976\\
+4 1.5 -0.90458671\\
+4.5 1.5 -0.99951869\\
+4.5 2 -0.97760364\\
+4 2 -0.9712778\\
+-5 2 -0.7820949\\
+-4.5 2 -0.97760364\\
+-4.5 2.5 -0.9066904\\
+-5 2.5 -0.63885987\\
+-4.5 2 -0.97760364\\
+-4 2 -0.9712778\\
+-4 2.5 -0.99998941\\
+-4.5 2.5 -0.9066904\\
+-4 2 -0.9712778\\
+-3.5 2 -0.77677976\\
+-3.5 2.5 -0.9166313\\
+-4 2.5 -0.99998941\\
+-3.5 2 -0.77677976\\
+-3 2 -0.44749175\\
+-3 2.5 -0.6914774\\
+-3.5 2.5 -0.9166313\\
+-3 2 -0.44749175\\
+-2.5 2 -0.059933527\\
+-2.5 2.5 -0.38383075\\
+-3 2.5 -0.6914774\\
+-2.5 2 -0.059933527\\
+-2 2 0.30807174\\
+-2 2.5 -0.059933527\\
+-2.5 2.5 -0.38383075\\
+-2 2 0.30807174\\
+-1.5 2 0.59847214\\
+-1.5 2.5 0.22419478\\
+-2 2.5 -0.059933527\\
+-1.5 2 0.59847214\\
+-1 2 0.78674913\\
+-1 2.5 0.4340741\\
+-1.5 2.5 0.22419478\\
+-1 2 0.78674913\\
+-0.5 2 0.88197658\\
+-0.5 2.5 0.55809058\\
+-1 2.5 0.4340741\\
+-0.5 2 0.88197658\\
+0 2 0.90929743\\
+0 2.5 0.59847214\\
+-0.5 2.5 0.55809058\\
+0 2 0.90929743\\
+0.5 2 0.88197658\\
+0.5 2.5 0.55809058\\
+0 2.5 0.59847214\\
+0.5 2 0.88197658\\
+1 2 0.78674913\\
+1 2.5 0.4340741\\
+0.5 2.5 0.55809058\\
+1 2 0.78674913\\
+1.5 2 0.59847214\\
+1.5 2.5 0.22419478\\
+1 2.5 0.4340741\\
+1.5 2 0.59847214\\
+2 2 0.30807174\\
+2 2.5 -0.059933527\\
+1.5 2.5 0.22419478\\
+2 2 0.30807174\\
+2.5 2 -0.059933527\\
+2.5 2.5 -0.38383075\\
+2 2.5 -0.059933527\\
+2.5 2 -0.059933527\\
+3 2 -0.44749175\\
+3 2.5 -0.6914774\\
+2.5 2.5 -0.38383075\\
+3 2 -0.44749175\\
+3.5 2 -0.77677976\\
+3.5 2.5 -0.9166313\\
+3 2.5 -0.6914774\\
+3.5 2 -0.77677976\\
+4 2 -0.9712778\\
+4 2.5 -0.99998941\\
+3.5 2.5 -0.9166313\\
+4 2 -0.9712778\\
+4.5 2 -0.97760364\\
+4.5 2.5 -0.9066904\\
+4 2.5 -0.99998941\\
+-5 2.5 -0.63885987\\
+-4.5 2.5 -0.9066904\\
+-4.5 3 -0.76745273\\
+-5 3 -0.43697552\\
+-4.5 2.5 -0.9066904\\
+-4 2.5 -0.99998941\\
+-4 3 -0.95892427\\
+-4.5 3 -0.76745273\\
+-4 2.5 -0.99998941\\
+-3.5 2.5 -0.9166313\\
+-3.5 3 -0.99473952\\
+-4 3 -0.95892427\\
+-3.5 2.5 -0.9166313\\
+-3 2.5 -0.6914774\\
+-3 3 -0.89168225\\
+-3.5 3 -0.99473952\\
+-3 2.5 -0.6914774\\
+-2.5 2.5 -0.38383075\\
+-2.5 3 -0.6914774\\
+-3 3 -0.89168225\\
+-2.5 2.5 -0.38383075\\
+-2 2.5 -0.059933527\\
+-2 3 -0.44749175\\
+-2.5 3 -0.6914774\\
+-2 2.5 -0.059933527\\
+-1.5 2.5 0.22419478\\
+-1.5 3 -0.21091343\\
+-2 3 -0.44749175\\
+-1.5 2.5 0.22419478\\
+-1 2.5 0.4340741\\
+-1 3 -0.020683532\\
+-1.5 3 -0.21091343\\
+-1 2.5 0.4340741\\
+-0.5 2.5 0.55809058\\
+-0.5 3 0.10004375\\
+-1 3 -0.020683532\\
+-0.5 2.5 0.55809058\\
+0 2.5 0.59847214\\
+0 3 0.14112001\\
+-0.5 3 0.10004375\\
+0 2.5 0.59847214\\
+0.5 2.5 0.55809058\\
+0.5 3 0.10004375\\
+0 3 0.14112001\\
+0.5 2.5 0.55809058\\
+1 2.5 0.4340741\\
+1 3 -0.020683532\\
+0.5 3 0.10004375\\
+1 2.5 0.4340741\\
+1.5 2.5 0.22419478\\
+1.5 3 -0.21091343\\
+1 3 -0.020683532\\
+1.5 2.5 0.22419478\\
+2 2.5 -0.059933527\\
+2 3 -0.44749175\\
+1.5 3 -0.21091343\\
+2 2.5 -0.059933527\\
+2.5 2.5 -0.38383075\\
+2.5 3 -0.6914774\\
+2 3 -0.44749175\\
+2.5 2.5 -0.38383075\\
+3 2.5 -0.6914774\\
+3 3 -0.89168225\\
+2.5 3 -0.6914774\\
+3 2.5 -0.6914774\\
+3.5 2.5 -0.9166313\\
+3.5 3 -0.99473952\\
+3 3 -0.89168225\\
+3.5 2.5 -0.9166313\\
+4 2.5 -0.99998941\\
+4 3 -0.95892427\\
+3.5 3 -0.99473952\\
+4 2.5 -0.99998941\\
+4.5 2.5 -0.9066904\\
+4.5 3 -0.76745273\\
+4 3 -0.95892427\\
+-5 3 -0.43697552\\
+-4.5 3 -0.76745273\\
+-4.5 3.5 -0.54995318\\
+-5 3.5 -0.17893857\\
+-4.5 3 -0.76745273\\
+-4 3 -0.95892427\\
+-4 3.5 -0.82381719\\
+-4.5 3.5 -0.54995318\\
+-4 3 -0.95892427\\
+-3.5 3 -0.99473952\\
+-3.5 3.5 -0.97196248\\
+-4 3.5 -0.82381719\\
+-3.5 3 -0.99473952\\
+-3 3 -0.89168225\\
+-3 3.5 -0.99473952\\
+-3.5 3.5 -0.97196248\\
+-3 3 -0.89168225\\
+-2.5 3 -0.6914774\\
+-2.5 3.5 -0.9166313\\
+-3 3.5 -0.99473952\\
+-2.5 3 -0.6914774\\
+-2 3 -0.44749175\\
+-2 3.5 -0.77677976\\
+-2.5 3.5 -0.9166313\\
+-2 3 -0.44749175\\
+-1.5 3 -0.21091343\\
+-1.5 3.5 -0.61807681\\
+-2 3.5 -0.77677976\\
+-1.5 3 -0.21091343\\
+-1 3 -0.020683532\\
+-1 3.5 -0.47807551\\
+-1.5 3.5 -0.61807681\\
+-1 3 -0.020683532\\
+-0.5 3 0.10004375\\
+-0.5 3.5 -0.38383075\\
+-1 3.5 -0.47807551\\
+-0.5 3 0.10004375\\
+0 3 0.14112001\\
+0 3.5 -0.35078323\\
+-0.5 3.5 -0.38383075\\
+0 3 0.14112001\\
+0.5 3 0.10004375\\
+0.5 3.5 -0.38383075\\
+0 3.5 -0.35078323\\
+0.5 3 0.10004375\\
+1 3 -0.020683532\\
+1 3.5 -0.47807551\\
+0.5 3.5 -0.38383075\\
+1 3 -0.020683532\\
+1.5 3 -0.21091343\\
+1.5 3.5 -0.61807681\\
+1 3.5 -0.47807551\\
+1.5 3 -0.21091343\\
+2 3 -0.44749175\\
+2 3.5 -0.77677976\\
+1.5 3.5 -0.61807681\\
+2 3 -0.44749175\\
+2.5 3 -0.6914774\\
+2.5 3.5 -0.9166313\\
+2 3.5 -0.77677976\\
+2.5 3 -0.6914774\\
+3 3 -0.89168225\\
+3 3.5 -0.99473952\\
+2.5 3.5 -0.9166313\\
+3 3 -0.89168225\\
+3.5 3 -0.99473952\\
+3.5 3.5 -0.97196248\\
+3 3.5 -0.99473952\\
+3.5 3 -0.99473952\\
+4 3 -0.95892427\\
+4 3.5 -0.82381719\\
+3.5 3.5 -0.97196248\\
+4 3 -0.95892427\\
+4.5 3 -0.76745273\\
+4.5 3.5 -0.54995318\\
+4 3.5 -0.82381719\\
+-5 3.5 -0.17893857\\
+-4.5 3.5 -0.54995318\\
+-4.5 4 -0.25938757\\
+-5 4 0.11965158\\
+-4.5 3.5 -0.54995318\\
+-4 3.5 -0.82381719\\
+-4 4 -0.58617619\\
+-4.5 4 -0.25938757\\
+-4 3.5 -0.82381719\\
+-3.5 3.5 -0.97196248\\
+-3.5 4 -0.82381719\\
+-4 4 -0.58617619\\
+-3.5 3.5 -0.97196248\\
+-3 3.5 -0.99473952\\
+-3 4 -0.95892427\\
+-3.5 4 -0.82381719\\
+-3 3.5 -0.99473952\\
+-2.5 3.5 -0.9166313\\
+-2.5 4 -0.99998941\\
+-3 4 -0.95892427\\
+-2.5 3.5 -0.9166313\\
+-2 3.5 -0.77677976\\
+-2 4 -0.9712778\\
+-2.5 4 -0.99998941\\
+-2 3.5 -0.77677976\\
+-1.5 3.5 -0.61807681\\
+-1.5 4 -0.90458671\\
+-2 4 -0.9712778\\
+-1.5 3.5 -0.61807681\\
+-1 3.5 -0.47807551\\
+-1 4 -0.83133918\\
+-1.5 4 -0.90458671\\
+-1 3.5 -0.47807551\\
+-0.5 3.5 -0.38383075\\
+-0.5 4 -0.77677976\\
+-1 4 -0.83133918\\
+-0.5 3.5 -0.38383075\\
+0 3.5 -0.35078323\\
+0 4 -0.7568025\\
+-0.5 4 -0.77677976\\
+0 3.5 -0.35078323\\
+0.5 3.5 -0.38383075\\
+0.5 4 -0.77677976\\
+0 4 -0.7568025\\
+0.5 3.5 -0.38383075\\
+1 3.5 -0.47807551\\
+1 4 -0.83133918\\
+0.5 4 -0.77677976\\
+1 3.5 -0.47807551\\
+1.5 3.5 -0.61807681\\
+1.5 4 -0.90458671\\
+1 4 -0.83133918\\
+1.5 3.5 -0.61807681\\
+2 3.5 -0.77677976\\
+2 4 -0.9712778\\
+1.5 4 -0.90458671\\
+2 3.5 -0.77677976\\
+2.5 3.5 -0.9166313\\
+2.5 4 -0.99998941\\
+2 4 -0.9712778\\
+2.5 3.5 -0.9166313\\
+3 3.5 -0.99473952\\
+3 4 -0.95892427\\
+2.5 4 -0.99998941\\
+3 3.5 -0.99473952\\
+3.5 3.5 -0.97196248\\
+3.5 4 -0.82381719\\
+3 4 -0.95892427\\
+3.5 3.5 -0.97196248\\
+4 3.5 -0.82381719\\
+4 4 -0.58617619\\
+3.5 4 -0.82381719\\
+4 3.5 -0.82381719\\
+4.5 3.5 -0.54995318\\
+4.5 4 -0.25938757\\
+4 4 -0.58617619\\
+-5 4 0.11965158\\
+-4.5 4 -0.25938757\\
+-4.5 4.5 0.080687912\\
+-5 4.5 0.42921793\\
+-4.5 4 -0.25938757\\
+-4 4 -0.58617619\\
+-4 4.5 -0.25938757\\
+-4.5 4.5 0.080687912\\
+-4 4 -0.58617619\\
+-3.5 4 -0.82381719\\
+-3.5 4.5 -0.54995318\\
+-4 4.5 -0.25938757\\
+-3.5 4 -0.82381719\\
+-3 4 -0.95892427\\
+-3 4.5 -0.76745273\\
+-3.5 4.5 -0.54995318\\
+-3 4 -0.95892427\\
+-2.5 4 -0.99998941\\
+-2.5 4.5 -0.9066904\\
+-3 4.5 -0.76745273\\
+-2.5 4 -0.99998941\\
+-2 4 -0.9712778\\
+-2 4.5 -0.97760364\\
+-2.5 4.5 -0.9066904\\
+-2 4 -0.9712778\\
+-1.5 4 -0.90458671\\
+-1.5 4.5 -0.99951869\\
+-2 4.5 -0.97760364\\
+-1.5 4 -0.90458671\\
+-1 4 -0.83133918\\
+-1 4.5 -0.99473952\\
+-1.5 4.5 -0.99951869\\
+-1 4 -0.83133918\\
+-0.5 4 -0.77677976\\
+-0.5 4.5 -0.98299205\\
+-1 4.5 -0.99473952\\
+-0.5 4 -0.77677976\\
+0 4 -0.7568025\\
+0 4.5 -0.97753012\\
+-0.5 4.5 -0.98299205\\
+0 4 -0.7568025\\
+0.5 4 -0.77677976\\
+0.5 4.5 -0.98299205\\
+0 4.5 -0.97753012\\
+0.5 4 -0.77677976\\
+1 4 -0.83133918\\
+1 4.5 -0.99473952\\
+0.5 4.5 -0.98299205\\
+1 4 -0.83133918\\
+1.5 4 -0.90458671\\
+1.5 4.5 -0.99951869\\
+1 4.5 -0.99473952\\
+1.5 4 -0.90458671\\
+2 4 -0.9712778\\
+2 4.5 -0.97760364\\
+1.5 4.5 -0.99951869\\
+2 4 -0.9712778\\
+2.5 4 -0.99998941\\
+2.5 4.5 -0.9066904\\
+2 4.5 -0.97760364\\
+2.5 4 -0.99998941\\
+3 4 -0.95892427\\
+3 4.5 -0.76745273\\
+2.5 4.5 -0.9066904\\
+3 4 -0.95892427\\
+3.5 4 -0.82381719\\
+3.5 4.5 -0.54995318\\
+3 4.5 -0.76745273\\
+3.5 4 -0.82381719\\
+4 4 -0.58617619\\
+4 4.5 -0.25938757\\
+3.5 4.5 -0.54995318\\
+4 4 -0.58617619\\
+4.5 4 -0.25938757\\
+4.5 4.5 0.080687912\\
+4 4.5 -0.25938757\\
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 -5 1.4588613
+-4.5 -5 1.1792179
+-4 -5 0.86965158
+-3.5 -5 0.57106143
+-3 -5 0.31302448
+-2.5 -5 0.11114013
+-2 -5 -0.032094899
+-1.5 -5 -0.12383376
+-1 -5 -0.17618484
+-0.5 -5 -0.20155293
+0 -5 -0.20892427
+0.5 -5 -0.20155293
+1 -5 -0.17618484
+1.5 -5 -0.12383376
+2 -5 -0.032094899
+2.5 -5 0.11114013
+3 -5 0.31302448
+3.5 -5 0.57106143
+4 -5 0.86965158
+4.5 -5 1.1792179
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 -4 0.86965158
+-4.5 -4 0.49061243
+-4 -4 0.16382381
+-3.5 -4 -0.073817185
+-3 -4 -0.20892427
+-2.5 -4 -0.24998941
+-2 -4 -0.2212778
+-1.5 -4 -0.15458671
+-1 -4 -0.081339179
+-0.5 -4 -0.026779756
+0 -4 -0.0068024953
+0.5 -4 -0.026779756
+1 -4 -0.081339179
+1.5 -4 -0.15458671
+2 -4 -0.2212778
+2.5 -4 -0.24998941
+3 -4 -0.20892427
+3.5 -4 -0.073817185
+4 -4 0.16382381
+4.5 -4 0.49061243
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 -3 0.31302448
+-4.5 -3 -0.017452726
+-4 -3 -0.20892427
+-3.5 -3 -0.24473952
+-3 -3 -0.14168225
+-2.5 -3 0.058522603
+-2 -3 0.30250825
+-1.5 -3 0.53908657
+-1 -3 0.72931647
+-0.5 -3 0.85004375
+0 -3 0.89112001
+0.5 -3 0.85004375
+1 -3 0.72931647
+1.5 -3 0.53908657
+2 -3 0.30250825
+2.5 -3 0.058522603
+3 -3 -0.14168225
+3.5 -3 -0.24473952
+4 -3 -0.20892427
+4.5 -3 -0.017452726
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 -2 -0.032094899
+-4.5 -2 -0.22760364
+-4 -2 -0.2212778
+-3.5 -2 -0.026779756
+-3 -2 0.30250825
+-2.5 -2 0.69006647
+-2 -2 1.0580717
+-1.5 -2 1.3484721
+-1 -2 1.5367491
+-0.5 -2 1.6319766
+0 -2 1.6592974
+0.5 -2 1.6319766
+1 -2 1.5367491
+1.5 -2 1.3484721
+2 -2 1.0580717
+2.5 -2 0.69006647
+3 -2 0.30250825
+3.5 -2 -0.026779756
+4 -2 -0.2212778
+4.5 -2 -0.22760364
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 -1 -0.17618484
+-4.5 -1 -0.24473952
+-4 -1 -0.081339179
+-3.5 -1 0.27192449
+-3 -1 0.72931647
+-2.5 -1 1.1840741
+-2 -1 1.5367491
+-1.5 -1 1.7232132
+-1 -1 1.7377659
+-0.5 -1 1.6492421
+0 -1 1.591471
+0.5 -1 1.6492421
+1 -1 1.7377659
+1.5 -1 1.7232132
+2 -1 1.5367491
+2.5 -1 1.1840741
+3 -1 0.72931647
+3.5 -1 0.27192449
+4 -1 -0.081339179
+4.5 -1 -0.24473952
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 0 -0.20892427
+-4.5 0 -0.22753012
+-4 0 -0.0068024953
+-3.5 0 0.39921677
+-3 0 0.89112001
+-2.5 0 1.3484721
+-2 0 1.6592974
+-1.5 0 1.747495
+-1 0 1.591471
+-0.5 0 1.2294255
+0 0 0.75
+0.5 0 1.2294255
+1 0 1.591471
+1.5 0 1.747495
+2 0 1.6592974
+2.5 0 1.3484721
+3 0 0.89112001
+3.5 0 0.39921677
+4 0 -0.0068024953
+4.5 0 -0.22753012
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 1 -0.17618484
+-4.5 1 -0.24473952
+-4 1 -0.081339179
+-3.5 1 0.27192449
+-3 1 0.72931647
+-2.5 1 1.1840741
+-2 1 1.5367491
+-1.5 1 1.7232132
+-1 1 1.7377659
+-0.5 1 1.6492421
+0 1 1.591471
+0.5 1 1.6492421
+1 1 1.7377659
+1.5 1 1.7232132
+2 1 1.5367491
+2.5 1 1.1840741
+3 1 0.72931647
+3.5 1 0.27192449
+4 1 -0.081339179
+4.5 1 -0.24473952
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 2 -0.032094899
+-4.5 2 -0.22760364
+-4 2 -0.2212778
+-3.5 2 -0.026779756
+-3 2 0.30250825
+-2.5 2 0.69006647
+-2 2 1.0580717
+-1.5 2 1.3484721
+-1 2 1.5367491
+-0.5 2 1.6319766
+0 2 1.6592974
+0.5 2 1.6319766
+1 2 1.5367491
+1.5 2 1.3484721
+2 2 1.0580717
+2.5 2 0.69006647
+3 2 0.30250825
+3.5 2 -0.026779756
+4 2 -0.2212778
+4.5 2 -0.22760364
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 3 0.31302448
+-4.5 3 -0.017452726
+-4 3 -0.20892427
+-3.5 3 -0.24473952
+-3 3 -0.14168225
+-2.5 3 0.058522603
+-2 3 0.30250825
+-1.5 3 0.53908657
+-1 3 0.72931647
+-0.5 3 0.85004375
+0 3 0.89112001
+0.5 3 0.85004375
+1 3 0.72931647
+1.5 3 0.53908657
+2 3 0.30250825
+2.5 3 0.058522603
+3 3 -0.14168225
+3.5 3 -0.24473952
+4 3 -0.20892427
+4.5 3 -0.017452726
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 4 0.86965158
+-4.5 4 0.49061243
+-4 4 0.16382381
+-3.5 4 -0.073817185
+-3 4 -0.20892427
+-2.5 4 -0.24998941
+-2 4 -0.2212778
+-1.5 4 -0.15458671
+-1 4 -0.081339179
+-0.5 4 -0.026779756
+0 4 -0.0068024953
+0.5 4 -0.026779756
+1 4 -0.081339179
+1.5 4 -0.15458671
+2 4 -0.2212778
+2.5 4 -0.24998941
+3 4 -0.20892427
+3.5 4 -0.073817185
+4 4 0.16382381
+4.5 4 0.49061243
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 4.5 1.1792179
+-4.5 4.5 0.83068791
+-4 4.5 0.49061243
+-3.5 4.5 0.20004682
+-3 4.5 -0.017452726
+-2.5 4.5 -0.1566904
+-2 4.5 -0.22760364
+-1.5 4.5 -0.24951869
+-1 4.5 -0.24473952
+-0.5 4.5 -0.23299205
+0 4.5 -0.22753012
+0.5 4.5 -0.23299205
+1 4.5 -0.24473952
+1.5 4.5 -0.24951869
+2 4.5 -0.22760364
+2.5 4.5 -0.1566904
+3 4.5 -0.017452726
+3.5 4.5 0.20004682
+4 4.5 0.49061243
+4.5 4.5 0.83068791
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-5 -5 1.4588613
+-5 -4.5 1.1792179
+-5 -4 0.86965158
+-5 -3.5 0.57106143
+-5 -3 0.31302448
+-5 -2.5 0.11114013
+-5 -2 -0.032094899
+-5 -1.5 -0.12383376
+-5 -1 -0.17618484
+-5 -0.5 -0.20155293
+-5 0 -0.20892427
+-5 0.5 -0.20155293
+-5 1 -0.17618484
+-5 1.5 -0.12383376
+-5 2 -0.032094899
+-5 2.5 0.11114013
+-5 3 0.31302448
+-5 3.5 0.57106143
+-5 4 0.86965158
+-5 4.5 1.1792179
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-4 -5 0.86965158
+-4 -4.5 0.49061243
+-4 -4 0.16382381
+-4 -3.5 -0.073817185
+-4 -3 -0.20892427
+-4 -2.5 -0.24998941
+-4 -2 -0.2212778
+-4 -1.5 -0.15458671
+-4 -1 -0.081339179
+-4 -0.5 -0.026779756
+-4 0 -0.0068024953
+-4 0.5 -0.026779756
+-4 1 -0.081339179
+-4 1.5 -0.15458671
+-4 2 -0.2212778
+-4 2.5 -0.24998941
+-4 3 -0.20892427
+-4 3.5 -0.073817185
+-4 4 0.16382381
+-4 4.5 0.49061243
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-3 -5 0.31302448
+-3 -4.5 -0.017452726
+-3 -4 -0.20892427
+-3 -3.5 -0.24473952
+-3 -3 -0.14168225
+-3 -2.5 0.058522603
+-3 -2 0.30250825
+-3 -1.5 0.53908657
+-3 -1 0.72931647
+-3 -0.5 0.85004375
+-3 0 0.89112001
+-3 0.5 0.85004375
+-3 1 0.72931647
+-3 1.5 0.53908657
+-3 2 0.30250825
+-3 2.5 0.058522603
+-3 3 -0.14168225
+-3 3.5 -0.24473952
+-3 4 -0.20892427
+-3 4.5 -0.017452726
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-2 -5 -0.032094899
+-2 -4.5 -0.22760364
+-2 -4 -0.2212778
+-2 -3.5 -0.026779756
+-2 -3 0.30250825
+-2 -2.5 0.69006647
+-2 -2 1.0580717
+-2 -1.5 1.3484721
+-2 -1 1.5367491
+-2 -0.5 1.6319766
+-2 0 1.6592974
+-2 0.5 1.6319766
+-2 1 1.5367491
+-2 1.5 1.3484721
+-2 2 1.0580717
+-2 2.5 0.69006647
+-2 3 0.30250825
+-2 3.5 -0.026779756
+-2 4 -0.2212778
+-2 4.5 -0.22760364
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+-1 -5 -0.17618484
+-1 -4.5 -0.24473952
+-1 -4 -0.081339179
+-1 -3.5 0.27192449
+-1 -3 0.72931647
+-1 -2.5 1.1840741
+-1 -2 1.5367491
+-1 -1.5 1.7232132
+-1 -1 1.7377659
+-1 -0.5 1.6492421
+-1 0 1.591471
+-1 0.5 1.6492421
+-1 1 1.7377659
+-1 1.5 1.7232132
+-1 2 1.5367491
+-1 2.5 1.1840741
+-1 3 0.72931647
+-1 3.5 0.27192449
+-1 4 -0.081339179
+-1 4.5 -0.24473952
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+0 -5 -0.20892427
+0 -4.5 -0.22753012
+0 -4 -0.0068024953
+0 -3.5 0.39921677
+0 -3 0.89112001
+0 -2.5 1.3484721
+0 -2 1.6592974
+0 -1.5 1.747495
+0 -1 1.591471
+0 -0.5 1.2294255
+0 0 0.75
+0 0.5 1.2294255
+0 1 1.591471
+0 1.5 1.747495
+0 2 1.6592974
+0 2.5 1.3484721
+0 3 0.89112001
+0 3.5 0.39921677
+0 4 -0.0068024953
+0 4.5 -0.22753012
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+1 -5 -0.17618484
+1 -4.5 -0.24473952
+1 -4 -0.081339179
+1 -3.5 0.27192449
+1 -3 0.72931647
+1 -2.5 1.1840741
+1 -2 1.5367491
+1 -1.5 1.7232132
+1 -1 1.7377659
+1 -0.5 1.6492421
+1 0 1.591471
+1 0.5 1.6492421
+1 1 1.7377659
+1 1.5 1.7232132
+1 2 1.5367491
+1 2.5 1.1840741
+1 3 0.72931647
+1 3.5 0.27192449
+1 4 -0.081339179
+1 4.5 -0.24473952
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+2 -5 -0.032094899
+2 -4.5 -0.22760364
+2 -4 -0.2212778
+2 -3.5 -0.026779756
+2 -3 0.30250825
+2 -2.5 0.69006647
+2 -2 1.0580717
+2 -1.5 1.3484721
+2 -1 1.5367491
+2 -0.5 1.6319766
+2 0 1.6592974
+2 0.5 1.6319766
+2 1 1.5367491
+2 1.5 1.3484721
+2 2 1.0580717
+2 2.5 0.69006647
+2 3 0.30250825
+2 3.5 -0.026779756
+2 4 -0.2212778
+2 4.5 -0.22760364
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+3 -5 0.31302448
+3 -4.5 -0.017452726
+3 -4 -0.20892427
+3 -3.5 -0.24473952
+3 -3 -0.14168225
+3 -2.5 0.058522603
+3 -2 0.30250825
+3 -1.5 0.53908657
+3 -1 0.72931647
+3 -0.5 0.85004375
+3 0 0.89112001
+3 0.5 0.85004375
+3 1 0.72931647
+3 1.5 0.53908657
+3 2 0.30250825
+3 2.5 0.058522603
+3 3 -0.14168225
+3 3.5 -0.24473952
+3 4 -0.20892427
+3 4.5 -0.017452726
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+4 -5 0.86965158
+4 -4.5 0.49061243
+4 -4 0.16382381
+4 -3.5 -0.073817185
+4 -3 -0.20892427
+4 -2.5 -0.24998941
+4 -2 -0.2212778
+4 -1.5 -0.15458671
+4 -1 -0.081339179
+4 -0.5 -0.026779756
+4 0 -0.0068024953
+4 0.5 -0.026779756
+4 1 -0.081339179
+4 1.5 -0.15458671
+4 2 -0.2212778
+4 2.5 -0.24998941
+4 3 -0.20892427
+4 3.5 -0.073817185
+4 4 0.16382381
+4 4.5 0.49061243
+};
+\addplot3 [draw=black, line width=0.18pt]
+table {%
+4.5 -5 1.1792179
+4.5 -4.5 0.83068791
+4.5 -4 0.49061243
+4.5 -3.5 0.20004682
+4.5 -3 -0.017452726
+4.5 -2.5 -0.1566904
+4.5 -2 -0.22760364
+4.5 -1.5 -0.24951869
+4.5 -1 -0.24473952
+4.5 -0.5 -0.23299205
+4.5 0 -0.22753012
+4.5 0.5 -0.23299205
+4.5 1 -0.24473952
+4.5 1.5 -0.24951869
+4.5 2 -0.22760364
+4.5 2.5 -0.1566904
+4.5 3 -0.017452726
+4.5 3.5 0.20004682
+4.5 4 0.49061243
+4.5 4.5 0.83068791
+};
+\addplot3 [draw=navy, line width=0.36pt]
+table {%
+-3.4591891 -5 -0.2
+-3.5 -4.9716164 -0.2
+-4 -4.5783396 -0.2
+-4.0873153 -4.5 -0.2
+-4.5 -4.0873153 -0.2
+-4.5783396 -4 -0.2
+-4.9716164 -3.5 -0.2
+-5 -3.4591891 -0.2
+};
+\addplot3 [draw=navy, line width=0.36pt]
+table {%
+4.5 -4.0873153 -0.2
+4.0873153 -4.5 -0.2
+4 -4.5783396 -0.2
+3.5 -4.9716164 -0.2
+3.4591891 -5 -0.2
+};
+\addplot3 [draw=navy, line width=0.36pt]
+table {%
+-5 3.4591891 -0.2
+-4.9716164 3.5 -0.2
+-4.5783396 4 -0.2
+-4.5 4.0873153 -0.2
+-4.0873153 4.5 -0.2
+};
+\addplot3 [draw=navy, line width=0.36pt]
+table {%
+4.0873153 4.5 -0.2
+4.5 4.0873153 -0.2
+};
+\addplot3 [draw=navy, line width=0.36pt]
+table {%
+-1 -3.1960206 -0.2
+-0.5 -3.3100429 -0.2
+0 -3.3467349 -0.2
+0.5 -3.3100429 -0.2
+1 -3.1960206 -0.2
+1.4713152 -3 -0.2
+1.5 -2.987459 -0.2
+2 -2.6807038 -0.2
+2.2162206 -2.5 -0.2
+2.5 -2.2162206 -0.2
+2.6807038 -2 -0.2
+2.987459 -1.5 -0.2
+3 -1.4713152 -0.2
+3.1960206 -1 -0.2
+3.3100429 -0.5 -0.2
+3.3467349 0 -0.2
+3.3100429 0.5 -0.2
+3.1960206 1 -0.2
+3 1.4713152 -0.2
+2.987459 1.5 -0.2
+2.6807038 2 -0.2
+2.5 2.2162206 -0.2
+2.2162206 2.5 -0.2
+2 2.6807038 -0.2
+1.5 2.987459 -0.2
+1.4713152 3 -0.2
+1 3.1960206 -0.2
+0.5 3.3100429 -0.2
+0 3.3467349 -0.2
+-0.5 3.3100429 -0.2
+-1 3.1960206 -0.2
+-1.4713152 3 -0.2
+-1.5 2.987459 -0.2
+-2 2.6807038 -0.2
+-2.2162206 2.5 -0.2
+-2.5 2.2162206 -0.2
+-2.6807038 2 -0.2
+-2.987459 1.5 -0.2
+-3 1.4713152 -0.2
+-3.1960206 1 -0.2
+-3.3100429 0.5 -0.2
+-3.3467349 0 -0.2
+-3.3100429 -0.5 -0.2
+-3.1960206 -1 -0.2
+-3 -1.4713152 -0.2
+-2.987459 -1.5 -0.2
+-2.6807038 -2 -0.2
+-2.5 -2.2162206 -0.2
+-2.2162206 -2.5 -0.2
+-2 -2.6807038 -0.2
+-1.5 -2.987459 -0.2
+-1.4713152 -3 -0.2
+};
+\addplot3 [draw=darkorange, line width=0.36pt]
+table {%
+-3.7996391 -5 0
+-4 -4.8421646 0
+-4.3813676 -4.5 0
+-4.5 -4.3813676 0
+-4.8421646 -4 0
+-5 -3.7996391 0
+};
+\addplot3 [draw=darkorange, line width=0.36pt]
+table {%
+4.5 -4.3813676 0
+4.3813676 -4.5 0
+4 -4.8421646 0
+3.7996391 -5 0
+};
+\addplot3 [draw=darkorange, line width=0.36pt]
+table {%
+-5 3.7996391 0
+-4.8421646 4 0
+-4.5 4.3813676 0
+-4.3813676 4.5 0
+};
+\addplot3 [draw=darkorange, line width=0.36pt]
+table {%
+4.3813676 4.5 0
+4.5 4.3813676 0
+};
+\addplot3 [draw=darkorange, line width=0.36pt]
+table {%
+-0.5 -3.1033778 0
+0 -3.1434429 0
+0.5 -3.1033778 0
+0.91433779 -3 0
+1 -2.9772587 0
+1.5 -2.7576311 0
+1.8945309 -2.5 0
+2 -2.4185697 0
+2.4185697 -2 0
+2.5 -1.8945309 0
+2.7576311 -1.5 0
+2.9772587 -1 0
+3 -0.91433779 0
+3.1033778 -0.5 0
+3.1434429 0 0
+3.1033778 0.5 0
+3 0.91433779 0
+2.9772587 1 0
+2.7576311 1.5 0
+2.5 1.8945309 0
+2.4185697 2 0
+2 2.4185697 0
+1.8945309 2.5 0
+1.5 2.7576311 0
+1 2.9772587 0
+0.91433779 3 0
+0.5 3.1033778 0
+0 3.1434429 0
+-0.5 3.1033778 0
+-0.91433779 3 0
+-1 2.9772587 0
+-1.5 2.7576311 0
+-1.8945309 2.5 0
+-2 2.4185697 0
+-2.4185697 2 0
+-2.5 1.8945309 0
+-2.7576311 1.5 0
+-2.9772587 1 0
+-3 0.91433779 0
+-3.1033778 0.5 0
+-3.1434429 0 0
+-3.1033778 -0.5 0
+-3 -0.91433779 0
+-2.9772587 -1 0
+-2.7576311 -1.5 0
+-2.5 -1.8945309 0
+-2.4185697 -2 0
+-2 -2.4185697 0
+-1.8945309 -2.5 0
+-1.5 -2.7576311 0
+-1 -2.9772587 0
+-0.91433779 -3 0
+};
+\addplot3 [draw=darkorange, line width=0.36pt]
+table {%
+0 0 0
+0 0 0
+0 0 0
+0 0 0
+};
+\addplot3 [draw=crimson, line width=0.36pt]
+table {%
+-4.1297758 -5 0.2
+-4.5 -4.6711647 0.2
+-4.6711647 -4.5 0.2
+-5 -4.1297758 0.2
+};
+\addplot3 [draw=crimson, line width=0.36pt]
+table {%
+4.5 -4.6711647 0.2
+4.1297758 -5 0.2
+};
+\addplot3 [draw=crimson, line width=0.36pt]
+table {%
+-5 4.1297758 0.2
+-4.6711647 4.5 0.2
+};
+\addplot3 [draw=crimson, line width=0.36pt]
+table {%
+-1.5 -2.5278032 0.2
+-1 -2.7573614 0.2
+-0.5 -2.8908886 0.2
+0 -2.9356295 0.2
+0.5 -2.8908886 0.2
+1 -2.7573614 0.2
+1.5 -2.5278032 0.2
+1.5425772 -2.5 0.2
+2 -2.1468345 0.2
+2.1468345 -2 0.2
+2.5 -1.5425772 0.2
+2.5278032 -1.5 0.2
+2.7573614 -1 0.2
+2.8908886 -0.5 0.2
+2.9356295 0 0.2
+2.8908886 0.5 0.2
+2.7573614 1 0.2
+2.5278032 1.5 0.2
+2.5 1.5425772 0.2
+2.1468345 2 0.2
+2 2.1468345 0.2
+1.5425772 2.5 0.2
+1.5 2.5278032 0.2
+1 2.7573614 0.2
+0.5 2.8908886 0.2
+0 2.9356295 0.2
+-0.5 2.8908886 0.2
+-1 2.7573614 0.2
+-1.5 2.5278032 0.2
+-1.5425772 2.5 0.2
+-2 2.1468345 0.2
+-2.1468345 2 0.2
+-2.5 1.5425772 0.2
+-2.5278032 1.5 0.2
+-2.7573614 1 0.2
+-2.8908886 0.5 0.2
+-2.9356295 0 0.2
+-2.8908886 -0.5 0.2
+-2.7573614 -1 0.2
+-2.5278032 -1.5 0.2
+-2.5 -1.5425772 0.2
+-2.1468345 -2 0.2
+-2 -2.1468345 0.2
+-1.5425772 -2.5 0.2
+};
+\addplot3 [draw=crimson, line width=0.36pt]
+table {%
+0 -0.20858296 0.2
+-0.20858296 0 0.2
+0 0.20858296 0.2
+0.20858296 0 0.2
+};
+\addplot3 [draw=indigo68184, line width=0.24pt]
+table {%
+-3.4591891 -5 -0.85
+-3.5 -4.9716164 -0.85
+-4 -4.5783396 -0.85
+-4.0873153 -4.5 -0.85
+-4.5 -4.0873153 -0.85
+-4.5783396 -4 -0.85
+-4.9716164 -3.5 -0.85
+-5 -3.4591891 -0.85
+};
+\addplot3 [draw=indigo68184, line width=0.24pt]
+table {%
+4.5 -4.0873153 -0.85
+4.0873153 -4.5 -0.85
+4 -4.5783396 -0.85
+3.5 -4.9716164 -0.85
+3.4591891 -5 -0.85
+};
+\addplot3 [draw=indigo68184, line width=0.24pt]
+table {%
+-5 3.4591891 -0.85
+-4.9716164 3.5 -0.85
+-4.5783396 4 -0.85
+-4.5 4.0873153 -0.85
+-4.0873153 4.5 -0.85
+};
+\addplot3 [draw=indigo68184, line width=0.24pt]
+table {%
+4.0873153 4.5 -0.85
+4.5 4.0873153 -0.85
+};
+\addplot3 [draw=indigo68184, line width=0.24pt]
+table {%
+-1 -3.1960206 -0.85
+-0.5 -3.3100429 -0.85
+0 -3.3467349 -0.85
+0.5 -3.3100429 -0.85
+1 -3.1960206 -0.85
+1.4713152 -3 -0.85
+1.5 -2.987459 -0.85
+2 -2.6807038 -0.85
+2.2162206 -2.5 -0.85
+2.5 -2.2162206 -0.85
+2.6807038 -2 -0.85
+2.987459 -1.5 -0.85
+3 -1.4713152 -0.85
+3.1960206 -1 -0.85
+3.3100429 -0.5 -0.85
+3.3467349 0 -0.85
+3.3100429 0.5 -0.85
+3.1960206 1 -0.85
+3 1.4713152 -0.85
+2.987459 1.5 -0.85
+2.6807038 2 -0.85
+2.5 2.2162206 -0.85
+2.2162206 2.5 -0.85
+2 2.6807038 -0.85
+1.5 2.987459 -0.85
+1.4713152 3 -0.85
+1 3.1960206 -0.85
+0.5 3.3100429 -0.85
+0 3.3467349 -0.85
+-0.5 3.3100429 -0.85
+-1 3.1960206 -0.85
+-1.4713152 3 -0.85
+-1.5 2.987459 -0.85
+-2 2.6807038 -0.85
+-2.2162206 2.5 -0.85
+-2.5 2.2162206 -0.85
+-2.6807038 2 -0.85
+-2.987459 1.5 -0.85
+-3 1.4713152 -0.85
+-3.1960206 1 -0.85
+-3.3100429 0.5 -0.85
+-3.3467349 0 -0.85
+-3.3100429 -0.5 -0.85
+-3.1960206 -1 -0.85
+-3 -1.4713152 -0.85
+-2.987459 -1.5 -0.85
+-2.6807038 -2 -0.85
+-2.5 -2.2162206 -0.85
+-2.2162206 -2.5 -0.85
+-2 -2.6807038 -0.85
+-1.5 -2.987459 -0.85
+-1.4713152 -3 -0.85
+};
+\addplot3 [draw=darkcyan32144140, line width=0.24pt]
+table {%
+-3.7996391 -5 -0.85
+-4 -4.8421646 -0.85
+-4.3813676 -4.5 -0.85
+-4.5 -4.3813676 -0.85
+-4.8421646 -4 -0.85
+-5 -3.7996391 -0.85
+};
+\addplot3 [draw=darkcyan32144140, line width=0.24pt]
+table {%
+4.5 -4.3813676 -0.85
+4.3813676 -4.5 -0.85
+4 -4.8421646 -0.85
+3.7996391 -5 -0.85
+};
+\addplot3 [draw=darkcyan32144140, line width=0.24pt]
+table {%
+-5 3.7996391 -0.85
+-4.8421646 4 -0.85
+-4.5 4.3813676 -0.85
+-4.3813676 4.5 -0.85
+};
+\addplot3 [draw=darkcyan32144140, line width=0.24pt]
+table {%
+4.3813676 4.5 -0.85
+4.5 4.3813676 -0.85
+};
+\addplot3 [draw=darkcyan32144140, line width=0.24pt]
+table {%
+-0.5 -3.1033778 -0.85
+0 -3.1434429 -0.85
+0.5 -3.1033778 -0.85
+0.91433779 -3 -0.85
+1 -2.9772587 -0.85
+1.5 -2.7576311 -0.85
+1.8945309 -2.5 -0.85
+2 -2.4185697 -0.85
+2.4185697 -2 -0.85
+2.5 -1.8945309 -0.85
+2.7576311 -1.5 -0.85
+2.9772587 -1 -0.85
+3 -0.91433779 -0.85
+3.1033778 -0.5 -0.85
+3.1434429 0 -0.85
+3.1033778 0.5 -0.85
+3 0.91433779 -0.85
+2.9772587 1 -0.85
+2.7576311 1.5 -0.85
+2.5 1.8945309 -0.85
+2.4185697 2 -0.85
+2 2.4185697 -0.85
+1.8945309 2.5 -0.85
+1.5 2.7576311 -0.85
+1 2.9772587 -0.85
+0.91433779 3 -0.85
+0.5 3.1033778 -0.85
+0 3.1434429 -0.85
+-0.5 3.1033778 -0.85
+-0.91433779 3 -0.85
+-1 2.9772587 -0.85
+-1.5 2.7576311 -0.85
+-1.8945309 2.5 -0.85
+-2 2.4185697 -0.85
+-2.4185697 2 -0.85
+-2.5 1.8945309 -0.85
+-2.7576311 1.5 -0.85
+-2.9772587 1 -0.85
+-3 0.91433779 -0.85
+-3.1033778 0.5 -0.85
+-3.1434429 0 -0.85
+-3.1033778 -0.5 -0.85
+-3 -0.91433779 -0.85
+-2.9772587 -1 -0.85
+-2.7576311 -1.5 -0.85
+-2.5 -1.8945309 -0.85
+-2.4185697 -2 -0.85
+-2 -2.4185697 -0.85
+-1.8945309 -2.5 -0.85
+-1.5 -2.7576311 -0.85
+-1 -2.9772587 -0.85
+-0.91433779 -3 -0.85
+};
+\addplot3 [draw=darkcyan32144140, line width=0.24pt]
+table {%
+0 0 -0.85
+0 0 -0.85
+0 0 -0.85
+0 0 -0.85
+};
+\addplot3 [draw=gold25323136, line width=0.24pt]
+table {%
+-4.1297758 -5 -0.85
+-4.5 -4.6711647 -0.85
+-4.6711647 -4.5 -0.85
+-5 -4.1297758 -0.85
+};
+\addplot3 [draw=gold25323136, line width=0.24pt]
+table {%
+4.5 -4.6711647 -0.85
+4.1297758 -5 -0.85
+};
+\addplot3 [draw=gold25323136, line width=0.24pt]
+table {%
+-5 4.1297758 -0.85
+-4.6711647 4.5 -0.85
+};
+\addplot3 [draw=gold25323136, line width=0.24pt]
+table {%
+-1.5 -2.5278032 -0.85
+-1 -2.7573614 -0.85
+-0.5 -2.8908886 -0.85
+0 -2.9356295 -0.85
+0.5 -2.8908886 -0.85
+1 -2.7573614 -0.85
+1.5 -2.5278032 -0.85
+1.5425772 -2.5 -0.85
+2 -2.1468345 -0.85
+2.1468345 -2 -0.85
+2.5 -1.5425772 -0.85
+2.5278032 -1.5 -0.85
+2.7573614 -1 -0.85
+2.8908886 -0.5 -0.85
+2.9356295 0 -0.85
+2.8908886 0.5 -0.85
+2.7573614 1 -0.85
+2.5278032 1.5 -0.85
+2.5 1.5425772 -0.85
+2.1468345 2 -0.85
+2 2.1468345 -0.85
+1.5425772 2.5 -0.85
+1.5 2.5278032 -0.85
+1 2.7573614 -0.85
+0.5 2.8908886 -0.85
+0 2.9356295 -0.85
+-0.5 2.8908886 -0.85
+-1 2.7573614 -0.85
+-1.5 2.5278032 -0.85
+-1.5425772 2.5 -0.85
+-2 2.1468345 -0.85
+-2.1468345 2 -0.85
+-2.5 1.5425772 -0.85
+-2.5278032 1.5 -0.85
+-2.7573614 1 -0.85
+-2.8908886 0.5 -0.85
+-2.9356295 0 -0.85
+-2.8908886 -0.5 -0.85
+-2.7573614 -1 -0.85
+-2.5278032 -1.5 -0.85
+-2.5 -1.5425772 -0.85
+-2.1468345 -2 -0.85
+-2 -2.1468345 -0.85
+-1.5425772 -2.5 -0.85
+};
+\addplot3 [draw=gold25323136, line width=0.24pt]
+table {%
+0 -0.20858296 -0.85
+-0.20858296 0 -0.85
+0 0.20858296 -0.85
+0.20858296 0 -0.85
+};
+\end{axis}
+
+\end{tikzpicture}
diff --git a/tests/test_escape_chars.py b/tests/test_escape_chars.py
index 70c31422..edaccabd 100644
--- a/tests/test_escape_chars.py
+++ b/tests/test_escape_chars.py
@@ -3,11 +3,13 @@
https://github.com/nschloe/tikzplotlib/issues/332
"""
+from collections.abc import Callable
+
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
-from .helpers import assert_equality
+import matplot2tikz
mpl.use("Agg")
@@ -21,5 +23,66 @@ def plot() -> Figure:
return fig
-def test() -> None:
- assert_equality(plot, "test_escape_chars_reference.tex")
+def _get_tikz_code(plot_func: Callable[[], Figure]) -> str:
+ plot_func()
+ code = matplot2tikz.get_tikz_code(
+ include_disclaimer=False,
+ float_format=".8g",
+ )
+ plt.close("all")
+ return code
+
+
+def test_text_mode_escaping() -> None:
+ code = _get_tikz_code(plot)
+ escaped = "Foo \\& Bar Dogs\\_N\\_Cats \\%"
+ assert f"title={{{escaped}}}" in code
+ assert f"xlabel={{{escaped}}}" in code
+ assert f"ylabel={{{escaped}}}" in code
+
+
+def plot_math_label() -> Figure:
+ fig = plt.figure()
+ plt.plot([0.1, 10.0], [0.0, 1.0])
+ plt.xscale("log")
+ plt.xlabel(r"$\\log_{10} \\mathrm{X}$")
+ return fig
+
+
+def test_math_mode_underscore_not_escaped() -> None:
+ code = _get_tikz_code(plot_math_label)
+ xlabel_line = next(line for line in code.splitlines() if line.strip().startswith("xlabel="))
+ assert r"\\log_{10} \\mathrm{X}" in xlabel_line
+ assert r"\\log\\_{10}" not in xlabel_line
+
+
+def plot_escaped_dollar() -> Figure:
+ fig = plt.figure()
+ plt.plot([0.0, 1.0], [0.0, 1.0])
+ plt.xlabel(r"$\text{Price } \${}100 \approx 86\,\text{\euro}_{\text{in mai 2026}}$")
+ return fig
+
+
+def test_escaped_dollar_is_literal() -> None:
+ code = _get_tikz_code(plot_escaped_dollar)
+ xlabel_line = next(line for line in code.splitlines() if line.strip().startswith("xlabel="))
+ assert (
+ r"\(\displaystyle \text{Price } \${}100 \approx 86\,"
+ r"\text{\euro}_{\text{in mai 2026}}\)" in xlabel_line
+ )
+
+
+def plot_multiple_math_segments() -> Figure:
+ fig = plt.figure()
+ plt.plot([0.0, 1.0], [0.0, 1.0])
+ plt.xlabel(r"gain $x_1$ vs $x_2$")
+ return fig
+
+
+def test_multiple_math_segments() -> None:
+ code = _get_tikz_code(plot_multiple_math_segments)
+ xlabel_line = next(line for line in code.splitlines() if line.strip().startswith("xlabel="))
+ assert r"\(\displaystyle x_1\)" in xlabel_line
+ assert r"\(\displaystyle x_2\)" in xlabel_line
+ assert r"x\_1" not in xlabel_line
+ assert r"x\_2" not in xlabel_line
diff --git a/tests/test_externalize_tables_reference.tex b/tests/test_externalize_tables_reference.tex
index b91a5021..71dcc9df 100644
--- a/tests/test_externalize_tables_reference.tex
+++ b/tests/test_externalize_tables_reference.tex
@@ -23,9 +23,9 @@
ytick style={color=dimgray85}
]
\addplot [semithick, greenyellow17025576, mark=*, mark size=3, mark options={solid}]
-table {}{tmp-000.dat};
+table {tmp-000.dat};
\addplot [very thick, chocolate2267451, opacity=0.3, mark=*, mark size=3, mark options={solid}]
-table {}{tmp-001.dat};
+table {tmp-001.dat};
\end{axis}
\end{tikzpicture}
diff --git a/tests/test_scatter_colormap_reference.tex b/tests/test_scatter_colormap_reference.tex
index 1b7bd50c..040118c4 100644
--- a/tests/test_scatter_colormap_reference.tex
+++ b/tests/test_scatter_colormap_reference.tex
@@ -1,6 +1,7 @@
\begin{tikzpicture}
\definecolor{darkgray176}{RGB}{176,176,176}
+\definecolor{steelblue31119180}{RGB}{31,119,180}
\begin{axis}[
tick align=outside,
@@ -14,6 +15,8 @@
]
\addplot [
colormap/viridis,
+ draw=steelblue31119180,
+ mark=*,
only marks,
scatter,
scatter src=explicit,
@@ -22,16 +25,16 @@
]
table [x=x, y=y, meta=colordata]{%
x y colordata sizedata
--0.98912135 0.097167319 -0.3617668721609232 3.133157971456955
--0.36778665 -1.5259304 -1.230232195490445 2.817072624262411
-1.2879253 1.1921661 1.2262292928211507 4.1765658564645
-0.19397442 -0.67108968 -2.1720438866851817 4.458355091802991
-0.9202309 1.0002694 -0.37014734585231535 2.909294279310992
-0.57710379 0.13632112 0.16438006967466792 1.8999491559296784
--0.63646365 1.5320331 0.8598811846127368 4.08412434479352
-0.54195222 -0.65996941 1.761661236511811 4.903644914098962
--0.31659545 -0.31179486 0.993323775951811 5.431304123364008
--0.32238912 0.33776913 -0.29152142609843873 4.593835880515875
+-0.98912135 0.097167319 -0.36176687 3.133158
+-0.36778665 -1.5259304 -1.2302322 2.8170726
+1.2879253 1.1921661 1.2262293 4.1765659
+0.19397442 -0.67108968 -2.1720439 4.4583551
+0.9202309 1.0002694 -0.37014735 2.9092943
+0.57710379 0.13632112 0.16438007 1.8999492
+-0.63646365 1.5320331 0.85988118 4.0841243
+0.54195222 -0.65996941 1.7616612 4.9036449
+-0.31659545 -0.31179486 0.99332378 5.4313041
+-0.32238912 0.33776913 -0.29152143 4.5938359
};
\end{axis}
diff --git a/tests/test_scatter_different_sizes_reference.tex b/tests/test_scatter_different_sizes_reference.tex
index 0ad33694..aa51801c 100644
--- a/tests/test_scatter_different_sizes_reference.tex
+++ b/tests/test_scatter_different_sizes_reference.tex
@@ -22,9 +22,9 @@
]
table{%
x y sizedata
-1 5 9.772050238058398
-2 7 9.772050238058398
-3 1 9.772050238058398
+1 5 9.7720502
+2 7 9.7720502
+3 1 9.7720502
};
\end{axis}