From 51353c7ffdd2ab50523d6c6900cd22601aed6535 Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Wed, 8 Apr 2026 18:07:18 +0200 Subject: [PATCH 1/8] [fix] ensure export directory is correct for heatmap --- src/tikzplotly/_heatmap.py | 5 +++-- src/tikzplotly/_save.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/tikzplotly/_heatmap.py b/src/tikzplotly/_heatmap.py index afaf6bd..b999927 100755 --- a/src/tikzplotly/_heatmap.py +++ b/src/tikzplotly/_heatmap.py @@ -102,9 +102,10 @@ def draw_heatmap(data, fig, img_name, axis: Axis): img_bytes = fig_copy.to_image(format="png") # The image created by plotly keeps places around the heatmap cropped_image = crop_image(Image.open(io.BytesIO(img_bytes))) # so we crop all the white around the figure - resized_image = resize_image(cropped_image, *figure_data.shape) # and we resize it so each square is a 1px x 1px square + resized_image = resize_image(cropped_image, *figure_data.shape) # and we resize it so each square is a 1px x 1px square - os.makedirs(os.path.dirname(img_name), exist_ok=True) # Make sure the directory exists + dirname = os.path.dirname(img_name) or '.' # Ensure that dirname is not empty + os.makedirs(dirname, exist_ok=True) # Make sure the directory exists resized_image.save(img_name) diff --git a/src/tikzplotly/_save.py b/src/tikzplotly/_save.py index a4e4390..5d880aa 100755 --- a/src/tikzplotly/_save.py +++ b/src/tikzplotly/_save.py @@ -78,13 +78,13 @@ def get_tikz_code( # If x is textual => symbolic x coords if all(isinstance(v, str) for v in trace.x): - sanitized_trace_x = [sanitize_text(x, keep_space=-1) for x in trace.x] + sanitized_trace_x = [sanitize_text(str(x), keep_space=-1) for x in trace.x] axis.add_option("symbolic x coords", "{" + ",".join(sanitized_trace_x) + "}") axis.add_option("xtick", "data") # If y is textual => symbolic y coords if all(isinstance(v, str) for v in trace.y): - sanitized_trace_y = [sanitize_text(y, keep_space=-1) for y in trace.y] + sanitized_trace_y = [sanitize_text(str(y), keep_space=-1) for y in trace.y] axis.add_option("symbolic y coords", "{" + ",".join(sanitized_trace_y) + "}") axis.add_option("ytick", "data") From f6c48dc15bfd96b301e1749854fdd96d5a2e3422 Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Thu, 9 Apr 2026 09:21:44 +0200 Subject: [PATCH 2/8] [fix+tests] handle correclty plot type mark+lines and add color tests #31 --- requirements.txt | 2 +- src/tikzplotly/_color.py | 19 +++++---- src/tikzplotly/_scatter.py | 15 +++---- tests/test_colors.py | 41 +++++++++++++++++++ .../test_colors_colors-dict-hit_reference.tex | 17 ++++++++ .../test_colors/test_colors_hex_reference.tex | 17 ++++++++ .../test_colors_named-color_reference.tex | 16 ++++++++ .../test_colors_non-string_reference.tex | 16 ++++++++ .../test_colors_none_reference.tex | 14 +++++++ .../test_colors_numpy-array_reference.tex | 16 ++++++++ .../test_colors/test_colors_rgb_reference.tex | 17 ++++++++ ...est_colors_rgba-with-opacity_reference.tex | 17 ++++++++ ..._colors_rgba-without-opacity_reference.tex | 17 ++++++++ tox.ini | 2 +- 14 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 tests/test_colors.py create mode 100644 tests/test_colors/test_colors_colors-dict-hit_reference.tex create mode 100644 tests/test_colors/test_colors_hex_reference.tex create mode 100644 tests/test_colors/test_colors_named-color_reference.tex create mode 100644 tests/test_colors/test_colors_non-string_reference.tex create mode 100644 tests/test_colors/test_colors_none_reference.tex create mode 100644 tests/test_colors/test_colors_numpy-array_reference.tex create mode 100644 tests/test_colors/test_colors_rgb_reference.tex create mode 100644 tests/test_colors/test_colors_rgba-with-opacity_reference.tex create mode 100644 tests/test_colors/test_colors_rgba-without-opacity_reference.tex diff --git a/requirements.txt b/requirements.txt index 58bc7e0..9c4c3f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy plotly pillow -kaleido +kaleido<1.0.0 tox pandas mkdocs-material[recommended] diff --git a/src/tikzplotly/_color.py b/src/tikzplotly/_color.py index 21ed038..21d9b9b 100644 --- a/src/tikzplotly/_color.py +++ b/src/tikzplotly/_color.py @@ -30,7 +30,7 @@ def convert_color(color): string from plotly color palette, can be one of the following: - hex string : "#______" - rgb string : "rgb(__, __, __)" with values between 0 and 255 - - rgba string : "rgba(__, __, __, __)" with values between 0 and 255 and an opacity value between 0 and 1 + - rgba string : "rgba(__, __, __[, __])" with values between 0 and 255 and an opacity value between 0 and 1 (optional) - color name : "red", "green", "blue", "yellow", "orange", "purple", "brown", "black", "gray", "white" or any other color name from https://github.com/plotly/plotly.py/blob/main/templategen/utils/colors.py @@ -45,20 +45,21 @@ def convert_color(color): if color is None: return None, None, None, 1 if isinstance(color, numpy.ndarray): - warn("Color from data is not supported yet. Returning the default color: blue.") - return "blue", "HTML", "0000ff", 1 + warn("Color from data is not supported yet. Returning no color.") + return None, None, None, 1 if not isinstance(color, str): - warn(f"Color {color} type '{color.__class__.__name__}' is not supported yet. Returning the default color: blue.") - return "blue", "HTML", "0000ff", 1 + warn(f"Color {color} type '{color.__class__.__name__}' is not supported yet. Returning no color.") + return None, None, None, 1 if color.startswith("#"): return color[1:], "HTML", color[1:], 1 if color.startswith("rgba"): - sp = color.split("(")[1].split(",") - rgb_color = sp[:-1] - rgb_color = convert_color(f"rgb({rgb_color[0]},{rgb_color[1]},{rgb_color[2]})")[:-1] - return rgb_color + (float(sp[-1][:-1]),) + sp = color.split("(")[1].replace(')', '').split(",") + rgb_color = [s.strip() for s in sp] + r, g, b = int(rgb_color[0]), int(rgb_color[1]), int(rgb_color[2]) + opacity = float(rgb_color[3]) if len(rgb_color) == 4 else 1.0 + return hashlib.sha1(f"{r}, {g}, {b}".encode('UTF-8')).hexdigest()[:10], "RGB", rgb_str(r, g, b), opacity if color.startswith("rgb"): color = color[4:-1].replace("[", "{").replace("]", "}") diff --git a/src/tikzplotly/_scatter.py b/src/tikzplotly/_scatter.py index a064a8a..d766ea1 100644 --- a/src/tikzplotly/_scatter.py +++ b/src/tikzplotly/_scatter.py @@ -53,15 +53,17 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): options_dict = {} mark_option_dict = {} - if mode == "markers": + if "markers" in mode: if marker.symbol is not None: symbol, symbol_options = marker_symbol_to_tex(marker.symbol) options_dict["mark"] = symbol - options_dict["only marks"] = None + if "lines" not in mode: + options_dict["only marks"] = None if symbol_options is not None: mark_option_dict[symbol_options[0]] = symbol_options[1] else: - options_dict["only marks"] = None + if "lines" not in mode: + options_dict["only marks"] = None if scatter.marker.size is not None: options_dict["mark size"] = px_to_pt(marker.size) @@ -93,13 +95,6 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): elif mode == "lines": options_dict["mark"] = "none" - elif "lines" in mode and "markers" in mode: - if marker.symbol is not None: - symbol, symbol_options = marker_symbol_to_tex(marker.symbol) - options_dict["mark"] = symbol - if symbol_options is not None: - mark_option_dict[symbol_options[0]] = symbol_options[1] - else: warn(f"Scatter : Mode {mode} is not supported yet.") diff --git a/tests/test_colors.py b/tests/test_colors.py new file mode 100644 index 0000000..117a497 --- /dev/null +++ b/tests/test_colors.py @@ -0,0 +1,41 @@ +import os +import pathlib + +import numpy +import plotly.graph_objects as go +import pytest + +from .helpers import assert_equality + + +this_dir = pathlib.Path(__file__).resolve().parent +test_name = "test_colors" + + +def plot_color(color_scheme): + fig = go.Figure() + fig.add_trace(go.Scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16], marker_color=color_scheme)) + return fig + + +@pytest.mark.parametrize( + "color, warning_match", + [ + pytest.param(None, None, id="none"), + pytest.param(numpy.array([1, 2, 3]), "Color from data is not supported yet", id="numpy-array"), # ("blue", "HTML", "0000ff", 1) + pytest.param(123, "Color 123 type 'int' is not supported yet", id="non-string"), # ("blue", "HTML", "0000ff", 1) + pytest.param("#ffb6c1", None, id="hex"), + pytest.param("rgba(255, 182, 193, .2)", None, id="rgba-with-opacity"), + pytest.param("rgba(255, 182, 193)", None, id="rgba-without-opacity"), + pytest.param("rgb(255, 182, 193)", None, id="rgb"), + pytest.param("red", None, id="named-color"), + pytest.param("LightBlue", None, id="colors-dict-hit"), + ], +) +def test_color(color, warning_match, request): + id = request.node.callspec.id + if warning_match is None: + assert_equality(plot_color(color), os.path.join(this_dir, test_name, f"{test_name}_{id}_reference.tex")) + else: + with pytest.warns(UserWarning, match=warning_match): + assert_equality(plot_color(color), os.path.join(this_dir, test_name, f"{test_name}_{id}_reference.tex")) diff --git a/tests/test_colors/test_colors_colors-dict-hit_reference.tex b/tests/test_colors/test_colors_colors-dict-hit_reference.tex new file mode 100644 index 0000000..a37cbd4 --- /dev/null +++ b/tests/test_colors/test_colors_colors-dict-hit_reference.tex @@ -0,0 +1,17 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + +\definecolor{lightblue}{RGB}{173, 216, 230} + +\begin{axis} +\addplot+ [mark options={solid, fill=lightblue}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_hex_reference.tex b/tests/test_colors/test_colors_hex_reference.tex new file mode 100644 index 0000000..03764a3 --- /dev/null +++ b/tests/test_colors/test_colors_hex_reference.tex @@ -0,0 +1,17 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + +\definecolor{ffb6c1}{HTML}{ffb6c1} + +\begin{axis} +\addplot+ [mark options={solid, fill=ffb6c1}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_named-color_reference.tex b/tests/test_colors/test_colors_named-color_reference.tex new file mode 100644 index 0000000..791ec3f --- /dev/null +++ b/tests/test_colors/test_colors_named-color_reference.tex @@ -0,0 +1,16 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + + +\begin{axis} +\addplot+ [mark options={solid, fill=red}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_non-string_reference.tex b/tests/test_colors/test_colors_non-string_reference.tex new file mode 100644 index 0000000..b3d5f3d --- /dev/null +++ b/tests/test_colors/test_colors_non-string_reference.tex @@ -0,0 +1,16 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + + +\begin{axis} +\addplot+ [mark options={solid, fill}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_none_reference.tex b/tests/test_colors/test_colors_none_reference.tex new file mode 100644 index 0000000..75785aa --- /dev/null +++ b/tests/test_colors/test_colors_none_reference.tex @@ -0,0 +1,14 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} +\begin{axis} +\addplot+ table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_numpy-array_reference.tex b/tests/test_colors/test_colors_numpy-array_reference.tex new file mode 100644 index 0000000..b3d5f3d --- /dev/null +++ b/tests/test_colors/test_colors_numpy-array_reference.tex @@ -0,0 +1,16 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + + +\begin{axis} +\addplot+ [mark options={solid, fill}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_rgb_reference.tex b/tests/test_colors/test_colors_rgb_reference.tex new file mode 100644 index 0000000..bcb9a51 --- /dev/null +++ b/tests/test_colors/test_colors_rgb_reference.tex @@ -0,0 +1,17 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + +\definecolor{8d466771e5}{RGB}{255, 182, 193} + +\begin{axis} +\addplot+ [mark options={solid, fill=8d466771e5}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_rgba-with-opacity_reference.tex b/tests/test_colors/test_colors_rgba-with-opacity_reference.tex new file mode 100644 index 0000000..bcb9a51 --- /dev/null +++ b/tests/test_colors/test_colors_rgba-with-opacity_reference.tex @@ -0,0 +1,17 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + +\definecolor{8d466771e5}{RGB}{255, 182, 193} + +\begin{axis} +\addplot+ [mark options={solid, fill=8d466771e5}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tests/test_colors/test_colors_rgba-without-opacity_reference.tex b/tests/test_colors/test_colors_rgba-without-opacity_reference.tex new file mode 100644 index 0000000..bcb9a51 --- /dev/null +++ b/tests/test_colors/test_colors_rgba-without-opacity_reference.tex @@ -0,0 +1,17 @@ +\pgfplotstableread{ +x y0 +0 0 +1 1 +2 4 +3 9 +4 16 +}\dataA + +\begin{tikzpicture} + +\definecolor{8d466771e5}{RGB}{255, 182, 193} + +\begin{axis} +\addplot+ [mark options={solid, fill=8d466771e5}] table[y=y0] {\dataA}; +\end{axis} +\end{tikzpicture} diff --git a/tox.ini b/tox.ini index cb3d533..741a24d 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,6 @@ deps = pytest-codeblocks plotly pytest-randomly - kaleido + kaleido<1.0.0 commands = pytest {posargs} \ No newline at end of file From a9292fe8f4220e8275398cd9725d55d497869fd7 Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Thu, 9 Apr 2026 09:24:52 +0200 Subject: [PATCH 3/8] fix failing test --- tests/test_scatter3d/test_scatter3d_2_markers_reference.tex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_scatter3d/test_scatter3d_2_markers_reference.tex b/tests/test_scatter3d/test_scatter3d_2_markers_reference.tex index 44b1c57..13c7884 100644 --- a/tests/test_scatter3d/test_scatter3d_2_markers_reference.tex +++ b/tests/test_scatter3d/test_scatter3d_2_markers_reference.tex @@ -104,9 +104,8 @@ \begin{tikzpicture} -\definecolor{blue}{HTML}{0000ff} \begin{axis} -\addplot3+ [mark=diamond*, only marks, mark size=9, mark options={solid, fill=blue, opacity=0.8}] table[x=x, y=y, z=z] {\dataDFHNIDGLEFPCLNOOILBDMAJGJEBAMDGP}; +\addplot3+ [mark=diamond*, only marks, mark size=9, mark options={solid, fill, opacity=0.8}] table[x=x, y=y, z=z] {\dataDFHNIDGLEFPCLNOOILBDMAJGJEBAMDGP}; \end{axis} \end{tikzpicture} From e1aa83cdd5d881d09e92a08a7d0f4d3696627f5d Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Thu, 9 Apr 2026 18:40:13 +0200 Subject: [PATCH 4/8] [fix] do the same for 3d scatter #31 --- src/tikzplotly/_scatter3d.py | 16 +++++----------- .../test_scatter3d_2_markers+lines_reference.tex | 4 +++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/tikzplotly/_scatter3d.py b/src/tikzplotly/_scatter3d.py index 9ac833e..d338e64 100644 --- a/src/tikzplotly/_scatter3d.py +++ b/src/tikzplotly/_scatter3d.py @@ -28,16 +28,17 @@ def draw_scatter3d(data_name, scatter, color_set): options_dict = {} mark_option_dict = {} - # Markers only - if mode == "markers": + if "markers" in mode: if marker.symbol is not None: symbol, symbol_options = marker_symbol_to_tex(marker.symbol) options_dict["mark"] = symbol - options_dict["only marks"] = None + if "lines" not in mode: + options_dict["only marks"] = None if symbol_options is not None: mark_option_dict[symbol_options[0]] = symbol_options[1] else: - options_dict["only marks"] = None + if "lines" not in mode: + options_dict["only marks"] = None if marker.size is not None: size = marker.size @@ -72,13 +73,6 @@ def draw_scatter3d(data_name, scatter, color_set): elif mode == "lines": options_dict["mark"] = "none" - elif "lines" in mode and "markers" in mode: - if marker.symbol is not None: - symbol, symbol_options = marker_symbol_to_tex(marker.symbol) - options_dict["mark"] = symbol - if symbol_options is not None: - mark_option_dict[symbol_options[0]] = symbol_options[1] - else: warn(f"Scatter3d : Mode {mode} is not supported yet.") diff --git a/tests/test_scatter3d/test_scatter3d_2_markers+lines_reference.tex b/tests/test_scatter3d/test_scatter3d_2_markers+lines_reference.tex index 019173b..ffca7e9 100644 --- a/tests/test_scatter3d/test_scatter3d_2_markers+lines_reference.tex +++ b/tests/test_scatter3d/test_scatter3d_2_markers+lines_reference.tex @@ -103,7 +103,9 @@ }{\dataDFHNIDGLEFPCLNOOILBDMAJGJEBAMDGP} \begin{tikzpicture} + + \begin{axis} -\addplot3+ [mark=diamond*] table[x=x, y=y, z=z] {\dataDFHNIDGLEFPCLNOOILBDMAJGJEBAMDGP}; +\addplot3+ [mark=diamond*, mark size=9, mark options={solid, fill, opacity=0.8}] table[x=x, y=y, z=z] {\dataDFHNIDGLEFPCLNOOILBDMAJGJEBAMDGP}; \end{axis} \end{tikzpicture} From 1e698f7b801ee1927e04bd955325cc203f1906e9 Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Sat, 11 Apr 2026 08:52:08 +0200 Subject: [PATCH 5/8] [tests] improve testing process by catching warning and explain it in doc #36 --- .gitignore | 6 ++++-- docs/develop/contributing.md | 16 +++++++++++----- docs/develop/tests.md | 2 +- pyproject.toml | 7 +++++++ requirements.txt | 5 ++++- src/tikzplotly/_histogram.py | 3 +++ tests/test_colors.py | 18 +++++++++++++++--- ...olors_transparent_background_reference.tex} | 0 tests/test_heatmap.py | 14 +++++++++----- tests/test_histograms.py | 11 +++++++++-- tests/test_markers.py | 8 +++++++- tests/test_polar.py | 4 +++- tests/test_scatter.py | 14 ++++++++------ tests/test_scatter3d.py | 12 ++++++++++-- tests/test_specific.py | 16 +++++----------- tox.ini | 10 ++-------- 16 files changed, 98 insertions(+), 48 deletions(-) rename tests/{test_specific/test_specific_transparent_background_reference.tex => test_colors/test_colors_transparent_background_reference.tex} (100%) diff --git a/.gitignore b/.gitignore index c3f47ff..ea63bf1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ src/tests/outputs/** # Doxygen output doxygen/* - # Virtual environment .venv/ *.egg-info/ @@ -29,4 +28,7 @@ dist/ # Tox .tox/ .coverage -coverage.xml \ No newline at end of file +coverage.xml + +# Mkdocs site +site/** diff --git a/docs/develop/contributing.md b/docs/develop/contributing.md index 9bcdd2f..fe96318 100644 --- a/docs/develop/contributing.md +++ b/docs/develop/contributing.md @@ -10,7 +10,7 @@ You can also submit a pull request with the desired feature or the correction of ## Development -The actual code maybe quite messy (is with your contribution you manage to imrpove it, I thank you in advance !). +The actual code maybe quite messy (is with your contribution you manage to improve it, I thank you in advance !). The source code of the package are present in the directory `src/tikzplotly`, in which each file is dedicated to a specific feature of the library. Some external packages are necessary to make tikzplotly work, that are specified in the `requirements.txt` file. @@ -40,13 +40,19 @@ tox -- --cov tikzplotly --cov-report html --cov-report term The code coverage is available in the directory `htmlcov`. -!!! info "Note about coverage" +!!! Tip "Note about coverage" The coverage CI is quite strict, so you need to cover all the modification to pass it. - I found that tedious at first, but actually making it pass make me realize that there was some bogs in the code! + I found that tedious at first, but actually making it pass make me realize that there were some bogs in the code! + +??? Info "Pytest" + Some warnings are ignored by `pytest`, as they are intended for the user in some specific cases, that are not relevant in general test cases. + This corresponds to: + - the warning raised when [heat maps](../plot/supported/#heat-maps) PNG file is not resized (cf [issue comment](https://github.com/thomas-saigre/tikzplotly/issues/6#issuecomment-2106180586)), + - warning for features that are not yet implemented (such as text templates, or `barpolar` plots). ## Documentation -Feel free to add some comments on this page, espacially if there are some notable differences between plotly and pgfplots (see [this page](../plot/NB.md)). -This pages are written in Markdown and are present in the directory `docs`. +Feel free to add some comments on this page, especially if there are some notable differences between plotly and pgfplots (see [this page](../plot/NB.md)). +These pages are written in Markdown and are present in the directory `docs`. The site is build using [Mkdocs-materials](https://squidfunk.github.io/mkdocs-material/). diff --git a/docs/develop/tests.md b/docs/develop/tests.md index af8f468..2dabd97 100644 --- a/docs/develop/tests.md +++ b/docs/develop/tests.md @@ -2,7 +2,7 @@ ## Local tests -These tests are for development purpose, to ensure that the features that are developped are working as expected. +These tests are for development purpose, to ensure that the features that are developed are working as expected. They are present in the directory `src/tests` and can be run with the following command, from the `src` directory. ```bash diff --git a/pyproject.toml b/pyproject.toml index 688078b..463c831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,3 +36,10 @@ dev = ["pandas"] [project.urls] Code = "https://github.com/thomas-saigre/tikzplotly" Issues = "https://github.com/thomas-saigre/tikzplotly/issues" + + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore:png image has not been reduced.*:UserWarning", + "ignore:Text template is not supported yet.:UserWarning", +] diff --git a/requirements.txt b/requirements.txt index 9c4c3f3..8fe6378 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,11 @@ numpy plotly pillow -kaleido<1.0.0 +kaleido tox pandas mkdocs-material[recommended] mkdocs-git-revision-date-localized-plugin +pytest +pytest-cov +pytest-codeblocks diff --git a/src/tikzplotly/_histogram.py b/src/tikzplotly/_histogram.py index 6be3c80..6cc4187 100644 --- a/src/tikzplotly/_histogram.py +++ b/src/tikzplotly/_histogram.py @@ -92,6 +92,9 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"): axis.add_option("x filter/.expression", "rawy") axis.add_option("y filter/.expression", "rawx") hist_options["handler/.style"] = "{xbar interval}" + else: # trace.x and trace.y are both empty + warn("Normally, we should reach this line") + data_str = "" diff --git a/tests/test_colors.py b/tests/test_colors.py index 117a497..61cf64f 100644 --- a/tests/test_colors.py +++ b/tests/test_colors.py @@ -1,8 +1,10 @@ import os import pathlib +from contextlib import nullcontext import numpy import plotly.graph_objects as go +import plotly.express as px import pytest from .helpers import assert_equality @@ -17,6 +19,12 @@ def plot_color(color_scheme): fig.add_trace(go.Scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16], marker_color=color_scheme)) return fig +def plot_transparent_background(): + fig = px.scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16]) + fig.update_layout(plot_bgcolor='rgba(255, 182, 193, .5)') + + return fig + @pytest.mark.parametrize( "color, warning_match", @@ -35,7 +43,11 @@ def plot_color(color_scheme): def test_color(color, warning_match, request): id = request.node.callspec.id if warning_match is None: - assert_equality(plot_color(color), os.path.join(this_dir, test_name, f"{test_name}_{id}_reference.tex")) + context = nullcontext() else: - with pytest.warns(UserWarning, match=warning_match): - assert_equality(plot_color(color), os.path.join(this_dir, test_name, f"{test_name}_{id}_reference.tex")) + context = pytest.warns(UserWarning, match=warning_match) + with context: + assert_equality(plot_color(color), os.path.join(this_dir, test_name, f"{test_name}_{id}_reference.tex")) + +def test_transparent_background(): + assert_equality(plot_transparent_background(), os.path.join(this_dir, test_name, test_name + "_transparent_background_reference.tex")) diff --git a/tests/test_specific/test_specific_transparent_background_reference.tex b/tests/test_colors/test_colors_transparent_background_reference.tex similarity index 100% rename from tests/test_specific/test_specific_transparent_background_reference.tex rename to tests/test_colors/test_colors_transparent_background_reference.tex diff --git a/tests/test_heatmap.py b/tests/test_heatmap.py index 56c795e..bb5c676 100644 --- a/tests/test_heatmap.py +++ b/tests/test_heatmap.py @@ -1,6 +1,7 @@ import plotly.express as px import plotly.graph_objects as go import numpy as np +import pytest import os from .helpers import assert_equality import pathlib @@ -22,6 +23,7 @@ def plot_2(): return fig def plot_3(): + # A plot with no colorscale fig = go.Figure(data=go.Heatmap( z=[[1, None, 30, 50, 1], [20, 1, 60, 80, 30], [30, 60, 1, -10, 20]], x=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], @@ -35,7 +37,7 @@ def plot_4(): programmers = ['Alex','Nicole','Sara','Etienne','Chelsea','Jody','Marianne'] base = datetime.datetime(2021, 7, 20, 19, 30, 0) - dates = base - np.arange(180) * datetime.timedelta(days=1) + dates = [base - datetime.timedelta(days=int(i)) for i in np.arange(180)] np.random.seed(43) z = np.random.poisson(size=(len(programmers), len(dates))) @@ -51,7 +53,7 @@ def plot_4(): return fig -def plot_5(): +def plot_empty_trace(): fig = px.imshow([[1, 20, 30], [20, 1, 60], [30, 60, 1]]) @@ -65,10 +67,12 @@ def test_2(): assert_equality(plot_2(), os.path.join(this_dir, test_name, test_name + "_2_reference.tex"), img_name="/tmp/tikzplotly/fig2.png") def test_3(): - assert_equality(plot_3(), os.path.join(this_dir, test_name, test_name + "_3_reference.tex"), img_name="/tmp/tikzplotly/fig3.png") + with pytest.warns(UserWarning, match="No colorscale found, using default"): + assert_equality(plot_3(), os.path.join(this_dir, test_name, test_name + "_3_reference.tex"), img_name="/tmp/tikzplotly/fig3.png") def test_4(): assert_equality(plot_4(), os.path.join(this_dir, test_name, test_name + "_4_reference.tex"), img_name="/tmp/tikzplotly/fig4.png") -def test_5(): - assert_equality(plot_5(), os.path.join(this_dir, test_name, test_name + "_5_reference.tex")) \ No newline at end of file +def test_empty_trace(): + with pytest.warns(UserWarning, match="Adding empty trace."): + assert_equality(plot_empty_trace(), os.path.join(this_dir, test_name, test_name + "_5_reference.tex")) \ No newline at end of file diff --git a/tests/test_histograms.py b/tests/test_histograms.py index 9253c74..015bdd5 100644 --- a/tests/test_histograms.py +++ b/tests/test_histograms.py @@ -2,6 +2,7 @@ import plotly.graph_objects as go import numpy as np import os +from contextlib import nullcontext from .helpers import assert_equality import pathlib import pytest @@ -77,7 +78,12 @@ def test_3(): @pytest.mark.parametrize("histnorm", ["percent", "probability", "density", "probability density"]) def test_4(histnorm): - assert_equality(plot_4(histnorm), os.path.join(this_dir, test_name, test_name + "_4_reference.tex")) + if histnorm in ["percent", "probability", "density"]: + context = pytest.warns(UserWarning, match=r"Sorry, I did not find an equivalent for histnorm='\w+' in TikZ*") + else: + context = nullcontext() + with context: + assert_equality(plot_4(histnorm), os.path.join(this_dir, test_name, test_name + "_4_reference.tex")) def test_5(): assert_equality(plot_5(), os.path.join(this_dir, test_name, test_name + "_5_reference.tex")) @@ -86,7 +92,8 @@ def test_6(): assert_equality(plot_6(), os.path.join(this_dir, test_name, test_name + "_6_reference.tex")) def test_7(): - assert_equality(plot_7(), os.path.join(this_dir, test_name, test_name + "_7_reference.tex")) + with pytest.warns(UserWarning, match="To the best of our knowledge, other aggregate function than 'count' are not supported in pgfplots.*"): + assert_equality(plot_7(), os.path.join(this_dir, test_name, test_name + "_7_reference.tex")) def test_8(): assert_equality(plot_8(), os.path.join(this_dir, test_name, test_name + "_8_reference.tex")) diff --git a/tests/test_markers.py b/tests/test_markers.py index 688bd3c..9811d00 100644 --- a/tests/test_markers.py +++ b/tests/test_markers.py @@ -3,6 +3,7 @@ import pytest import numpy as np import os +from contextlib import nullcontext from .helpers import assert_equality import pathlib @@ -96,7 +97,12 @@ def plot_with_angle(): @pytest.mark.parametrize("symbol", ["circle", 0, "0", "circle-dot"]) def test_1(symbol): - assert_equality(plot_1(symbol), os.path.join(this_dir, test_name, test_name + "_1_reference.tex")) + if symbol == "circle-dot": + context = pytest.warns(UserWarning, match=r"Dotted markers are not supported \(yet\), the symbol without dot will be used instead.") + else: + context = nullcontext() + with context: + assert_equality(plot_1(symbol), os.path.join(this_dir, test_name, test_name + "_1_reference.tex")) def test_2(): assert_equality(plot_2(), os.path.join(this_dir, test_name, test_name + "_2_reference.tex")) diff --git a/tests/test_polar.py b/tests/test_polar.py index 847a32a..03f36e6 100644 --- a/tests/test_polar.py +++ b/tests/test_polar.py @@ -5,6 +5,7 @@ import plotly.express as px import plotly.graph_objects as go import pandas as pd +import pytest from .helpers import assert_equality this_dir = pathlib.Path(__file__).resolve().parent @@ -275,4 +276,5 @@ def test_polar_1(): assert_equality(fig_polar_1(), os.path.join(this_dir, test_name, test_name + "_1_reference.tex")) def test_polar_matplotlib(): - assert_equality(fig_polar_matplotlib(), os.path.join(this_dir, "empty_plot.tex")) + with pytest.warns(UserWarning, match="Trace type barpolar is not supported yet"): + assert_equality(fig_polar_matplotlib(), os.path.join(this_dir, "empty_plot.tex")) diff --git a/tests/test_scatter.py b/tests/test_scatter.py index 4322cb3..19f0314 100644 --- a/tests/test_scatter.py +++ b/tests/test_scatter.py @@ -2,6 +2,7 @@ import plotly.graph_objects as go import numpy as np import os +from contextlib import nullcontext from .helpers import assert_equality import pathlib import pytest @@ -220,9 +221,6 @@ def test_2(): def test_tranparent_color(): assert_equality(plot_transparent_color(), os.path.join(this_dir, test_name, test_name + "_transparent_color_reference.tex")) -# def test_tranparent_color_rgba(): - # assert_equality(plot_transparent_color_rgba(), os.path.join(this_dir, test_name, test_name + "_transparent_color_rgba_reference.tex")) - def test_3(): assert_equality(plot_3(), os.path.join(this_dir, test_name, test_name + "_3_reference.tex")) @@ -230,14 +228,18 @@ def test_4(): assert_equality(plot_4(), os.path.join(this_dir, test_name, test_name + "_4_reference.tex")) def test_5(): - assert_equality(plot_5(), os.path.join(this_dir, test_name, test_name + "_5_reference.tex")) + with pytest.warns(UserWarning, match="Assuming this is a date*"): + assert_equality(plot_5(), os.path.join(this_dir, test_name, test_name + "_5_reference.tex")) @pytest.mark.parametrize("x, y", [(True, True), (True, False), (False, True)]) def test_6(x, y): - assert_equality(plot_6(x, y), os.path.join(this_dir, test_name, test_name + f"_6_{x}_{y}_reference.tex")) + context = pytest.warns(UserWarning, match="Adding empty trace.") if x and y else nullcontext() + with context: + assert_equality(plot_6(x, y), os.path.join(this_dir, test_name, test_name + f"_6_{x}_{y}_reference.tex")) def test_7(): - assert_equality(plot_7(), os.path.join(this_dir, test_name, test_name + "_7_reference.tex")) + with pytest.warns(UserWarning, match="Assuming data January is a month*"): + assert_equality(plot_7(), os.path.join(this_dir, test_name, test_name + "_7_reference.tex")) def test_8(): assert_equality(plot_8(), os.path.join(this_dir, test_name, test_name + "_8_reference.tex")) diff --git a/tests/test_scatter3d.py b/tests/test_scatter3d.py index 55f4349..0b3b0d8 100644 --- a/tests/test_scatter3d.py +++ b/tests/test_scatter3d.py @@ -2,6 +2,7 @@ Test of 3D scatter plots https://plotly.com/python/3d-scatter-plots/ """ import os, pathlib +from contextlib import nullcontext import plotly.express as px import plotly.graph_objects as go import numpy as np @@ -88,7 +89,13 @@ def test_scatter_3d_1(): @pytest.mark.parametrize("mode", ["markers", "markers+lines", "lines"]) def test_scatter_3d_2(mode): - assert_equality(plot_scatter_3d_2(mode), os.path.join(this_dir, test_name, test_name + f"_2_{mode}_reference.tex")) + if "markers" in mode: + context = pytest.warns(UserWarning, match="Color from data is not supported yet*") + # context = nullcontext() + else: + context = nullcontext() + with context: + assert_equality(plot_scatter_3d_2(mode), os.path.join(this_dir, test_name, test_name + f"_2_{mode}_reference.tex")) def test_scatter_3d_3(): assert_equality(plot_scatter_3d_3(), os.path.join(this_dir, test_name, test_name + "_3_reference.tex")) @@ -97,4 +104,5 @@ def test_scatter_3d_view(): assert_equality(plot_scatter_3d_view(), os.path.join(this_dir, test_name, test_name + "_view_reference.tex")) def test_scatter_3d_empty(): - assert_equality(plot_scatter_3d_empty(), os.path.join(this_dir, test_name, test_name + "_empty_reference.tex")) + with pytest.warns(UserWarning, match="Adding empty 3D trace."): + assert_equality(plot_scatter_3d_empty(), os.path.join(this_dir, test_name, test_name + "_empty_reference.tex")) diff --git a/tests/test_specific.py b/tests/test_specific.py index f638320..391b8fb 100644 --- a/tests/test_specific.py +++ b/tests/test_specific.py @@ -2,7 +2,7 @@ In this file are present the test of some very specific usage case, that should occur very rarely. """ import os, pathlib -import plotly.express as px +import pytest import plotly.graph_objects as go from .helpers import assert_equality @@ -29,12 +29,7 @@ def plot_sanitized_text(): return fig -def plot_transparent_background(): - # TODO: move it into test_color (when created) - fig = px.scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16]) - fig.update_layout(plot_bgcolor='rgba(255, 182, 193, .5)') - return fig def plot_empty_figure(): fig = go.Figure() @@ -43,10 +38,9 @@ def plot_empty_figure(): def test_sanitized_text(): - assert_equality(plot_sanitized_text(), os.path.join(this_dir, test_name, test_name + "_sanitized_text_reference.tex")) - -def test_transparent_background(): - assert_equality(plot_transparent_background(), os.path.join(this_dir, test_name, test_name + "_transparent_background_reference.tex")) + with pytest.warns(UserWarning, match="Character .+ has been replaced by \"x[0-9a-f]+\" in output file"): + assert_equality(plot_sanitized_text(), os.path.join(this_dir, test_name, test_name + "_sanitized_text_reference.tex")) def test_empty_figure(): - assert_equality(plot_empty_figure(), os.path.join(this_dir, "empty_plot.tex")) + with pytest.warns(UserWarning, match="No data in figure."): + assert_equality(plot_empty_figure(), os.path.join(this_dir, "empty_plot.tex")) diff --git a/tox.ini b/tox.ini index 741a24d..73c5836 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,7 @@ envlist = isolated_build = True [testenv] -deps = - pandas - pytest - pytest-cov - pytest-codeblocks - plotly - pytest-randomly - kaleido<1.0.0 +deps = -rrequirements.txt commands = + plotly_get_chrome -y pytest {posargs} \ No newline at end of file From e9384b5c1d0d3ef53bfe474f2bee8f41dafd4d29 Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Sat, 11 Apr 2026 10:45:37 +0200 Subject: [PATCH 6/8] [fix] correctly handle empty histogram --- src/tikzplotly/_histogram.py | 4 ++-- tests/test_histograms.py | 2 +- tests/test_specific.py | 11 +++++++++++ .../test_specific_empty_histogram_reference.tex | 7 +++++++ 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/test_specific/test_specific_empty_histogram_reference.tex diff --git a/src/tikzplotly/_histogram.py b/src/tikzplotly/_histogram.py index 6cc4187..ef56b72 100644 --- a/src/tikzplotly/_histogram.py +++ b/src/tikzplotly/_histogram.py @@ -93,8 +93,8 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"): axis.add_option("y filter/.expression", "rawx") hist_options["handler/.style"] = "{xbar interval}" else: # trace.x and trace.y are both empty - warn("Normally, we should reach this line") - data_str = "" + warn("Empty histogram. Returning no plot") + return "" diff --git a/tests/test_histograms.py b/tests/test_histograms.py index 015bdd5..fb3bd98 100644 --- a/tests/test_histograms.py +++ b/tests/test_histograms.py @@ -99,4 +99,4 @@ def test_8(): assert_equality(plot_8(), os.path.join(this_dir, test_name, test_name + "_8_reference.tex")) def test_9(): - assert_equality(plot_9(), os.path.join(this_dir, test_name, test_name + "_9_reference.tex")) \ No newline at end of file + assert_equality(plot_9(), os.path.join(this_dir, test_name, test_name + "_9_reference.tex")) diff --git a/tests/test_specific.py b/tests/test_specific.py index 391b8fb..56c302b 100644 --- a/tests/test_specific.py +++ b/tests/test_specific.py @@ -4,6 +4,7 @@ import os, pathlib import pytest import plotly.graph_objects as go +import plotly.express as px from .helpers import assert_equality this_dir = pathlib.Path(__file__).resolve().parent @@ -36,6 +37,12 @@ def plot_empty_figure(): fig.show() return fig +def plot_empty_histogram(): + # Normally user shouldn't create this kind on figure, but we never know ! + fig = px.histogram(x=[1]) + fig.data[0].x = None + return fig + def test_sanitized_text(): with pytest.warns(UserWarning, match="Character .+ has been replaced by \"x[0-9a-f]+\" in output file"): @@ -44,3 +51,7 @@ def test_sanitized_text(): def test_empty_figure(): with pytest.warns(UserWarning, match="No data in figure."): assert_equality(plot_empty_figure(), os.path.join(this_dir, "empty_plot.tex")) + +def test_empty_histogram(): + with pytest.warns(UserWarning, match="Empty histogram.*"): + assert_equality(plot_empty_histogram(), os.path.join(this_dir, test_name, test_name + "_empty_histogram_reference.tex")) diff --git a/tests/test_specific/test_specific_empty_histogram_reference.tex b/tests/test_specific/test_specific_empty_histogram_reference.tex new file mode 100644 index 0000000..32f644f --- /dev/null +++ b/tests/test_specific/test_specific_empty_histogram_reference.tex @@ -0,0 +1,7 @@ +\begin{tikzpicture} +\begin{axis}[ +xlabel=x, +ylabel=count +] +\end{axis} +\end{tikzpicture} From 21a6f0bc0232963d54a63f03adca121a017945e1 Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Sat, 11 Apr 2026 12:42:20 +0200 Subject: [PATCH 7/8] [refactor] avoid code duplication between scatter 2d and 3d --- .../{_dataContainer.py => _data_container.py} | 2 +- src/tikzplotly/_histogram.py | 51 +++++++++---------- src/tikzplotly/_polar.py | 2 +- src/tikzplotly/_save.py | 4 +- src/tikzplotly/_scatter.py | 30 +++-------- src/tikzplotly/_scatter3d.py | 33 ++---------- src/tikzplotly/_trace_utils.py | 44 ++++++++++++++++ src/tikzplotly/_utils.py | 12 +---- .../test_markers/test_markers_1_reference.tex | 6 +-- .../test_markers/test_markers_2_reference.tex | 4 +- .../test_markers/test_markers_3_reference.tex | 6 +-- .../test_markers_angle_reference.tex | 6 +-- tox.ini | 2 +- 13 files changed, 97 insertions(+), 105 deletions(-) rename src/tikzplotly/{_dataContainer.py => _data_container.py} (99%) create mode 100644 src/tikzplotly/_trace_utils.py diff --git a/src/tikzplotly/_dataContainer.py b/src/tikzplotly/_data_container.py similarity index 99% rename from src/tikzplotly/_dataContainer.py rename to src/tikzplotly/_data_container.py index 9ccd493..4db4f2f 100644 --- a/src/tikzplotly/_dataContainer.py +++ b/src/tikzplotly/_data_container.py @@ -186,7 +186,7 @@ def add_data3d(self, x, y, z, name=None): return data.name, data.z_name data_obj = Data3D(x, y, z, name) self.data.append(data_obj) - return data_obj.name, data_obj.z_name + return data_obj.name def export_data(self): """Generate LaTeX code to export the data from DataContainer. diff --git a/src/tikzplotly/_histogram.py b/src/tikzplotly/_histogram.py index ef56b72..2543cd4 100644 --- a/src/tikzplotly/_histogram.py +++ b/src/tikzplotly/_histogram.py @@ -50,6 +50,27 @@ def formalize_data(data, axis:Axis, row_sep="\\\\"): return data_str +def treat_histnorm(histnorm: str, hist_options: dict): + """Handle the normalization option of the histogram + + Parameters + ---------- + histnorm : str + The normalization mode for the histogram. + hist_options : dict + Dictionary to store histogram options. + """ + if histnorm in ("percent", "probability", "density"): + warn( + f"Sorry, I did not find an equivalent for histnorm='{histnorm}' in TikZ. " + "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code " + "that would plot this :).\nFor now, the histogram will be plotted without normalization " + "(as if histnorm='probability density')." + ) + hist_options["density"] = None + elif histnorm in ("probability density", ): + hist_options["density"] = None + def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"): """ Draw a histogram and return the TikZ code. @@ -79,7 +100,8 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"): code = "" - plot_options = {"hist": None} + plot_options = {} + plot_options["hist"] = None type_options = {"row sep": row_sep, "y index": 0} hist_options = {} @@ -97,34 +119,10 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"): return "" - if trace.nbinsx is not None: hist_options["bins"] = trace.nbinsx - if trace.histnorm == "percent": - warn( - f"Sorry, I did not find an equivalent for histnorm='{trace.histnorm}' in TikZ. " - "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code " - "that would plot this :).\nFor now, the histogram will be plotted without normalization " - "(as if histnorm='probability density')." - ) - hist_options["density"] = None - elif trace.histnorm == "probability": - warn( - f"Sorry, I did not find an equivalent for histnorm='{trace.histnorm}' in TikZ. " - "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code that would plot this :).\n" - "For now, the histogram will be plotted without normalization (as if histnorm='probability density')." - ) - hist_options["density"] = None - elif trace.histnorm == "density": - warn( - f"Sorry, I did not find an equivalent for histnorm='{trace.histnorm}' in TikZ. " - "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code that would plot this :).\n" - "For now, the histogram will be plotted without normalization (as if histnorm='probability density')." - ) - hist_options["density"] = None - elif trace.histnorm == "probability density": - hist_options["density"] = None + treat_histnorm(trace.histnorm, hist_options) if trace.cumulative.enabled: hist_options["cumulative"] = None @@ -155,5 +153,4 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"): code += tex_addplot(data_str, plot_type = "table", options = option_dict_to_str(plot_options), type_options=option_dict_to_str(type_options)) - return code diff --git a/src/tikzplotly/_polar.py b/src/tikzplotly/_polar.py index a293a9a..5d9229f 100644 --- a/src/tikzplotly/_polar.py +++ b/src/tikzplotly/_polar.py @@ -7,7 +7,7 @@ from ._utils import option_dict_to_str from ._tex import tex_addplot from ._color import convert_color -from ._dataContainer import DataContainer +from ._data_container import DataContainer def get_polar_coord(trace, axis: Axis, data_container: DataContainer): """Get polar coordinates from the trace diff --git a/src/tikzplotly/_save.py b/src/tikzplotly/_save.py index 5d880aa..ea157ff 100755 --- a/src/tikzplotly/_save.py +++ b/src/tikzplotly/_save.py @@ -19,7 +19,7 @@ from ._axis import Axis from ._color import convert_color from ._annotations import str_from_annotation -from ._dataContainer import DataContainer +from ._data_container import DataContainer from ._utils import sanitize_tex_text, sanitize_text @@ -173,7 +173,7 @@ def get_tikz_code( if hasattr(figure_layout.scene, "title") and getattr(figure_layout.scene.title, "text", None): axis.add_option("title", f"{{{sanitize_tex_text(figure_layout.scene.title.text)}}}") - data_name_macro, z_name = data_container.add_data3d(trace.x, trace.y, trace.z, trace.name) + data_name_macro = data_container.add_data3d(trace.x, trace.y, trace.z, trace.name) data_str.append(draw_scatter3d(data_name_macro, trace, colors_set)) if trace.name and trace['showlegend'] is not False: diff --git a/src/tikzplotly/_scatter.py b/src/tikzplotly/_scatter.py index d766ea1..2c963eb 100644 --- a/src/tikzplotly/_scatter.py +++ b/src/tikzplotly/_scatter.py @@ -2,16 +2,16 @@ Provides functionality to convert Plotly scatter traces into TikZ/PGFPlots code for LaTeX documents. """ -from warnings import warn import numpy as np from ._tex import tex_addplot, tex_add_text from ._color import convert_color -from ._marker import marker_symbol_to_tex from ._dash import DASH_PATTERN from ._axis import Axis from ._data import data_type +from ._trace_utils import configure_marker_options, finalize_marker_options from ._utils import px_to_pt, option_dict_to_str + def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): """Get code for a scatter trace. @@ -54,16 +54,7 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): mark_option_dict = {} if "markers" in mode: - if marker.symbol is not None: - symbol, symbol_options = marker_symbol_to_tex(marker.symbol) - options_dict["mark"] = symbol - if "lines" not in mode: - options_dict["only marks"] = None - if symbol_options is not None: - mark_option_dict[symbol_options[0]] = symbol_options[1] - else: - if "lines" not in mode: - options_dict["only marks"] = None + configure_marker_options(mode, marker, options_dict, mark_option_dict, color_set) if scatter.marker.size is not None: options_dict["mark size"] = px_to_pt(marker.size) @@ -73,14 +64,14 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): mark_option_dict["solid"] = None mark_option_dict["fill"] = convert_color(scatter.marker.color)[0] - if (line:=scatter.marker.line) is not None: + if (line := scatter.marker.line) is not None: if line.color is not None: color_set.add(convert_color(line.color)[:3]) mark_option_dict["draw"] = convert_color(line.color)[0] if line.width is not None: mark_option_dict["line width"] = px_to_pt(line.width) - if (angle:=scatter.marker.angle) is not None: + if (angle := scatter.marker.angle) is not None: mark_option_dict["rotate"] = angle if (opacity := scatter.opacity) is not None: @@ -88,15 +79,7 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): if (opacity := scatter.marker.opacity) is not None: mark_option_dict["opacity"] = np.round(opacity, 2) - if mark_option_dict: - mark_options = option_dict_to_str(mark_option_dict) - options_dict["mark options"] = f"{{{mark_options}}}" - - elif mode == "lines": - options_dict["mark"] = "none" - - else: - warn(f"Scatter : Mode {mode} is not supported yet.") + finalize_marker_options(mode, options_dict, mark_option_dict, "Scatter") if scatter.line.width is not None: options_dict["line width"] = px_to_pt(scatter.line.width) @@ -105,7 +88,6 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): if scatter.connectgaps in [False, None] and None in scatter.x: options_dict["unbounded coords"] = "jump" - if scatter.line.color is not None: options_dict["color"] = convert_color(scatter.line.color)[0] if "mark" in mode: diff --git a/src/tikzplotly/_scatter3d.py b/src/tikzplotly/_scatter3d.py index d338e64..a1ff2d4 100644 --- a/src/tikzplotly/_scatter3d.py +++ b/src/tikzplotly/_scatter3d.py @@ -1,12 +1,13 @@ """ Provides functionality to convert Plotly 3D scatter traces into TikZ/PGFPlots code for LaTeX documents. """ -from warnings import warn + import numpy as np from ._color import convert_color -from ._marker import marker_symbol_to_tex +from ._trace_utils import configure_marker_options, finalize_marker_options from ._utils import px_to_pt, option_dict_to_str + def draw_scatter3d(data_name, scatter, color_set): """ Get code for a scatter3d trace. @@ -29,16 +30,7 @@ def draw_scatter3d(data_name, scatter, color_set): mark_option_dict = {} if "markers" in mode: - if marker.symbol is not None: - symbol, symbol_options = marker_symbol_to_tex(marker.symbol) - options_dict["mark"] = symbol - if "lines" not in mode: - options_dict["only marks"] = None - if symbol_options is not None: - mark_option_dict[symbol_options[0]] = symbol_options[1] - else: - if "lines" not in mode: - options_dict["only marks"] = None + configure_marker_options(mode, marker, options_dict, mark_option_dict, color_set) if marker.size is not None: size = marker.size @@ -54,27 +46,12 @@ def draw_scatter3d(data_name, scatter, color_set): mark_option_dict["solid"] = None mark_option_dict["fill"] = convert_color(c)[0] - if (line := marker.line) is not None: - if line.color is not None: - color_set.add(convert_color(line.color)[:3]) - mark_option_dict["draw"] = convert_color(line.color)[0] - if line.width is not None: - mark_option_dict["line width"] = px_to_pt(line.width) - if (opacity := scatter.opacity) is not None: options_dict["opacity"] = opacity if (opacity := marker.opacity) is not None: mark_option_dict["opacity"] = opacity - if mark_option_dict: - mark_options = option_dict_to_str(mark_option_dict) - options_dict["mark options"] = f"{{{mark_options}}}" - - elif mode == "lines": - options_dict["mark"] = "none" - - else: - warn(f"Scatter3d : Mode {mode} is not supported yet.") + finalize_marker_options(mode, options_dict, mark_option_dict, "Scatter3d") if scatter.line is not None: if scatter.line.width is not None: diff --git a/src/tikzplotly/_trace_utils.py b/src/tikzplotly/_trace_utils.py new file mode 100644 index 0000000..4b32d17 --- /dev/null +++ b/src/tikzplotly/_trace_utils.py @@ -0,0 +1,44 @@ +"""Shared helpers for Plotly trace to TikZ conversion.""" + +from warnings import warn + +from ._color import convert_color +from ._marker import marker_symbol_to_tex +from ._utils import px_to_pt, option_dict_to_str + + +def configure_marker_options(mode, marker, options_dict, mark_option_dict, color_set): + """Populate common marker options for scatter-like traces.""" + + if marker.symbol is not None: + symbol, symbol_options = marker_symbol_to_tex(marker.symbol) + options_dict["mark"] = symbol + if "lines" not in mode: + options_dict["only marks"] = None + if symbol_options is not None: + mark_option_dict[symbol_options[0]] = symbol_options[1] + elif "lines" not in mode: + options_dict["only marks"] = None + + if marker.line is not None: + if marker.line.color is not None: + color = convert_color(marker.line.color) + color_set.add(color[:3]) + mark_option_dict["draw"] = color[0] + if marker.line.width is not None: + mark_option_dict["line width"] = px_to_pt(marker.line.width) + + +def finalize_marker_options(mode, options_dict, mark_option_dict, trace_name): + """Finalize common marker options for scatter-like traces.""" + + if "markers" in mode: + if mark_option_dict: + mark_options = option_dict_to_str(mark_option_dict) + options_dict["mark options"] = f"{{{mark_options}}}" + return + + if mode == "lines": + options_dict["mark"] = "none" + else: + warn(f"{trace_name} : Mode {mode} is not supported yet.") diff --git a/src/tikzplotly/_utils.py b/src/tikzplotly/_utils.py index 29bdca4..d4ab6b5 100644 --- a/src/tikzplotly/_utils.py +++ b/src/tikzplotly/_utils.py @@ -93,18 +93,10 @@ def sanitize_char(ch: str, keep_space: int = 0) -> str: if ch == "@": return "at" if ch == " ": - if keep_space == 1: - return " " - if keep_space == 0: - return "_" - return "" + return " " if keep_space == 1 else "_" if keep_space == 0 else "" if ch in "[]{}= ": return f"x{ord(ch):x}" - # if not ascii, return hex - if ord(ch) > 127: - return f"x{ord(ch):x}" - # if not printable, return hex - if not ch.isprintable(): + if ord(ch) > 127 or not ch.isprintable(): # not ascii or not printable: return hex return f"x{ord(ch):x}" return ch diff --git a/tests/test_markers/test_markers_1_reference.tex b/tests/test_markers/test_markers_1_reference.tex index 0569891..4080010 100644 --- a/tests/test_markers/test_markers_1_reference.tex +++ b/tests/test_markers/test_markers_1_reference.tex @@ -169,11 +169,11 @@ xlabel=sepal\_width, ylabel=sepal\_length ] -\addplot+ [mark=*, only marks, mark size=9, mark options={solid, fill=636efa, draw=darkslategrey, line width=1.5}] table[y=setosa] {\dataA}; +\addplot+ [mark=*, only marks, mark size=9, mark options={draw=darkslategrey, line width=1.5, solid, fill=636efa}] table[y=setosa] {\dataA}; \addlegendentry{setosa} -\addplot+ [mark=*, only marks, mark size=9, mark options={solid, fill=EF553B, draw=darkslategrey, line width=1.5}] table[y=versicolor] {\dataB}; +\addplot+ [mark=*, only marks, mark size=9, mark options={draw=darkslategrey, line width=1.5, solid, fill=EF553B}] table[y=versicolor] {\dataB}; \addlegendentry{versicolor} -\addplot+ [mark=*, only marks, mark size=9, mark options={solid, fill=00cc96, draw=darkslategrey, line width=1.5}] table[y=virginica] {\dataC}; +\addplot+ [mark=*, only marks, mark size=9, mark options={draw=darkslategrey, line width=1.5, solid, fill=00cc96}] table[y=virginica] {\dataC}; \addlegendentry{virginica} \end{axis} \end{tikzpicture} diff --git a/tests/test_markers/test_markers_2_reference.tex b/tests/test_markers/test_markers_2_reference.tex index 60e4e4e..d7baf20 100644 --- a/tests/test_markers/test_markers_2_reference.tex +++ b/tests/test_markers/test_markers_2_reference.tex @@ -513,7 +513,7 @@ \definecolor{mediumpurple}{RGB}{147, 112, 219} \begin{axis} -\addplot+ [only marks, mark size=15, mark options={solid, fill=lightskyblue, draw=mediumpurple, line width=1.5, opacity=0.5}, forget plot] table[y=y0] {\dataA}; -\addplot+ [only marks, mark size=60, mark options={solid, fill=lightskyblue, draw=mediumpurple, line width=6, opacity=0.5}, forget plot] table[y=y0] {\dataB}; +\addplot+ [only marks, mark size=15, mark options={draw=mediumpurple, line width=1.5, solid, fill=lightskyblue, opacity=0.5}, forget plot] table[y=y0] {\dataA}; +\addplot+ [only marks, mark size=60, mark options={draw=mediumpurple, line width=6, solid, fill=lightskyblue, opacity=0.5}, forget plot] table[y=y0] {\dataB}; \end{axis} \end{tikzpicture} diff --git a/tests/test_markers/test_markers_3_reference.tex b/tests/test_markers/test_markers_3_reference.tex index 9e869e9..b7c97a1 100644 --- a/tests/test_markers/test_markers_3_reference.tex +++ b/tests/test_markers/test_markers_3_reference.tex @@ -169,11 +169,11 @@ xlabel=sepal\_width, ylabel=sepal\_length ] -\addplot+ [mark=diamond*, only marks, mark size=6, mark options={solid, fill=636efa, draw=darkslategrey, line width=1.5}] table[y=setosa] {\dataA}; +\addplot+ [mark=diamond*, only marks, mark size=6, mark options={draw=darkslategrey, line width=1.5, solid, fill=636efa}] table[y=setosa] {\dataA}; \addlegendentry{setosa} -\addplot+ [mark=diamond*, only marks, mark size=6, mark options={solid, fill=EF553B, draw=darkslategrey, line width=1.5}] table[y=versicolor] {\dataB}; +\addplot+ [mark=diamond*, only marks, mark size=6, mark options={draw=darkslategrey, line width=1.5, solid, fill=EF553B}] table[y=versicolor] {\dataB}; \addlegendentry{versicolor} -\addplot+ [mark=diamond*, only marks, mark size=6, mark options={solid, fill=00cc96, draw=darkslategrey, line width=1.5}] table[y=virginica] {\dataC}; +\addplot+ [mark=diamond*, only marks, mark size=6, mark options={draw=darkslategrey, line width=1.5, solid, fill=00cc96}] table[y=virginica] {\dataC}; \addlegendentry{virginica} \end{axis} \end{tikzpicture} diff --git a/tests/test_markers/test_markers_angle_reference.tex b/tests/test_markers/test_markers_angle_reference.tex index b06aee8..3d1900f 100644 --- a/tests/test_markers/test_markers_angle_reference.tex +++ b/tests/test_markers/test_markers_angle_reference.tex @@ -169,11 +169,11 @@ xlabel=sepal\_width, ylabel=sepal\_length ] -\addplot+ [mark=triangle*, only marks, mark size=9, mark options={xscale=0.5, solid, fill=636efa, draw=darkslategrey, line width=1.5, rotate=45}] table[y=setosa] {\dataA}; +\addplot+ [mark=triangle*, only marks, mark size=9, mark options={xscale=0.5, draw=darkslategrey, line width=1.5, solid, fill=636efa, rotate=45}] table[y=setosa] {\dataA}; \addlegendentry{setosa} -\addplot+ [mark=triangle*, only marks, mark size=9, mark options={xscale=0.5, solid, fill=EF553B, draw=darkslategrey, line width=1.5, rotate=45}] table[y=versicolor] {\dataB}; +\addplot+ [mark=triangle*, only marks, mark size=9, mark options={xscale=0.5, draw=darkslategrey, line width=1.5, solid, fill=EF553B, rotate=45}] table[y=versicolor] {\dataB}; \addlegendentry{versicolor} -\addplot+ [mark=triangle*, only marks, mark size=9, mark options={xscale=0.5, solid, fill=00cc96, draw=darkslategrey, line width=1.5, rotate=45}] table[y=virginica] {\dataC}; +\addplot+ [mark=triangle*, only marks, mark size=9, mark options={xscale=0.5, draw=darkslategrey, line width=1.5, solid, fill=00cc96, rotate=45}] table[y=virginica] {\dataC}; \addlegendentry{virginica} \end{axis} \end{tikzpicture} diff --git a/tox.ini b/tox.ini index 73c5836..5845277 100644 --- a/tox.ini +++ b/tox.ini @@ -7,4 +7,4 @@ isolated_build = True deps = -rrequirements.txt commands = plotly_get_chrome -y - pytest {posargs} \ No newline at end of file + pytest {posargs} From 22448604aeb62cde0fe7b473fed3635333a5c802 Mon Sep 17 00:00:00 2001 From: Thomas Saigre Date: Sun, 12 Apr 2026 09:38:16 +0200 Subject: [PATCH 8/8] [fix] implement some fixes for #37 --- docs/develop/contributing.md | 2 +- pyproject.toml | 3 ++- src/tikzplotly/_scatter.py | 7 ------ src/tikzplotly/_trace_utils.py | 40 ++++++++++++++++++++++++++++------ tests/test_specific.py | 1 - tox.ini | 7 +++++- 6 files changed, 42 insertions(+), 18 deletions(-) diff --git a/docs/develop/contributing.md b/docs/develop/contributing.md index fe96318..0a4998b 100644 --- a/docs/develop/contributing.md +++ b/docs/develop/contributing.md @@ -42,7 +42,7 @@ The code coverage is available in the directory `htmlcov`. !!! Tip "Note about coverage" The coverage CI is quite strict, so you need to cover all the modification to pass it. - I found that tedious at first, but actually making it pass make me realize that there were some bogs in the code! + I found that tedious at first, but actually making it pass make me realize that there were some bugs in the code! ??? Info "Pytest" Some warnings are ignored by `pytest`, as they are intended for the user in some specific cases, that are not relevant in general test cases. diff --git a/pyproject.toml b/pyproject.toml index 463c831..186a123 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ keywords = ["plotly", "latex", "tikz", "figure", "conversion", "graphics"] dependencies = [ "numpy", "plotly", - "pillow" + "pillow", + "kaleido" ] [project.optional-dependencies] diff --git a/src/tikzplotly/_scatter.py b/src/tikzplotly/_scatter.py index 2c963eb..eb06bf2 100644 --- a/src/tikzplotly/_scatter.py +++ b/src/tikzplotly/_scatter.py @@ -64,13 +64,6 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set): mark_option_dict["solid"] = None mark_option_dict["fill"] = convert_color(scatter.marker.color)[0] - if (line := scatter.marker.line) is not None: - if line.color is not None: - color_set.add(convert_color(line.color)[:3]) - mark_option_dict["draw"] = convert_color(line.color)[0] - if line.width is not None: - mark_option_dict["line width"] = px_to_pt(line.width) - if (angle := scatter.marker.angle) is not None: mark_option_dict["rotate"] = angle diff --git a/src/tikzplotly/_trace_utils.py b/src/tikzplotly/_trace_utils.py index 4b32d17..b2f9b3f 100644 --- a/src/tikzplotly/_trace_utils.py +++ b/src/tikzplotly/_trace_utils.py @@ -8,7 +8,21 @@ def configure_marker_options(mode, marker, options_dict, mark_option_dict, color_set): - """Populate common marker options for scatter-like traces.""" + """Populate marker-related TikZ options for scatter-like traces. + + Parameters + ---------- + mode + Plotly trace mode string (for example ``markers`` or ``markers+lines``). + marker + Plotly marker object from the trace. + options_dict + Dictionary collecting top-level PGFPlots options. + mark_option_dict + Dictionary collecting nested ``mark options`` for PGFPlots. + color_set + Set used to track colors that must be defined in the LaTeX output. + """ if marker.symbol is not None: symbol, symbol_options = marker_symbol_to_tex(marker.symbol) @@ -20,17 +34,29 @@ def configure_marker_options(mode, marker, options_dict, mark_option_dict, color elif "lines" not in mode: options_dict["only marks"] = None - if marker.line is not None: - if marker.line.color is not None: - color = convert_color(marker.line.color) + if (line := marker.line) is not None: + if line.color is not None: + color = convert_color(line.color) color_set.add(color[:3]) mark_option_dict["draw"] = color[0] - if marker.line.width is not None: - mark_option_dict["line width"] = px_to_pt(marker.line.width) + if line.width is not None: + mark_option_dict["line width"] = px_to_pt(line.width) def finalize_marker_options(mode, options_dict, mark_option_dict, trace_name): - """Finalize common marker options for scatter-like traces.""" + """Finalize marker options and handle unsupported modes. + + Parameters + ---------- + mode + Plotly trace mode string. + options_dict + Dictionary collecting top-level PGFPlots options. + mark_option_dict + Dictionary collecting nested ``mark options`` for PGFPlots. + trace_name + Name used in warnings to identify the trace kind. + """ if "markers" in mode: if mark_option_dict: diff --git a/tests/test_specific.py b/tests/test_specific.py index 56c302b..337dccb 100644 --- a/tests/test_specific.py +++ b/tests/test_specific.py @@ -34,7 +34,6 @@ def plot_sanitized_text(): def plot_empty_figure(): fig = go.Figure() - fig.show() return fig def plot_empty_histogram(): diff --git a/tox.ini b/tox.ini index 5845277..3fba2c2 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,12 @@ envlist = isolated_build = True [testenv] -deps = -rrequirements.txt +deps = + pandas + pytest + pytest-cov + pytest-codeblocks + pytest-randomly commands = plotly_get_chrome -y pytest {posargs}