tikzplotly 0.2.2__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.2"
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()
@@ -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,16 +184,21 @@ 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
203
  return data_obj.name
190
204
 
@@ -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/_save.py CHANGED
@@ -25,10 +25,11 @@ from ._utils import sanitize_tex_text, sanitize_text
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,7 +75,7 @@ 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):
@@ -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 = 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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tikzplotly
3
- Version: 0.2.2
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
@@ -66,7 +66,7 @@ Dynamic: license-file
66
66
 
67
67
  # Tikzplotly
68
68
 
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
+ 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.
70
70
 
71
71
  This results in a ti*k*z code, that can be easily included into your LaTeX document.
72
72
  This also allows to easily edit the content of the figure.
@@ -152,9 +152,9 @@ To correctly compile the document, you will need to add the following packages t
152
152
 
153
153
  ## Note
154
154
 
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
+ * 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.
156
156
  You can also submit a pull request with the desired feature !
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
+ * 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/).
158
158
 
159
159
 
160
160
  ## References
@@ -5,27 +5,28 @@ tests/test_line_charts.py,sha256=cyiVuUyH1lZ51p_JVcP3xjLlEEbSm_iD5gj6F4p8DUo,158
5
5
  tests/test_markers.py,sha256=g3ioyRqRh__VyKGET2RpCiEmLnQAztsYF5drqTq3idU,11555
6
6
  tests/test_polar.py,sha256=xvVx-Af855-eIw5Kmq1ApHHCv9hGr2WF6HuaN4XMZms,17244
7
7
  tests/test_scatter3d.py,sha256=yfieEogsVS2Wa5DgPJzIvsW2jz8yaPhkVU_nDrBWVTg,4359
8
- tikzplotly/__about__.py,sha256=W0m95bZPSmPKyrbh5zZGO071YHBbLsK0LQxvES50b7c,204
9
- tikzplotly/__init__.py,sha256=PrbDMOviELxIm9nyXS6Qk56-YhuTb-bAn_0BuY2rHLE,552
8
+ tikzplotly/__about__.py,sha256=sSgIKdBwORCAvELWyq6Xw2FHBdwRMjSmC4bbXwVFQtI,204
9
+ tikzplotly/__init__.py,sha256=_0q4GULEJ5-HsYvckWpomrsdKck1Q7S92A4Ldxl3K9o,946
10
10
  tikzplotly/_annotations.py,sha256=r3MPz_Ocx3oQzsCuqKT3pdX1ijb3-LAmYHpLPLulASI,3108
11
11
  tikzplotly/_axis.py,sha256=t-v97WnUJMq3ulRi_1E9r0Sv3glodVtqmzIWUftwFlQ,8062
12
12
  tikzplotly/_bar.py,sha256=m4Hc4GR-bnEdrS3tU7VgZqCz0mdA9ENfBjGAvjavpA8,3263
13
+ tikzplotly/_browser.py,sha256=GUYAXAhRGr693ouSYC6aAbodrTUhcON2W3vP9aWdwS0,15247
13
14
  tikzplotly/_color.py,sha256=hnBMZsVBs1M6YV1_hqquZYuLCO-fBEUaxtwD6cd-fMQ,41213
14
15
  tikzplotly/_dash.py,sha256=8hOaqzMDRcikD2HNWc18fzJm2Lvoq3eo3Tu8n0gAqvE,228
15
16
  tikzplotly/_data.py,sha256=t3TG0jvc_cjR84Ug04OUDyfF0hKFD_xKOgbgb1Xz-8M,1847
16
- tikzplotly/_data_container.py,sha256=G2mE8tgRfZV4pzNGWa23JCo4e63j_Rpm0wszXlunvAU,7040
17
+ tikzplotly/_data_container.py,sha256=VMOq6-UmjMDF_Aox_33HxdSrFFZKA_uHG55IoneprDY,8837
17
18
  tikzplotly/_heatmap.py,sha256=tUxs4gEaqi0TyY5TIInTJa_APUQPTnV0RXuDkWQ0N7w,5095
18
19
  tikzplotly/_histogram.py,sha256=PH0c1ZFU7VnJoWD6NIsF4VJAWReWZhGwNm1iZs7OXu0,5112
19
20
  tikzplotly/_marker.py,sha256=L5E_-fb4Fkv231dBmA72byAZeYPeVuKOFZIYTyLbMrs,11839
20
21
  tikzplotly/_polar.py,sha256=qhza1x53wgCjAafGIt1ncdRfKFLIlJJE5cvoR80TrIs,7792
21
- tikzplotly/_save.py,sha256=YuR0gqVtK6EhCyYoMxbVEHvzCMofEILV-42Ulp8iHU4,10219
22
+ tikzplotly/_save.py,sha256=uqV0lCqf68_OyyfvzMBukf-JaPGG4UkFkFGVMjxnmLE,10343
22
23
  tikzplotly/_scatter.py,sha256=Eu9RhAUUZHH4q7l6NWayzVsJh33vPkCvWbCnR4vwUME,3538
23
24
  tikzplotly/_scatter3d.py,sha256=Fd3Rx0d8o-CefPSU8CyGD_BNJ7wB8yr_QbaKuWEG1qo,2389
24
25
  tikzplotly/_tex.py,sha256=LlsMRncCzmGJGO9OtgKyDMEnVA7WKvLptqh1yM5LdmQ,6555
25
26
  tikzplotly/_trace_utils.py,sha256=NyQYkxtrRLHuA9pjfNqUihW3rYvdwvDo8Kj9IEaJo5I,2316
26
27
  tikzplotly/_utils.py,sha256=vGvqlwKzv_R6euq1kGp5nmET9ACxy17P72Fv5cBx1Dc,6241
27
- tikzplotly-0.2.2.dist-info/licenses/LICENSE,sha256=cjxM8VJ_qnjUVKoOndGip4HgyyElZZI8Hk8g3rXEFf8,1080
28
- tikzplotly-0.2.2.dist-info/METADATA,sha256=YX9jbyjsA7-d1rGp5_Kil0v4O7-r9X7syJqgvINqQ0M,6643
29
- tikzplotly-0.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
30
- tikzplotly-0.2.2.dist-info/top_level.txt,sha256=NSVJ1u8Bv6DVRb_y_Roui9D7tVh2XYDaGVjSbZyTrH4,17
31
- tikzplotly-0.2.2.dist-info/RECORD,,
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 (82.0.1)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5