Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ src/tests/outputs/**
# Doxygen output
doxygen/*


# Virtual environment
.venv/
*.egg-info/
Expand All @@ -29,4 +28,7 @@ dist/
# Tox
.tox/
.coverage
coverage.xml
coverage.xml

# Mkdocs site
site/**
16 changes: 11 additions & 5 deletions docs/develop/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 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.
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/).
2 changes: 1 addition & 1 deletion docs/develop/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ keywords = ["plotly", "latex", "tikz", "figure", "conversion", "graphics"]
dependencies = [
"numpy",
"plotly",
"pillow"
"pillow",
"kaleido"
]

[project.optional-dependencies]
Expand All @@ -36,3 +37,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",
]
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ tox
pandas
mkdocs-material[recommended]
mkdocs-git-revision-date-localized-plugin
pytest
pytest-cov
pytest-codeblocks
19 changes: 10 additions & 9 deletions src/tikzplotly/_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Comment thread
thomas-saigre marked this conversation as resolved.

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("]", "}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/tikzplotly/_heatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
54 changes: 27 additions & 27 deletions src/tikzplotly/_histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = {}

Expand All @@ -92,36 +114,15 @@ 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("Empty histogram. Returning no plot")
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
Expand Down Expand Up @@ -152,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
2 changes: 1 addition & 1 deletion src/tikzplotly/_polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/tikzplotly/_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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:
Expand Down
42 changes: 6 additions & 36 deletions src/tikzplotly/_scatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -53,15 +53,8 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set):
options_dict = {}
mark_option_dict = {}

if mode == "markers":
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 symbol_options is not None:
mark_option_dict[symbol_options[0]] = symbol_options[1]
else:
options_dict["only marks"] = None
if "markers" in mode:
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)
Expand All @@ -71,37 +64,15 @@ 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:
if (angle := scatter.marker.angle) is not None:
mark_option_dict["rotate"] = angle

if (opacity := scatter.opacity) is not None:
options_dict["opacity"] = np.round(opacity, 2)
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"

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.")
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)
Expand All @@ -110,7 +81,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:
Expand Down
Loading
Loading