tikzplotly 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tikzplotly/__about__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Module metadata for tikzplotly.
3
3
  """
4
- __version__ = "0.2.1"
4
+ __version__ = "0.2.3"
5
5
  __author__ = "Thomas Saigre"
6
6
  __license__ = "MIT"
7
7
  __description__ = "Convert plotly figures to TikZ code for inclusion into PGFPlots figures."
tikzplotly/__init__.py CHANGED
@@ -7,8 +7,16 @@ functions for generating and saving TikZ code from plotly figures.
7
7
  Exports:
8
8
  get_tikz_code (Callable): Function to generate TikZ code from a plotly figure.
9
9
  save (Callable): Function to save TikZ code to a file.
10
+ show (Callable): Function to show a figure in the browser, with a button exporting it to TikZ.
11
+
12
+ Importing the package also registers the ``"tikzplotly"`` Plotly renderer, so that
13
+ ``fig.show(renderer="tikzplotly")`` adds the same button.
10
14
  """
11
15
  from .__about__ import __version__, __author__, __license__, __description__
12
16
  from ._save import get_tikz_code, save
17
+ from ._browser import show, TikzBrowserRenderer, register_renderer as _register_renderer
18
+
19
+ _register_renderer()
13
20
 
14
- __all__ = ["__version__", "__author__", "__license__", "__description__", "get_tikz_code", "save"]
21
+ __all__ = ["__version__", "__author__", "__license__", "__description__", "get_tikz_code", "save",
22
+ "show", "TikzBrowserRenderer"]
tikzplotly/_browser.py ADDED
@@ -0,0 +1,346 @@
1
+ """Show a figure in the web browser with an "Export to TikZ" button in the mode bar.
2
+
3
+ Plotly's own browser renderer serves the page once and returns, so the page has no way to
4
+ reach Python afterwards. Here a small local HTTP server keeps running while the page is open:
5
+ clicking the button sends the current view (axis ranges, hidden traces) to that server, which
6
+ calls :func:`get_tikz_code` and writes the result to a file.
7
+
8
+ Plotly itself is not modified: a renderer named ``"tikzplotly"`` is registered with Plotly's
9
+ renderer API when tikzplotly is imported, and :func:`show` is a shortcut to it.
10
+ """
11
+ import base64
12
+ import json
13
+ import secrets
14
+ import socketserver
15
+ import threading
16
+ import time
17
+ import webbrowser
18
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
19
+ from pathlib import Path
20
+ from typing import cast
21
+
22
+ import numpy as np
23
+ import plotly.graph_objects as go
24
+ import plotly.io as pio
25
+
26
+ from ._save import get_tikz_code
27
+
28
+ try: # private module: guarded so that a Plotly reorganisation cannot break ``import tikzplotly``
29
+ from plotly.io._base_renderers import ExternalRenderer
30
+ except ImportError: # pragma: no cover
31
+ ExternalRenderer = object
32
+
33
+ RENDERER_NAME = "tikzplotly"
34
+
35
+ # A hidden browser tab may only run its timers once a minute, so the idle timeout has to be
36
+ # well above 60 s, or a tab left in the background would be considered closed.
37
+ PING_MS = 10_000
38
+ IDLE_TIMEOUT = 150
39
+ # Time left after the page signals it is closing, so that a page reload can reconnect.
40
+ CLOSE_GRACE = 5
41
+
42
+ # "Export to TikZ" icon: a floppy disk with "TikZ" (glyph outlines of Latin Modern Roman Bold),
43
+ # in a 1000x1000 box. A single path, because Plotly only colours the first path of an icon,
44
+ # and its holes need the evenodd fill rule, set in _POST_SCRIPT.
45
+ TIKZ_ICON_PATH = (
46
+ "M110 40H760L960 240V890Q960 960 890 960H110Q40 960 40 890V110Q40 40 110 40Z M120 115V885H885V275L725"
47
+ " 115Z M230 115H690V290Q690 320 660 320H260Q230 320 230 290Z M540 155V280H630V155Z M381 604 374 523H1"
48
+ "47L140 604H156C159 566 162 539 211 539H237V735H185V750C203 749 241 749 260 749C280 749 318 749 336 7"
49
+ "50V735H284V539H310C358 539 361 566 365 604ZM491 750V735H471V599L412 602V617C432 617 434 617 434 631V"
50
+ "735H411V750L452 749ZM475 546C475 532 463 520 449 520C435 520 423 532 423 546C423 561 435 572 449 572"
51
+ "C463 572 475 561 475 546ZM673 701C673 696 668 696 663 696C655 696 655 696 653 705C648 725 641 741 62"
52
+ "9 741C626 741 621 740 621 729C621 729 621 723 624 712C625 709 625 705 625 702C625 676 596 669 572 66"
53
+ "6C584 657 596 645 596 644C619 619 634 610 647 610C648 610 652 610 653 614C639 616 630 628 630 639C63"
54
+ "0 647 636 654 647 654C657 654 671 647 671 625C671 615 667 598 648 598C629 598 612 611 591 634C577 64"
55
+ "8 571 653 563 658L596 528C596 527 597 525 597 523C597 516 591 517 586 517L545 519C541 519 534 519 53"
56
+ "4 529C534 535 541 535 544 535C548 535 554 536 558 536L509 731C508 736 508 737 508 739C508 747 514 75"
57
+ "3 523 753C530 753 540 749 543 739L558 678C568 680 589 685 589 703C589 705 589 707 588 710C588 713 58"
58
+ "7 715 587 719C587 739 605 753 627 753C661 753 673 703 673 701ZM865 655H849C846 692 842 733 773 733H7"
59
+ "26L860 536C863 532 863 532 863 528C863 519 859 519 852 519H679L675 601H691C693 556 716 535 763 535H8"
60
+ "07L672 733C669 737 669 738 669 742C669 750 673 750 680 750H859Z"
61
+ )
62
+
63
+ _POST_SCRIPT = """
64
+ (function () {
65
+ var gd = document.getElementById('{plot_id}');
66
+ var token = '__TOKEN__';
67
+
68
+ function post(path, body) {
69
+ return fetch('/' + token + path, {method: 'POST', body: JSON.stringify(body || {})});
70
+ }
71
+
72
+ function toast(message, failed) {
73
+ var el = document.createElement('div');
74
+ el.textContent = message;
75
+ el.style.cssText = 'position:fixed;left:16px;bottom:16px;padding:8px 12px;z-index:9999;'
76
+ + 'border-radius:4px;color:#fff;font:13px sans-serif;background:'
77
+ + (failed ? '#b3261e' : '#2e7d32');
78
+ document.body.appendChild(el);
79
+ setTimeout(function () { el.remove(); }, failed ? 8000 : 3000);
80
+ }
81
+
82
+ // Only the view state is sent: the data stays on the Python side.
83
+ function viewState() {
84
+ var axes = {};
85
+ Object.keys(gd.layout).forEach(function (key) {
86
+ if (/^[xy]axis\\d*$/.test(key)) {
87
+ var axis = gd.layout[key];
88
+ axes[key] = axis.autorange ? null : axis.range;
89
+ }
90
+ });
91
+ return {axes: axes, visible: gd.data.map(function (t) { return t.visible; })};
92
+ }
93
+
94
+ var button = {
95
+ name: 'exportTikz',
96
+ title: 'Export to TikZ',
97
+ icon: {width: 1000, height: 1000, path: '__ICON_PATH__'},
98
+ click: function () {
99
+ post('/export', viewState())
100
+ .then(function (r) { return r.json(); })
101
+ .then(function (r) {
102
+ if (r.ok) { toast('Saved ' + r.path); } else { toast(r.error, true); }
103
+ })
104
+ .catch(function () { toast('The Python process is no longer running', true); });
105
+ }
106
+ };
107
+
108
+ var style = document.createElement('style');
109
+ style.textContent = '.modebar-btn[data-title="Export to TikZ"] path { fill-rule: evenodd; }';
110
+ document.head.appendChild(style);
111
+
112
+ var config = __CONFIG__;
113
+ config.modeBarButtonsToAdd = (config.modeBarButtonsToAdd || []).concat([button]);
114
+ Plotly.react(gd, gd.data, gd.layout, config);
115
+
116
+ // Immediate ping: after a reload, cancels the "closing" signal sent by the previous page.
117
+ post('/ping');
118
+ setInterval(function () { post('/ping'); }, __PING_MS__);
119
+ window.addEventListener('pagehide', function () {
120
+ navigator.sendBeacon('/' + token + '/closing');
121
+ });
122
+ })();
123
+ """
124
+
125
+
126
+ def _decode_arrays(obj):
127
+ """Turn Plotly's ``{"dtype": ..., "bdata": ...}`` entries back into NumPy arrays.
128
+
129
+ Since Plotly 6, ``Figure.to_dict()``, hence the dict a renderer receives, stores NumPy
130
+ arrays in this base64 form, which the rest of tikzplotly cannot index.
131
+ """
132
+ if isinstance(obj, dict):
133
+ if "bdata" in obj and "dtype" in obj:
134
+ array = np.frombuffer(base64.b64decode(obj["bdata"]), dtype=obj["dtype"])
135
+ if "shape" in obj:
136
+ shape = obj["shape"]
137
+ if isinstance(shape, str):
138
+ shape = [int(n) for n in shape.split(",")]
139
+ array = array.reshape(shape)
140
+ return array
141
+ return {key: _decode_arrays(value) for key, value in obj.items()}
142
+ if isinstance(obj, (list, tuple)):
143
+ return [_decode_arrays(value) for value in obj]
144
+ return obj
145
+
146
+
147
+ class _LocalServer(ThreadingHTTPServer):
148
+ """HTTP server that does not look up its host name.
149
+
150
+ ``HTTPServer.server_bind`` calls ``socket.getfqdn``, a reverse DNS lookup that can take
151
+ several seconds, notably on macOS. The name is not used here.
152
+ """
153
+
154
+ def server_bind(self):
155
+ socketserver.TCPServer.server_bind(self)
156
+ self.server_name, self.server_port = self.server_address[:2] # type: ignore
157
+
158
+
159
+ def _free_path(path: Path) -> Path:
160
+ """Return ``path``, or ``stem_1.ext``, ``stem_2.ext``, ... if it already exists."""
161
+ candidate, n = path, 0
162
+ while candidate.exists():
163
+ n += 1
164
+ candidate = path.with_name(f"{path.stem}_{n}{path.suffix}")
165
+ return candidate
166
+
167
+
168
+ class TikzBrowserRenderer(ExternalRenderer): # type: ignore
169
+ """Plotly renderer opening the figure in the browser with an "Export to TikZ" button.
170
+
171
+ Parameters
172
+ ----------
173
+ output, optional
174
+ file written by the button, by default ``"figure.tex"``. If it already exists,
175
+ ``figure_1.tex``, ``figure_2.tex``, ... are used instead, so no export is overwritten.
176
+ config, optional
177
+ Plotly config dict for the page (see ``fig.show(config=...)``), by default None
178
+ **tikz_options
179
+ passed on to :func:`get_tikz_code` (``tikz_options``, ``axis_options``, ``decimate``...).
180
+ Unless ``img_name`` is given, the image of a heatmap is saved next to the ``.tex`` file,
181
+ with the same name.
182
+ """
183
+
184
+ def __init__(self, output="figure.tex", config=None, **tikz_options):
185
+ self.output = Path(output)
186
+ self.config = dict(config or {})
187
+ self.tikz_options = tikz_options
188
+ self._lock = threading.Lock()
189
+
190
+ def export(self, fig, view=None) -> Path:
191
+ """Write the TikZ code of ``fig``, as currently displayed in the page, to a free file name.
192
+
193
+ Parameters
194
+ ----------
195
+ fig
196
+ Plotly figure, or figure dict as received by a renderer
197
+ view, optional
198
+ ``{"axes": {"xaxis": [min, max] or None, ...}, "visible": [...]}`` as sent by the page
199
+
200
+ Returns
201
+ -------
202
+ path of the written file
203
+ """
204
+ view = view or {}
205
+ fig = go.Figure(_decode_arrays(fig) if isinstance(fig, dict) else fig) # copy: fig is reused
206
+ for name, axis_range in (view.get("axes") or {}).items():
207
+ fig.layout[name].range = axis_range
208
+ states = view.get("visible") or []
209
+ if len(states) == len(fig.data): # get_tikz_code exports hidden traces too
210
+ fig.data = [trace for trace, state in zip(fig.data, states) if state in (None, True)]
211
+
212
+ with self._lock: # two quick clicks must not pick the same file name
213
+ path = _free_path(self.output)
214
+ path.parent.mkdir(parents=True, exist_ok=True)
215
+ options = {"img_name": str(path.with_suffix(".png")), **self.tikz_options}
216
+ path.write_text(get_tikz_code(fig, **options), encoding="utf-8")
217
+ return path
218
+
219
+ def start_server(self, fig):
220
+ """Start serving the page for ``fig`` without opening a browser.
221
+
222
+ Returns
223
+ -------
224
+ ``(url, thread)``: the page's address, and the thread serving it, which ends once the
225
+ page has been closed or has not been heard from for ``IDLE_TIMEOUT`` seconds
226
+ """
227
+ if not isinstance(fig, dict):
228
+ fig = go.Figure(fig) # snapshot: later changes made by the script are not exported
229
+ fig_dict = fig if isinstance(fig, dict) else fig.to_dict()
230
+ token = secrets.token_urlsafe(24)
231
+ script = (_POST_SCRIPT.replace("__TOKEN__", token)
232
+ .replace("__CONFIG__", json.dumps(self.config))
233
+ .replace("__PING_MS__", str(PING_MS))
234
+ .replace("__ICON_PATH__", TIKZ_ICON_PATH))
235
+ html = pio.to_html(
236
+ fig_dict, config=self.config, include_plotlyjs=True,
237
+ include_mathjax="cdn", post_script=script, full_html=True, # type: ignore[reportArgumentType]
238
+ default_width="100%", default_height="100%", validate=False,
239
+ ).encode("utf-8")
240
+
241
+ renderer = self
242
+ state = {}
243
+
244
+ class Handler(BaseHTTPRequestHandler):
245
+ """Serves the page, and answers the requests sent by its script."""
246
+
247
+ def _reply(self, status, body, ctype="application/json"):
248
+ self.send_response(status)
249
+ self.send_header("Content-Type", ctype)
250
+ self.send_header("Content-Length", str(len(body)))
251
+ self.end_headers()
252
+ self.wfile.write(body)
253
+
254
+ def _host_ok(self):
255
+ # Rejects DNS rebinding: a remote site reaching this server under its own name.
256
+ server = cast(ThreadingHTTPServer, self.server)
257
+ return self.headers.get("Host") == f"127.0.0.1:{server.server_port}"
258
+
259
+ def do_GET(self): # pylint: disable=invalid-name
260
+ """Serve the page."""
261
+ if not self._host_ok() or self.path != "/":
262
+ self._reply(403, b"{}")
263
+ return
264
+ state["deadline"] = time.monotonic() + IDLE_TIMEOUT
265
+ self._reply(200, html, "text/html; charset=utf-8")
266
+
267
+ def do_POST(self): # pylint: disable=invalid-name
268
+ """Handle ``/<token>/ping``, ``/<token>/closing`` and ``/<token>/export``."""
269
+ raw = self.rfile.read(int(self.headers.get("Content-Length") or 0))
270
+ prefix = f"/{token}/"
271
+ if not self._host_ok() or not self.path.startswith(prefix):
272
+ self._reply(403, b"{}")
273
+ return
274
+ action = self.path[len(prefix):]
275
+ if action == "closing":
276
+ state["deadline"] = time.monotonic() + CLOSE_GRACE
277
+ self._reply(200, b"{}")
278
+ return
279
+ state["deadline"] = time.monotonic() + IDLE_TIMEOUT
280
+ if action == "ping":
281
+ self._reply(200, b"{}")
282
+ return
283
+ if action != "export":
284
+ self._reply(404, b"{}")
285
+ return
286
+ try:
287
+ path = renderer.export(fig, json.loads(raw or b"{}"))
288
+ result = {"ok": True, "path": str(path)}
289
+ except Exception as exc: # pylint: disable=broad-exception-caught # shown in the page
290
+ result = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
291
+ self._reply(200, json.dumps(result).encode("utf-8"))
292
+
293
+ def log_message(self, format, *args): # pylint: disable=redefined-builtin
294
+ pass
295
+
296
+ server = _LocalServer(("127.0.0.1", 0), Handler)
297
+ server.timeout = 0.5
298
+ state["deadline"] = time.monotonic() + IDLE_TIMEOUT # counted from when the server is ready
299
+
300
+ def serve():
301
+ while time.monotonic() < state["deadline"]:
302
+ server.handle_request()
303
+ server.server_close()
304
+
305
+ # Not a daemon thread: the script may reach its end while the page is still open,
306
+ # and the button has to keep working until the page is closed.
307
+ thread = threading.Thread(target=serve, name="tikzplotly-browser", daemon=False)
308
+ thread.start()
309
+ return f"http://127.0.0.1:{server.server_port}/", thread
310
+
311
+ def render(self, fig_dict):
312
+ """Called by Plotly, for ``fig.show(renderer="tikzplotly")``."""
313
+ url, _ = self.start_server(fig_dict)
314
+ webbrowser.open(url)
315
+
316
+
317
+ def show(fig, output="figure.tex", config=None, **tikz_options):
318
+ """Show a figure in the web browser, with an "Export to TikZ" button in its mode bar.
319
+
320
+ Each click on the button writes the TikZ code of the figure, as currently displayed (zoom,
321
+ traces hidden from the legend), to ``output``, or to ``output_1.tex``, ``output_2.tex``, ...
322
+ if that file already exists. The Python process keeps running while the page is open,
323
+ and stops a few seconds after it is closed.
324
+
325
+ Parameters
326
+ ----------
327
+ fig
328
+ Plotly figure
329
+ output, optional
330
+ file written by the button, by default ``"figure.tex"``
331
+ config, optional
332
+ Plotly config dict for the page, by default None
333
+ **tikz_options
334
+ passed on to :func:`get_tikz_code` (``tikz_options``, ``axis_options``, ``decimate``...)
335
+ """
336
+ renderer = TikzBrowserRenderer(output, config=config, **tikz_options)
337
+ url, _ = renderer.start_server(fig)
338
+ webbrowser.open(url)
339
+
340
+
341
+ def register_renderer():
342
+ """Register the ``"tikzplotly"`` renderer with Plotly, so ``fig.show(renderer="tikzplotly")`` works."""
343
+ if ExternalRenderer is object: # pragma: no cover
344
+ return
345
+ if RENDERER_NAME not in pio.renderers:
346
+ pio.renderers[RENDERER_NAME] = TikzBrowserRenderer()
tikzplotly/_color.py CHANGED
@@ -30,7 +30,7 @@ def convert_color(color):
30
30
  string from plotly color palette, can be one of the following:
31
31
  - hex string : "#______"
32
32
  - rgb string : "rgb(__, __, __)" with values between 0 and 255
33
- - rgba string : "rgba(__, __, __, __)" with values between 0 and 255 and an opacity value between 0 and 1
33
+ - rgba string : "rgba(__, __, __[, __])" with values between 0 and 255 and an opacity value between 0 and 1 (optional)
34
34
  - color name : "red", "green", "blue", "yellow", "orange", "purple", "brown", "black", "gray", "white" or any other color name from
35
35
  https://github.com/plotly/plotly.py/blob/main/templategen/utils/colors.py
36
36
 
@@ -45,20 +45,21 @@ def convert_color(color):
45
45
  if color is None:
46
46
  return None, None, None, 1
47
47
  if isinstance(color, numpy.ndarray):
48
- warn("Color from data is not supported yet. Returning the default color: blue.")
49
- return "blue", "HTML", "0000ff", 1
48
+ warn("Color from data is not supported yet. Returning no color.")
49
+ return None, None, None, 1
50
50
  if not isinstance(color, str):
51
- warn(f"Color {color} type '{color.__class__.__name__}' is not supported yet. Returning the default color: blue.")
52
- return "blue", "HTML", "0000ff", 1
51
+ warn(f"Color {color} type '{color.__class__.__name__}' is not supported yet. Returning no color.")
52
+ return None, None, None, 1
53
53
 
54
54
  if color.startswith("#"):
55
55
  return color[1:], "HTML", color[1:], 1
56
56
 
57
57
  if color.startswith("rgba"):
58
- sp = color.split("(")[1].split(",")
59
- rgb_color = sp[:-1]
60
- rgb_color = convert_color(f"rgb({rgb_color[0]},{rgb_color[1]},{rgb_color[2]})")[:-1]
61
- return rgb_color + (float(sp[-1][:-1]),)
58
+ sp = color.split("(")[1].replace(')', '').split(",")
59
+ rgb_color = [s.strip() for s in sp]
60
+ r, g, b = int(rgb_color[0]), int(rgb_color[1]), int(rgb_color[2])
61
+ opacity = float(rgb_color[3]) if len(rgb_color) == 4 else 1.0
62
+ return hashlib.sha1(f"{r}, {g}, {b}".encode('UTF-8')).hexdigest()[:10], "RGB", rgb_str(r, g, b), opacity
62
63
 
63
64
  if color.startswith("rgb"):
64
65
  color = color[4:-1].replace("[", "{").replace("]", "}")
@@ -46,7 +46,7 @@ class Data:
46
46
  """Class to handle data in TikZ plots.
47
47
  """
48
48
 
49
- def __init__(self, name, x):
49
+ def __init__(self, name, x, decimate: int|None=None):
50
50
  """Initialize a Data object.
51
51
 
52
52
  Parameters
@@ -59,6 +59,8 @@ class Data:
59
59
  self.name = name
60
60
  self.macro_name = "\\" + replace_all_digits(name)
61
61
  self.x = x
62
+ # decimate: None means keep all points, otherwise integer >= 1
63
+ self.decimate = decimate
62
64
  self.y_label = []
63
65
  self.y_data = []
64
66
 
@@ -84,7 +86,7 @@ class Data:
84
86
  class Data3D:
85
87
  """Handle 3D data in Tikz plots
86
88
  """
87
- def __init__(self, x, y, z, name):
89
+ def __init__(self, x, y, z, name, decimate: int|None=None):
88
90
  """Initialize the Data3D object
89
91
 
90
92
  Parameters
@@ -101,6 +103,8 @@ class Data3D:
101
103
  self.x = np.array(x)
102
104
  self.y = np.array(y)
103
105
  self.z = np.array(z)
106
+ # decimate: None means keep all points, otherwise integer >= 1
107
+ self.decimate = decimate
104
108
  if name:
105
109
  self.name = sanitize_text(name, keep_space=0)
106
110
  else:
@@ -128,7 +132,7 @@ class DataContainer:
128
132
  def __init__(self):
129
133
  self.data = []
130
134
 
131
- def add_data(self, x, y, name=None, y_label=None):
135
+ def add_data(self, x, y, name=None, y_label=None, decimate: int|None=None):
132
136
  """Add data to the container.
133
137
 
134
138
  Parameters
@@ -141,11 +145,16 @@ class DataContainer:
141
145
  name of the y data, by default None
142
146
  name, optional
143
147
  name of the data, by default None
148
+ decimate, optional
149
+ number of data to keep, all of them if None, by default None.
144
150
 
145
151
  Returns
146
152
  -------
147
153
  tuple (macro_name, y_label), where macro_name is the name of the data in LaTeX and y_label the name of the y data in LaTeX
148
154
  """
155
+ if decimate is not None and (not isinstance(decimate, int) or decimate < 1):
156
+ raise ValueError("decimate must be None or an integer >= 1")
157
+
149
158
  for data in self.data:
150
159
  if len(data.x) != len(x):
151
160
  continue
@@ -157,12 +166,12 @@ class DataContainer:
157
166
  elif hasattr(are_equals, "all") and are_equals.all():
158
167
  y_label_val = data.add_y_data(y, y_label or name)
159
168
  return data.macro_name, treat_data(y_label_val)
160
- data_to_add = Data(f"data{index_to_letters(len(self.data))}", x)
169
+ data_to_add = Data(f"data{index_to_letters(len(self.data))}", x, decimate=decimate)
161
170
  y_label_val = data_to_add.add_y_data(y, y_label or name)
162
171
  self.data.append(data_to_add)
163
172
  return data_to_add.macro_name, treat_data(y_label_val)
164
173
 
165
- def add_data3d(self, x, y, z, name=None):
174
+ def add_data3d(self, x, y, z, name=None, decimate: int|None=None):
166
175
  """Add data to the container.
167
176
 
168
177
  Parameters
@@ -175,18 +184,23 @@ class DataContainer:
175
184
  z values of the data
176
185
  name, optional
177
186
  name of the data, by default None
187
+ decimate, optional
188
+ number of data to keep, all of them if None, by default None.
178
189
 
179
190
  Returns
180
191
  -------
181
192
  tuple (macro_name, z_name), where macro_name is the name of the data in LaTeX and z_name the name of the z data in LaTeX
182
193
  """
194
+ if decimate is not None and (not isinstance(decimate, int) or decimate < 1):
195
+ raise ValueError("decimate must be None or an integer >= 1")
196
+
183
197
  for data in self.data:
184
198
  if hasattr(data, "x") and hasattr(data, "y") and hasattr(data, "z"):
185
199
  if np.array_equal(data.x, x) and np.array_equal(data.y, y) and np.array_equal(data.z, z):
186
200
  return data.name, data.z_name
187
- data_obj = Data3D(x, y, z, name)
201
+ data_obj = Data3D(x, y, z, name, decimate=decimate)
188
202
  self.data.append(data_obj)
189
- return data_obj.name, data_obj.z_name
203
+ return data_obj.name
190
204
 
191
205
  def export_data(self):
192
206
  """Generate LaTeX code to export the data from DataContainer.
@@ -202,8 +216,20 @@ class DataContainer:
202
216
  if hasattr(data, "z"):
203
217
  export_string += "\\pgfplotstableread{\n"
204
218
  export_string += "x y z\n"
205
- for x, y, z in zip(data.x, data.y, data.z):
206
- export_string += f"{treat_data(x)} {treat_data(y)} {treat_data(z)}\n"
219
+ n = len(data.x)
220
+ if getattr(data, 'decimate', None) is None:
221
+ indices = range(n)
222
+ else:
223
+ d = data.decimate
224
+ if d <= 1:
225
+ indices = range(n)
226
+ else:
227
+ # always include first and last
228
+ indices = list(range(0, n, d))
229
+ if indices[-1] != n-1:
230
+ indices.append(n-1)
231
+ for i in indices:
232
+ export_string += f"{treat_data(data.x[i])} {treat_data(data.y[i])} {treat_data(data.z[i])}\n"
207
233
  export_string += f"}}{{\\{data.name}}}\n"
208
234
 
209
235
  # 2D
@@ -216,8 +242,19 @@ class DataContainer:
216
242
  else:
217
243
  header += " y"
218
244
  export_string += header + "\n"
219
- for i, x in enumerate(data.x):
220
- row = [treat_data(x)]
245
+ n = len(data.x)
246
+ if getattr(data, 'decimate', None) is None:
247
+ indices = range(n)
248
+ else:
249
+ d = data.decimate
250
+ if d <= 1:
251
+ indices = range(n)
252
+ else:
253
+ indices = list(range(0, n, d))
254
+ if indices[-1] != n-1:
255
+ indices.append(n-1)
256
+ for i in indices:
257
+ row = [treat_data(data.x[i])]
221
258
  for y_col in data.y_data:
222
259
  row.append(treat_data(y_col[i]))
223
260
  export_string += " ".join(row) + "\n"
tikzplotly/_heatmap.py CHANGED
@@ -102,9 +102,10 @@ def draw_heatmap(data, fig, img_name, axis: Axis):
102
102
 
103
103
  img_bytes = fig_copy.to_image(format="png") # The image created by plotly keeps places around the heatmap
104
104
  cropped_image = crop_image(Image.open(io.BytesIO(img_bytes))) # so we crop all the white around the figure
105
- resized_image = resize_image(cropped_image, *figure_data.shape) # and we resize it so each square is a 1px x 1px square
105
+ resized_image = resize_image(cropped_image, *figure_data.shape) # and we resize it so each square is a 1px x 1px square
106
106
 
107
- os.makedirs(os.path.dirname(img_name), exist_ok=True) # Make sure the directory exists
107
+ dirname = os.path.dirname(img_name) or '.' # Ensure that dirname is not empty
108
+ os.makedirs(dirname, exist_ok=True) # Make sure the directory exists
108
109
  resized_image.save(img_name)
109
110
 
110
111
 
tikzplotly/_histogram.py CHANGED
@@ -50,6 +50,27 @@ def formalize_data(data, axis:Axis, row_sep="\\\\"):
50
50
 
51
51
  return data_str
52
52
 
53
+ def treat_histnorm(histnorm: str, hist_options: dict):
54
+ """Handle the normalization option of the histogram
55
+
56
+ Parameters
57
+ ----------
58
+ histnorm : str
59
+ The normalization mode for the histogram.
60
+ hist_options : dict
61
+ Dictionary to store histogram options.
62
+ """
63
+ if histnorm in ("percent", "probability", "density"):
64
+ warn(
65
+ f"Sorry, I did not find an equivalent for histnorm='{histnorm}' in TikZ. "
66
+ "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code "
67
+ "that would plot this :).\nFor now, the histogram will be plotted without normalization "
68
+ "(as if histnorm='probability density')."
69
+ )
70
+ hist_options["density"] = None
71
+ elif histnorm in ("probability density", ):
72
+ hist_options["density"] = None
73
+
53
74
  def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"):
54
75
  """
55
76
  Draw a histogram and return the TikZ code.
@@ -79,7 +100,8 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"):
79
100
 
80
101
  code = ""
81
102
 
82
- plot_options = {"hist": None}
103
+ plot_options = {}
104
+ plot_options["hist"] = None
83
105
  type_options = {"row sep": row_sep, "y index": 0}
84
106
  hist_options = {}
85
107
 
@@ -92,36 +114,15 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"):
92
114
  axis.add_option("x filter/.expression", "rawy")
93
115
  axis.add_option("y filter/.expression", "rawx")
94
116
  hist_options["handler/.style"] = "{xbar interval}"
95
-
117
+ else: # trace.x and trace.y are both empty
118
+ warn("Empty histogram. Returning no plot")
119
+ return ""
96
120
 
97
121
 
98
122
  if trace.nbinsx is not None:
99
123
  hist_options["bins"] = trace.nbinsx
100
124
 
101
- if trace.histnorm == "percent":
102
- warn(
103
- f"Sorry, I did not find an equivalent for histnorm='{trace.histnorm}' in TikZ. "
104
- "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code "
105
- "that would plot this :).\nFor now, the histogram will be plotted without normalization "
106
- "(as if histnorm='probability density')."
107
- )
108
- hist_options["density"] = None
109
- elif trace.histnorm == "probability":
110
- warn(
111
- f"Sorry, I did not find an equivalent for histnorm='{trace.histnorm}' in TikZ. "
112
- "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code that would plot this :).\n"
113
- "For now, the histogram will be plotted without normalization (as if histnorm='probability density')."
114
- )
115
- hist_options["density"] = None
116
- elif trace.histnorm == "density":
117
- warn(
118
- f"Sorry, I did not find an equivalent for histnorm='{trace.histnorm}' in TikZ. "
119
- "If you need this feature implemented, please open an issue, if possible with a MWE pgfplots code that would plot this :).\n"
120
- "For now, the histogram will be plotted without normalization (as if histnorm='probability density')."
121
- )
122
- hist_options["density"] = None
123
- elif trace.histnorm == "probability density":
124
- hist_options["density"] = None
125
+ treat_histnorm(trace.histnorm, hist_options)
125
126
 
126
127
  if trace.cumulative.enabled:
127
128
  hist_options["cumulative"] = None
@@ -152,5 +153,4 @@ def draw_histogram(trace, axis: Axis, colors_set, row_sep="\\\\"):
152
153
  code += tex_addplot(data_str, plot_type = "table",
153
154
  options = option_dict_to_str(plot_options), type_options=option_dict_to_str(type_options))
154
155
 
155
-
156
156
  return code
tikzplotly/_polar.py CHANGED
@@ -7,7 +7,7 @@ from ._axis import Axis
7
7
  from ._utils import option_dict_to_str
8
8
  from ._tex import tex_addplot
9
9
  from ._color import convert_color
10
- from ._dataContainer import DataContainer
10
+ from ._data_container import DataContainer
11
11
 
12
12
  def get_polar_coord(trace, axis: Axis, data_container: DataContainer):
13
13
  """Get polar coordinates from the trace
tikzplotly/_save.py CHANGED
@@ -19,16 +19,17 @@ from ._polar import get_polar_coord, draw_scatterpolar
19
19
  from ._axis import Axis
20
20
  from ._color import convert_color
21
21
  from ._annotations import str_from_annotation
22
- from ._dataContainer import DataContainer
22
+ from ._data_container import DataContainer
23
23
  from ._utils import sanitize_tex_text, sanitize_text
24
24
 
25
25
 
26
26
  def get_tikz_code(
27
27
  fig,
28
- tikz_options = None,
29
- axis_options = None,
30
- include_disclamer = True,
31
- img_name = "heatmap.png",
28
+ tikz_options: dict | None = None,
29
+ axis_options: dict | None = None,
30
+ include_disclamer: bool = True,
31
+ img_name: str = "heatmap.png",
32
+ decimate: int | None = None,
32
33
  ):
33
34
  """Get the tikz code of a figure.
34
35
 
@@ -74,17 +75,17 @@ def get_tikz_code(
74
75
  if trace.y is None:
75
76
  trace.y = list(range(len(trace.x)))
76
77
 
77
- data_name_macro, y_name = data_container.add_data(trace.x, trace.y, trace.name)
78
+ data_name_macro, y_name = data_container.add_data(trace.x, trace.y, trace.name, decimate=decimate)
78
79
 
79
80
  # If x is textual => symbolic x coords
80
81
  if all(isinstance(v, str) for v in trace.x):
81
- sanitized_trace_x = [sanitize_text(x, keep_space=-1) for x in trace.x]
82
+ sanitized_trace_x = [sanitize_text(str(x), keep_space=-1) for x in trace.x]
82
83
  axis.add_option("symbolic x coords", "{" + ",".join(sanitized_trace_x) + "}")
83
84
  axis.add_option("xtick", "data")
84
85
 
85
86
  # If y is textual => symbolic y coords
86
87
  if all(isinstance(v, str) for v in trace.y):
87
- sanitized_trace_y = [sanitize_text(y, keep_space=-1) for y in trace.y]
88
+ sanitized_trace_y = [sanitize_text(str(y), keep_space=-1) for y in trace.y]
88
89
  axis.add_option("symbolic y coords", "{" + ",".join(sanitized_trace_y) + "}")
89
90
  axis.add_option("ytick", "data")
90
91
 
@@ -173,7 +174,7 @@ def get_tikz_code(
173
174
  if hasattr(figure_layout.scene, "title") and getattr(figure_layout.scene.title, "text", None):
174
175
  axis.add_option("title", f"{{{sanitize_tex_text(figure_layout.scene.title.text)}}}")
175
176
 
176
- data_name_macro, z_name = data_container.add_data3d(trace.x, trace.y, trace.z, trace.name)
177
+ data_name_macro = data_container.add_data3d(trace.x, trace.y, trace.z, trace.name, decimate=decimate)
177
178
  data_str.append(draw_scatter3d(data_name_macro, trace, colors_set))
178
179
 
179
180
  if trace.name and trace['showlegend'] is not False:
@@ -228,7 +229,7 @@ def get_tikz_code(
228
229
  return code
229
230
 
230
231
 
231
- def save(filepath, *args, **kwargs):
232
+ def save(filepath: str | Path, *args, **kwargs):
232
233
  """Save a figure to a file or a stream.
233
234
 
234
235
  Parameters
tikzplotly/_scatter.py CHANGED
@@ -2,16 +2,16 @@
2
2
  Provides functionality to convert Plotly scatter traces into TikZ/PGFPlots code for LaTeX documents.
3
3
  """
4
4
 
5
- from warnings import warn
6
5
  import numpy as np
7
6
  from ._tex import tex_addplot, tex_add_text
8
7
  from ._color import convert_color
9
- from ._marker import marker_symbol_to_tex
10
8
  from ._dash import DASH_PATTERN
11
9
  from ._axis import Axis
12
10
  from ._data import data_type
11
+ from ._trace_utils import configure_marker_options, finalize_marker_options
13
12
  from ._utils import px_to_pt, option_dict_to_str
14
13
 
14
+
15
15
  def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set):
16
16
  """Get code for a scatter trace.
17
17
 
@@ -53,15 +53,8 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set):
53
53
  options_dict = {}
54
54
  mark_option_dict = {}
55
55
 
56
- if mode == "markers":
57
- if marker.symbol is not None:
58
- symbol, symbol_options = marker_symbol_to_tex(marker.symbol)
59
- options_dict["mark"] = symbol
60
- options_dict["only marks"] = None
61
- if symbol_options is not None:
62
- mark_option_dict[symbol_options[0]] = symbol_options[1]
63
- else:
64
- options_dict["only marks"] = None
56
+ if "markers" in mode:
57
+ configure_marker_options(mode, marker, options_dict, mark_option_dict, color_set)
65
58
 
66
59
  if scatter.marker.size is not None:
67
60
  options_dict["mark size"] = px_to_pt(marker.size)
@@ -71,14 +64,7 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set):
71
64
  mark_option_dict["solid"] = None
72
65
  mark_option_dict["fill"] = convert_color(scatter.marker.color)[0]
73
66
 
74
- if (line:=scatter.marker.line) is not None:
75
- if line.color is not None:
76
- color_set.add(convert_color(line.color)[:3])
77
- mark_option_dict["draw"] = convert_color(line.color)[0]
78
- if line.width is not None:
79
- mark_option_dict["line width"] = px_to_pt(line.width)
80
-
81
- if (angle:=scatter.marker.angle) is not None:
67
+ if (angle := scatter.marker.angle) is not None:
82
68
  mark_option_dict["rotate"] = angle
83
69
 
84
70
  if (opacity := scatter.opacity) is not None:
@@ -86,22 +72,7 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set):
86
72
  if (opacity := scatter.marker.opacity) is not None:
87
73
  mark_option_dict["opacity"] = np.round(opacity, 2)
88
74
 
89
- if mark_option_dict:
90
- mark_options = option_dict_to_str(mark_option_dict)
91
- options_dict["mark options"] = f"{{{mark_options}}}"
92
-
93
- elif mode == "lines":
94
- options_dict["mark"] = "none"
95
-
96
- elif "lines" in mode and "markers" in mode:
97
- if marker.symbol is not None:
98
- symbol, symbol_options = marker_symbol_to_tex(marker.symbol)
99
- options_dict["mark"] = symbol
100
- if symbol_options is not None:
101
- mark_option_dict[symbol_options[0]] = symbol_options[1]
102
-
103
- else:
104
- warn(f"Scatter : Mode {mode} is not supported yet.")
75
+ finalize_marker_options(mode, options_dict, mark_option_dict, "Scatter")
105
76
 
106
77
  if scatter.line.width is not None:
107
78
  options_dict["line width"] = px_to_pt(scatter.line.width)
@@ -110,7 +81,6 @@ def draw_scatter2d(data_name, scatter, y_name, axis: Axis, color_set):
110
81
  if scatter.connectgaps in [False, None] and None in scatter.x:
111
82
  options_dict["unbounded coords"] = "jump"
112
83
 
113
-
114
84
  if scatter.line.color is not None:
115
85
  options_dict["color"] = convert_color(scatter.line.color)[0]
116
86
  if "mark" in mode:
tikzplotly/_scatter3d.py CHANGED
@@ -1,12 +1,13 @@
1
1
  """
2
2
  Provides functionality to convert Plotly 3D scatter traces into TikZ/PGFPlots code for LaTeX documents.
3
3
  """
4
- from warnings import warn
4
+
5
5
  import numpy as np
6
6
  from ._color import convert_color
7
- from ._marker import marker_symbol_to_tex
7
+ from ._trace_utils import configure_marker_options, finalize_marker_options
8
8
  from ._utils import px_to_pt, option_dict_to_str
9
9
 
10
+
10
11
  def draw_scatter3d(data_name, scatter, color_set):
11
12
  """
12
13
  Get code for a scatter3d trace.
@@ -28,16 +29,8 @@ def draw_scatter3d(data_name, scatter, color_set):
28
29
  options_dict = {}
29
30
  mark_option_dict = {}
30
31
 
31
- # Markers only
32
- if mode == "markers":
33
- if marker.symbol is not None:
34
- symbol, symbol_options = marker_symbol_to_tex(marker.symbol)
35
- options_dict["mark"] = symbol
36
- options_dict["only marks"] = None
37
- if symbol_options is not None:
38
- mark_option_dict[symbol_options[0]] = symbol_options[1]
39
- else:
40
- options_dict["only marks"] = None
32
+ if "markers" in mode:
33
+ configure_marker_options(mode, marker, options_dict, mark_option_dict, color_set)
41
34
 
42
35
  if marker.size is not None:
43
36
  size = marker.size
@@ -53,34 +46,12 @@ def draw_scatter3d(data_name, scatter, color_set):
53
46
  mark_option_dict["solid"] = None
54
47
  mark_option_dict["fill"] = convert_color(c)[0]
55
48
 
56
- if (line := marker.line) is not None:
57
- if line.color is not None:
58
- color_set.add(convert_color(line.color)[:3])
59
- mark_option_dict["draw"] = convert_color(line.color)[0]
60
- if line.width is not None:
61
- mark_option_dict["line width"] = px_to_pt(line.width)
62
-
63
49
  if (opacity := scatter.opacity) is not None:
64
50
  options_dict["opacity"] = opacity
65
51
  if (opacity := marker.opacity) is not None:
66
52
  mark_option_dict["opacity"] = opacity
67
53
 
68
- if mark_option_dict:
69
- mark_options = option_dict_to_str(mark_option_dict)
70
- options_dict["mark options"] = f"{{{mark_options}}}"
71
-
72
- elif mode == "lines":
73
- options_dict["mark"] = "none"
74
-
75
- elif "lines" in mode and "markers" in mode:
76
- if marker.symbol is not None:
77
- symbol, symbol_options = marker_symbol_to_tex(marker.symbol)
78
- options_dict["mark"] = symbol
79
- if symbol_options is not None:
80
- mark_option_dict[symbol_options[0]] = symbol_options[1]
81
-
82
- else:
83
- warn(f"Scatter3d : Mode {mode} is not supported yet.")
54
+ finalize_marker_options(mode, options_dict, mark_option_dict, "Scatter3d")
84
55
 
85
56
  if scatter.line is not None:
86
57
  if scatter.line.width is not None:
@@ -0,0 +1,70 @@
1
+ """Shared helpers for Plotly trace to TikZ conversion."""
2
+
3
+ from warnings import warn
4
+
5
+ from ._color import convert_color
6
+ from ._marker import marker_symbol_to_tex
7
+ from ._utils import px_to_pt, option_dict_to_str
8
+
9
+
10
+ def configure_marker_options(mode, marker, options_dict, mark_option_dict, color_set):
11
+ """Populate marker-related TikZ options for scatter-like traces.
12
+
13
+ Parameters
14
+ ----------
15
+ mode
16
+ Plotly trace mode string (for example ``markers`` or ``markers+lines``).
17
+ marker
18
+ Plotly marker object from the trace.
19
+ options_dict
20
+ Dictionary collecting top-level PGFPlots options.
21
+ mark_option_dict
22
+ Dictionary collecting nested ``mark options`` for PGFPlots.
23
+ color_set
24
+ Set used to track colors that must be defined in the LaTeX output.
25
+ """
26
+
27
+ if marker.symbol is not None:
28
+ symbol, symbol_options = marker_symbol_to_tex(marker.symbol)
29
+ options_dict["mark"] = symbol
30
+ if "lines" not in mode:
31
+ options_dict["only marks"] = None
32
+ if symbol_options is not None:
33
+ mark_option_dict[symbol_options[0]] = symbol_options[1]
34
+ elif "lines" not in mode:
35
+ options_dict["only marks"] = None
36
+
37
+ if (line := marker.line) is not None:
38
+ if line.color is not None:
39
+ color = convert_color(line.color)
40
+ color_set.add(color[:3])
41
+ mark_option_dict["draw"] = color[0]
42
+ if line.width is not None:
43
+ mark_option_dict["line width"] = px_to_pt(line.width)
44
+
45
+
46
+ def finalize_marker_options(mode, options_dict, mark_option_dict, trace_name):
47
+ """Finalize marker options and handle unsupported modes.
48
+
49
+ Parameters
50
+ ----------
51
+ mode
52
+ Plotly trace mode string.
53
+ options_dict
54
+ Dictionary collecting top-level PGFPlots options.
55
+ mark_option_dict
56
+ Dictionary collecting nested ``mark options`` for PGFPlots.
57
+ trace_name
58
+ Name used in warnings to identify the trace kind.
59
+ """
60
+
61
+ if "markers" in mode:
62
+ if mark_option_dict:
63
+ mark_options = option_dict_to_str(mark_option_dict)
64
+ options_dict["mark options"] = f"{{{mark_options}}}"
65
+ return
66
+
67
+ if mode == "lines":
68
+ options_dict["mark"] = "none"
69
+ else:
70
+ warn(f"{trace_name} : Mode {mode} is not supported yet.")
tikzplotly/_utils.py CHANGED
@@ -93,18 +93,10 @@ def sanitize_char(ch: str, keep_space: int = 0) -> str:
93
93
  if ch == "@":
94
94
  return "at"
95
95
  if ch == " ":
96
- if keep_space == 1:
97
- return " "
98
- if keep_space == 0:
99
- return "_"
100
- return ""
96
+ return " " if keep_space == 1 else "_" if keep_space == 0 else ""
101
97
  if ch in "[]{}= ":
102
98
  return f"x{ord(ch):x}"
103
- # if not ascii, return hex
104
- if ord(ch) > 127:
105
- return f"x{ord(ch):x}"
106
- # if not printable, return hex
107
- if not ch.isprintable():
99
+ if ord(ch) > 127 or not ch.isprintable(): # not ascii or not printable: return hex
108
100
  return f"x{ord(ch):x}"
109
101
  return ch
110
102
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tikzplotly
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Convert plotly figures to LaTeX / TikZ figures
5
5
  Author-email: Thomas Saigre <tikzplotly@outlook.fr>
6
6
  License: The MIT License (MIT)
@@ -30,11 +30,11 @@ Keywords: plotly,latex,tikz,figure,conversion,graphics
30
30
  Classifier: Development Status :: 5 - Production/Stable
31
31
  Classifier: Programming Language :: Python
32
32
  Classifier: Programming Language :: Python :: 3
33
- Classifier: Programming Language :: Python :: 3.9
34
33
  Classifier: Programming Language :: Python :: 3.10
35
34
  Classifier: Programming Language :: Python :: 3.11
36
35
  Classifier: Programming Language :: Python :: 3.12
37
36
  Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Programming Language :: Python :: 3.14
38
38
  Classifier: License :: OSI Approved :: MIT License
39
39
  Classifier: Operating System :: OS Independent
40
40
  Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
@@ -44,6 +44,7 @@ License-File: LICENSE
44
44
  Requires-Dist: numpy
45
45
  Requires-Dist: plotly
46
46
  Requires-Dist: pillow
47
+ Requires-Dist: kaleido
47
48
  Provides-Extra: dev
48
49
  Requires-Dist: pandas; extra == "dev"
49
50
  Dynamic: license-file
@@ -65,7 +66,7 @@ Dynamic: license-file
65
66
 
66
67
  # Tikzplotly
67
68
 
68
- Convert [plotly](https://plotly.com/python/) figures to TikZ code for inclusion into [PGFPlots](https://www.ctan.org/pkg/pgfplots) ([PGF/TikZ](https://www.ctan.org/pkg/pgf)) figures.
69
+ Convert [Plotly](https://plotly.com/python/) figures to TikZ code for inclusion into [PGFPlots](https://www.ctan.org/pkg/pgfplots) ([PGF/TikZ](https://www.ctan.org/pkg/pgf)) figures.
69
70
 
70
71
  This results in a ti*k*z code, that can be easily included into your LaTeX document.
71
72
  This also allows to easily edit the content of the figure.
@@ -151,9 +152,9 @@ To correctly compile the document, you will need to add the following packages t
151
152
 
152
153
  ## Note
153
154
 
154
- * This module is in development and new features are added bit by bit, when needed. If you have a feature request, please open an issue with the plotly figure you want to convert and the desired output.
155
+ * This module is in development and new features are added bit by bit, when needed. If you have a feature request, please open an issue with the Plotly figure you want to convert and the desired output.
155
156
  You can also submit a pull request with the desired feature !
156
- * Some feature can result in different output between the plotly figure and the TikZ figure, for instance the size of markers, more details can be found in [the documentation](https://thomas-saigre.github.io/tikzplotly/).
157
+ * Some feature can result in different output between the Plotly figure and the TikZ figure, for instance the size of markers, more details can be found in [the documentation](https://thomas-saigre.github.io/tikzplotly/).
157
158
 
158
159
 
159
160
  ## References
@@ -0,0 +1,32 @@
1
+ tests/test_bars.py,sha256=SGLDCNubaThFso47yad5w37AYrrVpoNK5fUkzuvAjtc,10122
2
+ tests/test_heatmaps.py,sha256=CIPadAP56Bb3cRTAqngo0O2iZPR_RSSVeopQdrikz_A,6251
3
+ tests/test_histograms.py,sha256=Ry4SsM4JBD5rXrvN6bxebNmLNK0mOZE5XIV9RXlQod4,8971
4
+ tests/test_line_charts.py,sha256=cyiVuUyH1lZ51p_JVcP3xjLlEEbSm_iD5gj6F4p8DUo,15811
5
+ tests/test_markers.py,sha256=g3ioyRqRh__VyKGET2RpCiEmLnQAztsYF5drqTq3idU,11555
6
+ tests/test_polar.py,sha256=xvVx-Af855-eIw5Kmq1ApHHCv9hGr2WF6HuaN4XMZms,17244
7
+ tests/test_scatter3d.py,sha256=yfieEogsVS2Wa5DgPJzIvsW2jz8yaPhkVU_nDrBWVTg,4359
8
+ tikzplotly/__about__.py,sha256=sSgIKdBwORCAvELWyq6Xw2FHBdwRMjSmC4bbXwVFQtI,204
9
+ tikzplotly/__init__.py,sha256=_0q4GULEJ5-HsYvckWpomrsdKck1Q7S92A4Ldxl3K9o,946
10
+ tikzplotly/_annotations.py,sha256=r3MPz_Ocx3oQzsCuqKT3pdX1ijb3-LAmYHpLPLulASI,3108
11
+ tikzplotly/_axis.py,sha256=t-v97WnUJMq3ulRi_1E9r0Sv3glodVtqmzIWUftwFlQ,8062
12
+ tikzplotly/_bar.py,sha256=m4Hc4GR-bnEdrS3tU7VgZqCz0mdA9ENfBjGAvjavpA8,3263
13
+ tikzplotly/_browser.py,sha256=GUYAXAhRGr693ouSYC6aAbodrTUhcON2W3vP9aWdwS0,15247
14
+ tikzplotly/_color.py,sha256=hnBMZsVBs1M6YV1_hqquZYuLCO-fBEUaxtwD6cd-fMQ,41213
15
+ tikzplotly/_dash.py,sha256=8hOaqzMDRcikD2HNWc18fzJm2Lvoq3eo3Tu8n0gAqvE,228
16
+ tikzplotly/_data.py,sha256=t3TG0jvc_cjR84Ug04OUDyfF0hKFD_xKOgbgb1Xz-8M,1847
17
+ tikzplotly/_data_container.py,sha256=VMOq6-UmjMDF_Aox_33HxdSrFFZKA_uHG55IoneprDY,8837
18
+ tikzplotly/_heatmap.py,sha256=tUxs4gEaqi0TyY5TIInTJa_APUQPTnV0RXuDkWQ0N7w,5095
19
+ tikzplotly/_histogram.py,sha256=PH0c1ZFU7VnJoWD6NIsF4VJAWReWZhGwNm1iZs7OXu0,5112
20
+ tikzplotly/_marker.py,sha256=L5E_-fb4Fkv231dBmA72byAZeYPeVuKOFZIYTyLbMrs,11839
21
+ tikzplotly/_polar.py,sha256=qhza1x53wgCjAafGIt1ncdRfKFLIlJJE5cvoR80TrIs,7792
22
+ tikzplotly/_save.py,sha256=uqV0lCqf68_OyyfvzMBukf-JaPGG4UkFkFGVMjxnmLE,10343
23
+ tikzplotly/_scatter.py,sha256=Eu9RhAUUZHH4q7l6NWayzVsJh33vPkCvWbCnR4vwUME,3538
24
+ tikzplotly/_scatter3d.py,sha256=Fd3Rx0d8o-CefPSU8CyGD_BNJ7wB8yr_QbaKuWEG1qo,2389
25
+ tikzplotly/_tex.py,sha256=LlsMRncCzmGJGO9OtgKyDMEnVA7WKvLptqh1yM5LdmQ,6555
26
+ tikzplotly/_trace_utils.py,sha256=NyQYkxtrRLHuA9pjfNqUihW3rYvdwvDo8Kj9IEaJo5I,2316
27
+ tikzplotly/_utils.py,sha256=vGvqlwKzv_R6euq1kGp5nmET9ACxy17P72Fv5cBx1Dc,6241
28
+ tikzplotly-0.2.3.dist-info/licenses/LICENSE,sha256=cjxM8VJ_qnjUVKoOndGip4HgyyElZZI8Hk8g3rXEFf8,1080
29
+ tikzplotly-0.2.3.dist-info/METADATA,sha256=kpfNghHJJP-cNXrXbAUlxpqWCwKJKGBjtQMu8XxeD7Q,6644
30
+ tikzplotly-0.2.3.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
31
+ tikzplotly-0.2.3.dist-info/top_level.txt,sha256=NSVJ1u8Bv6DVRb_y_Roui9D7tVh2XYDaGVjSbZyTrH4,17
32
+ tikzplotly-0.2.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,30 +0,0 @@
1
- tests/test_bars.py,sha256=SGLDCNubaThFso47yad5w37AYrrVpoNK5fUkzuvAjtc,10122
2
- tests/test_heatmaps.py,sha256=CIPadAP56Bb3cRTAqngo0O2iZPR_RSSVeopQdrikz_A,6251
3
- tests/test_histograms.py,sha256=Ry4SsM4JBD5rXrvN6bxebNmLNK0mOZE5XIV9RXlQod4,8971
4
- tests/test_line_charts.py,sha256=cyiVuUyH1lZ51p_JVcP3xjLlEEbSm_iD5gj6F4p8DUo,15811
5
- tests/test_markers.py,sha256=g3ioyRqRh__VyKGET2RpCiEmLnQAztsYF5drqTq3idU,11555
6
- tests/test_polar.py,sha256=xvVx-Af855-eIw5Kmq1ApHHCv9hGr2WF6HuaN4XMZms,17244
7
- tests/test_scatter3d.py,sha256=yfieEogsVS2Wa5DgPJzIvsW2jz8yaPhkVU_nDrBWVTg,4359
8
- tikzplotly/__about__.py,sha256=FzUiA25vg6WQJ-ceAlEl9VIpIjolTD0pIM618yZkIV8,204
9
- tikzplotly/__init__.py,sha256=PrbDMOviELxIm9nyXS6Qk56-YhuTb-bAn_0BuY2rHLE,552
10
- tikzplotly/_annotations.py,sha256=r3MPz_Ocx3oQzsCuqKT3pdX1ijb3-LAmYHpLPLulASI,3108
11
- tikzplotly/_axis.py,sha256=t-v97WnUJMq3ulRi_1E9r0Sv3glodVtqmzIWUftwFlQ,8062
12
- tikzplotly/_bar.py,sha256=m4Hc4GR-bnEdrS3tU7VgZqCz0mdA9ENfBjGAvjavpA8,3263
13
- tikzplotly/_color.py,sha256=5qhBog-KjoS_inliITOkuiIfC09FaXZWJ2vUZy0UWpU,41098
14
- tikzplotly/_dash.py,sha256=8hOaqzMDRcikD2HNWc18fzJm2Lvoq3eo3Tu8n0gAqvE,228
15
- tikzplotly/_data.py,sha256=t3TG0jvc_cjR84Ug04OUDyfF0hKFD_xKOgbgb1Xz-8M,1847
16
- tikzplotly/_dataContainer.py,sha256=6TISCqi-EUmZ_PlzDo2IxxHakddp2UPrA7uwIMpjRiE,7057
17
- tikzplotly/_heatmap.py,sha256=Pd_08D8Yt6TH-NsenSws6MnTmbxtvr17dY5s-6PE05Q,5023
18
- tikzplotly/_histogram.py,sha256=INS965-KmYYbFuG0fTOSXesmKduv4l3tz9hlbmU2xZE,5520
19
- tikzplotly/_marker.py,sha256=L5E_-fb4Fkv231dBmA72byAZeYPeVuKOFZIYTyLbMrs,11839
20
- tikzplotly/_polar.py,sha256=tXrZQqpCdWz15kYkKBI2CMwTyYvotlHhyObkaa4asMc,7791
21
- tikzplotly/_save.py,sha256=S4MeaaTcPRXMSmDpHMUXUoqathkhy-wDUORdpzS7NHQ,10216
22
- tikzplotly/_scatter.py,sha256=soPBLJqvy9vUUwwFPhrtJbpFFWtHehEJZ-ec4-LXJAw,4681
23
- tikzplotly/_scatter3d.py,sha256=49EzI-0IuQU8AcQua7N3Y87U1N6bp1qe_PpFgGEIEXc,3545
24
- tikzplotly/_tex.py,sha256=LlsMRncCzmGJGO9OtgKyDMEnVA7WKvLptqh1yM5LdmQ,6555
25
- tikzplotly/_utils.py,sha256=MykikiJzMmVFUIBsnesLj2OzIyoUwDgKTSqs8zkTv6o,6346
26
- tikzplotly-0.2.1.dist-info/licenses/LICENSE,sha256=cjxM8VJ_qnjUVKoOndGip4HgyyElZZI8Hk8g3rXEFf8,1080
27
- tikzplotly-0.2.1.dist-info/METADATA,sha256=DRQTfURJS7wCFhH6lAZuLH5FPkfbv_bGw4TB4SEbg0Q,6620
28
- tikzplotly-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
- tikzplotly-0.2.1.dist-info/top_level.txt,sha256=NSVJ1u8Bv6DVRb_y_Roui9D7tVh2XYDaGVjSbZyTrH4,17
30
- tikzplotly-0.2.1.dist-info/RECORD,,