tikzplot42 0.5.1__tar.gz → 0.5.4__tar.gz

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.
Files changed (53) hide show
  1. {tikzplot42-0.5.1/src/tikzplot42.egg-info → tikzplot42-0.5.4}/PKG-INFO +25 -2
  2. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/README.md +24 -1
  3. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/pyproject.toml +1 -1
  4. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/axes.py +255 -42
  5. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/axes.pyi +150 -40
  6. tikzplot42-0.5.4/src/tikzplot/axes3d.py +1273 -0
  7. tikzplot42-0.5.4/src/tikzplot/axes3d.pyi +582 -0
  8. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/colorbar.py +1 -1
  9. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/colors.py +64 -6
  10. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/config.py +2 -2
  11. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/elements.py +217 -62
  12. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/figure.py +18 -5
  13. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/figure.pyi +34 -5
  14. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/plots.py +19 -4
  15. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/plots.pyi +102 -8
  16. {tikzplot42-0.5.1 → tikzplot42-0.5.4/src/tikzplot42.egg-info}/PKG-INFO +25 -2
  17. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot42.egg-info/SOURCES.txt +3 -0
  18. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test11.py +1 -1
  19. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test14.py +1 -1
  20. tikzplot42-0.5.4/tests/test16.py +24 -0
  21. tikzplot42-0.5.4/tests/test17.py +40 -0
  22. tikzplot42-0.5.4/tests/test18.py +24 -0
  23. tikzplot42-0.5.1/src/tikzplot/axes3d.py +0 -591
  24. tikzplot42-0.5.1/src/tikzplot/axes3d.pyi +0 -43
  25. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/LICENSE +0 -0
  26. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/setup.cfg +0 -0
  27. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/__init__.py +0 -0
  28. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/__init__.pyi +0 -0
  29. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/colorbar.pyi +0 -0
  30. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/config.pyi +0 -0
  31. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/elements.pyi +0 -0
  32. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/latex_special.py +0 -0
  33. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/py.typed +0 -0
  34. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/state.py +0 -0
  35. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/styles.py +0 -0
  36. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/styles.pyi +0 -0
  37. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot/texts.py +0 -0
  38. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot42.egg-info/dependency_links.txt +0 -0
  39. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot42.egg-info/requires.txt +0 -0
  40. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/src/tikzplot42.egg-info/top_level.txt +0 -0
  41. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test1.py +0 -0
  42. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test10.py +0 -0
  43. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test12.py +0 -0
  44. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test13.py +0 -0
  45. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test15.py +0 -0
  46. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test2.py +0 -0
  47. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test3.py +0 -0
  48. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test4.py +0 -0
  49. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test5.py +0 -0
  50. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test6.py +0 -0
  51. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test7.py +0 -0
  52. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test8.py +0 -0
  53. {tikzplot42-0.5.1 → tikzplot42-0.5.4}/tests/test9.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tikzplot42
3
- Version: 0.5.1
3
+ Version: 0.5.4
4
4
  Summary: TikzPlot to make TikZ/PGFPlots plots with matplotlib.pyplot-like syntax.
5
5
  Author-email: Zan Ambrozic <zan.ambrozic7@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -706,6 +706,18 @@ Please report any bugs or unexpected behaviour in Issues. Examples may be found
706
706
  <img src="https://raw.githubusercontent.com/ZanAmb/TikzPlot/main/tests/demo.png" width="60%">
707
707
  </p>
708
708
 
709
+ ## Why use pgfplots instead of matplotlib?
710
+ - Tikz/pgfplots is a LaTeX package, so it is easy to integrate into your LaTeX document, and the fonts will match your document, rescaling will not affect font sizes.
711
+ - Tikz/pgfplots graph is easy to edit, especially if only minor style changes are needed, while matplotlib requires re-running the code to generate a new graph.
712
+ - Full math mode is supported, so you can use LaTeX math in your labels, legends, etc.
713
+
714
+ ## Why use this package instead of other available?
715
+ - It is easy to use, with code being very similar to matplotlib and only minor changes are needed to switch from matplotlib to this package.
716
+ - It relies on frontend commands, which is less likley to change in matplotlib, so new updates do not break it.
717
+ - It generates clean and readable Tikz code, which is easy to edit and understand. Code is also efficient and does not contain unnecessary commands, which is important escpecially for large graphs.
718
+ - If you use LLMs to generate pgfplots code, you will often get code that is not compilable, since for more complex requirements, LLMs make up commands that do not exist. This package covers many use cases and generates compilable code.
719
+
720
+
709
721
  # Installation
710
722
  PyPI: `pip install tikzplot42`.
711
723
  Alternativley, download this package and install using: `pip install [path]`, where [path] is the path to the directory, containing `pyproject.toml`.
@@ -741,6 +753,7 @@ Some basic plot commands are already implemented with commonly used arguments:
741
753
  - `hist()`,
742
754
  - `bar()/barh()`,
743
755
  - `bar_label()`,
756
+ - `stackplot()`,
744
757
  - `step()`,
745
758
  - `imshow()`,
746
759
  - `text()`,
@@ -748,6 +761,16 @@ Some basic plot commands are already implemented with commonly used arguments:
748
761
  - `axvline()/axhline()`,
749
762
  - `axvspan()/axhspan()` (background span).
750
763
 
764
+ #### 3D plotting
765
+ - `plot()`,
766
+ - `scatter()`,
767
+ - `errorbar()`,
768
+ - `fill_between()`,
769
+ - `plot_wireframe()`,
770
+ - `plot_surface()`,
771
+ - `bar3d()`,
772
+ - `text()`.
773
+
751
774
  #### Figures
752
775
  - `plt.figure()` (currently only to give you figure object or to set `figsize`),
753
776
  - `plt.subplot()`, `plt.subplots()` (with `sharex/sharey` fully implemented, projection `polar` and `3d` with basic support),
@@ -793,5 +816,5 @@ Currently supported parameters are listed with their default values in `src/tikz
793
816
 
794
817
  ## More features coming soon...
795
818
  - additional arguments support for implemented commands,
796
- - improved 3D plots (currently only basic support: plot, errorbar, fill_between, scatter, plot_wireframe, plot_surface),
819
+ - improved 3D and polar plots,
797
820
  - ...
@@ -7,6 +7,18 @@ Please report any bugs or unexpected behaviour in Issues. Examples may be found
7
7
  <img src="https://raw.githubusercontent.com/ZanAmb/TikzPlot/main/tests/demo.png" width="60%">
8
8
  </p>
9
9
 
10
+ ## Why use pgfplots instead of matplotlib?
11
+ - Tikz/pgfplots is a LaTeX package, so it is easy to integrate into your LaTeX document, and the fonts will match your document, rescaling will not affect font sizes.
12
+ - Tikz/pgfplots graph is easy to edit, especially if only minor style changes are needed, while matplotlib requires re-running the code to generate a new graph.
13
+ - Full math mode is supported, so you can use LaTeX math in your labels, legends, etc.
14
+
15
+ ## Why use this package instead of other available?
16
+ - It is easy to use, with code being very similar to matplotlib and only minor changes are needed to switch from matplotlib to this package.
17
+ - It relies on frontend commands, which is less likley to change in matplotlib, so new updates do not break it.
18
+ - It generates clean and readable Tikz code, which is easy to edit and understand. Code is also efficient and does not contain unnecessary commands, which is important escpecially for large graphs.
19
+ - If you use LLMs to generate pgfplots code, you will often get code that is not compilable, since for more complex requirements, LLMs make up commands that do not exist. This package covers many use cases and generates compilable code.
20
+
21
+
10
22
  # Installation
11
23
  PyPI: `pip install tikzplot42`.
12
24
  Alternativley, download this package and install using: `pip install [path]`, where [path] is the path to the directory, containing `pyproject.toml`.
@@ -42,6 +54,7 @@ Some basic plot commands are already implemented with commonly used arguments:
42
54
  - `hist()`,
43
55
  - `bar()/barh()`,
44
56
  - `bar_label()`,
57
+ - `stackplot()`,
45
58
  - `step()`,
46
59
  - `imshow()`,
47
60
  - `text()`,
@@ -49,6 +62,16 @@ Some basic plot commands are already implemented with commonly used arguments:
49
62
  - `axvline()/axhline()`,
50
63
  - `axvspan()/axhspan()` (background span).
51
64
 
65
+ #### 3D plotting
66
+ - `plot()`,
67
+ - `scatter()`,
68
+ - `errorbar()`,
69
+ - `fill_between()`,
70
+ - `plot_wireframe()`,
71
+ - `plot_surface()`,
72
+ - `bar3d()`,
73
+ - `text()`.
74
+
52
75
  #### Figures
53
76
  - `plt.figure()` (currently only to give you figure object or to set `figsize`),
54
77
  - `plt.subplot()`, `plt.subplots()` (with `sharex/sharey` fully implemented, projection `polar` and `3d` with basic support),
@@ -94,5 +117,5 @@ Currently supported parameters are listed with their default values in `src/tikz
94
117
 
95
118
  ## More features coming soon...
96
119
  - additional arguments support for implemented commands,
97
- - improved 3D plots (currently only basic support: plot, errorbar, fill_between, scatter, plot_wireframe, plot_surface),
120
+ - improved 3D and polar plots,
98
121
  - ...
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tikzplot42"
7
- version = "0.5.1"
7
+ version = "0.5.4"
8
8
  description = "TikzPlot to make TikZ/PGFPlots plots with matplotlib.pyplot-like syntax."
9
9
  authors = [
10
10
  { name = "Zan Ambrozic", email = "zan.ambrozic7@gmail.com" }
@@ -1,4 +1,3 @@
1
- from multiprocessing import parent_process
2
1
  from typing import Any, Iterable
3
2
  import copy
4
3
 
@@ -21,7 +20,7 @@ class BaseAxes:
21
20
  self._legend_on = False
22
21
  self._overlay_legend = False
23
22
  self._overlay_legend_entries = []
24
- self._overlay_special = {}
23
+ self._overlay_special: dict[int, dict[str, Any]] = {}
25
24
  self._yticks = True
26
25
  self._fig = None
27
26
  if TikzConfig.USE_DECIMAL_COMMA:
@@ -145,10 +144,17 @@ class BaseAxes:
145
144
  self._axis_options[key].update(value)
146
145
  self._axis_options[key] = value
147
146
 
147
+ def _parse_entry(self, k, v):
148
+ if v is None:
149
+ return f"{k}"
150
+ if isinstance(v, dict):
151
+ return f"{k}={{" + ",\n".join(f"{kk}={vv}" for kk, vv in v.items() if vv != {}) + "}"
152
+ return f"{k}={v}"
153
+
148
154
  def _plot(self, x, y, settings={}, xerr=None, yerr=None, overlay=None, note=None, **style):
149
155
  spec = None
150
156
  if self._get_overlay() in self._overlay_special:
151
- spec = self._overlay_special[self._get_overlay()]
157
+ spec = ",\n".join([self._parse_entry(k, v) for k, v in self._overlay_special[self._get_overlay()].items()])
152
158
  if note != spec:
153
159
  self._get_free_overlay()
154
160
 
@@ -256,7 +262,7 @@ class BaseAxes:
256
262
  return self._plot(y,x,settings={"xcomb": None}, **kwargs)
257
263
 
258
264
  def fill_between(self, x, y1, y2=None, **kwargs):
259
- kws = {"fmt", "alpha", "color", "c", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
265
+ kws = {"fmt", "alpha", "color", "c", "facecolor", "fc", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
260
266
  kwargs = self._check_kwargs("fill_between", kws, **kwargs)
261
267
  def _check_instance(xs, ys, pname):
262
268
  for el in self._elements[self._get_overlay()]:
@@ -277,7 +283,7 @@ class BaseAxes:
277
283
  name1 = inst
278
284
 
279
285
  if y2 is not None:
280
- if isinstance(y1, (int, float)):
286
+ if isinstance(y2, (int, float)):
281
287
  y2 = _np.asarray([y2] * len(x))
282
288
  inst = _check_instance(x,y2,name2)
283
289
  if inst is None:
@@ -292,6 +298,59 @@ class BaseAxes:
292
298
  self._plot(xs,ys,path_name=name2, alpha=0)
293
299
  else:
294
300
  name2 = inst
301
+ if "facecolor" in kwargs or "fc" in kwargs:
302
+ kwargs["color"] = kwargs.pop("facecolor", kwargs.pop("fc", None))
303
+ kwargs.pop("fc", None)
304
+ if not("color" in kwargs or "c" in kwargs):
305
+ assert isinstance(self, Axes) or isinstance(self, Secondary)
306
+ kwargs["color"] = f"C{self._get_defcol(1)}"
307
+ e = Graph(self, f"fill between [of={name1} and {name2}]",settings={}, xerr=None, yerr=None, **kwargs)
308
+ self._elements[self._get_overlay()].append(e)
309
+ return e
310
+
311
+ def fill_betweenx(self, y, x1, x2=None, **kwargs):
312
+ kws = {"fmt", "alpha", "color", "c", "facecolor", "fc", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
313
+ kwargs = self._check_kwargs("fill_between", kws, **kwargs)
314
+ def _check_instance(xs, ys, pname):
315
+ for el in self._elements[self._get_overlay()]:
316
+ if el._check_equal(xs,ys):
317
+ return el._try_set_pname(pname)
318
+ else:
319
+ return None
320
+ assert self._fig is not None
321
+ self._fig._add_required_package("\\usepgfplotslibrary{fillbetween}")
322
+ name1 = self._fig._get_free_path_name()
323
+ name2 = self._fig._get_free_path_name()
324
+ if isinstance(x1, (int, float)):
325
+ x1 = _np.asarray([x1] * len(y))
326
+ inst = _check_instance(x1,y,name1)
327
+ if inst is None:
328
+ self._plot(x1,y,path_name=name1, alpha=0)
329
+ else:
330
+ name1 = inst
331
+
332
+ if x2 is not None:
333
+ if isinstance(x2, (int, float)):
334
+ x2 = _np.asarray([x2] * len(y))
335
+ inst = _check_instance(x2,y,name2)
336
+ if inst is None:
337
+ self._plot(x2,y,path_name=name2, alpha=0)
338
+ else:
339
+ name2 = inst
340
+ else:
341
+ xs = [0, 0]
342
+ ys = [min(y), max(y)]
343
+ inst = _check_instance(xs,ys,name2)
344
+ if inst is None:
345
+ self._plot(xs,ys,path_name=name2, alpha=0)
346
+ else:
347
+ name2 = inst
348
+ if "facecolor" in kwargs or "fc" in kwargs:
349
+ kwargs["color"] = kwargs.pop("facecolor", kwargs.pop("fc", None))
350
+ kwargs.pop("fc", None)
351
+ if not("color" in kwargs or "c" in kwargs):
352
+ assert isinstance(self, Axes) or isinstance(self, Secondary)
353
+ kwargs["color"] = f"C{self._get_defcol(1)}"
295
354
  e = Graph(self, f"fill between [of={name1} and {name2}]",settings={}, xerr=None, yerr=None, **kwargs)
296
355
  self._elements[self._get_overlay()].append(e)
297
356
  return e
@@ -351,7 +410,7 @@ class BaseAxes:
351
410
  except (ValueError, TypeError):
352
411
  x_arr = _np.asarray(x)
353
412
  if x_arr.ndim == 2:
354
- datasets = [x_arr[:, i] for i in range(x_arr.shape[1])]
413
+ datasets = [col for col in x_arr.T]
355
414
  else:
356
415
  raise ValueError("Invalid dataset structure.")
357
416
  else:
@@ -359,7 +418,7 @@ class BaseAxes:
359
418
  if x_arr.ndim == 1:
360
419
  datasets = [x_arr]
361
420
  elif x_arr.ndim == 2:
362
- datasets = [x_arr[:, i] for i in range(x_arr.shape[1])]
421
+ datasets = [col for col in x_arr.T]
363
422
  else:
364
423
  raise ValueError(f"Input must be 1D or 2D, got {x_arr.ndim}D.")
365
424
 
@@ -409,13 +468,6 @@ class BaseAxes:
409
468
  offset = widths.mean() / (len(datasets) + 1)
410
469
  else:
411
470
  intervals = True
412
- if "range" in kwargs:
413
- if orientation == "horizontal":
414
- self.set_ylim(kwargs["range"])
415
- elif isinstance(self, Axes):
416
- self.set_xlim(kwargs["range"])
417
- elif isinstance(self, Secondary):
418
- self._primary.set_xlim(kwargs["range"])
419
471
  base_settings = settings.copy()
420
472
  outputs = []
421
473
  totals, _ = _np.histogram(all_data, edges, density=False, weights=kwargs.get("weights", None), range=kwargs.get("range", None))
@@ -489,24 +541,112 @@ class BaseAxes:
489
541
  outputs.append(e)
490
542
  return outputs
491
543
 
492
- def bar(self, x, height, *args, **kwargs):
493
- kws = {"alpha", "width", "bottom", "align", "color", "c", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "tick_label", "label", "xerr", "yerr", "ecolor", "capsize", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance", "tick_label"}
544
+ def bar(self, x, height, width=0.8, bottom=None, **kwargs):
545
+ kws = {"alpha", "align", "color", "c", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "tick_label", "label", "xerr", "yerr", "ecolor", "capsize", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance", "tick_label"}
494
546
  kwargs = self._check_kwargs("bar", kws, **kwargs)
547
+ kwargs["width"] = width
548
+ if bottom is not None:
549
+ kwargs["bottom"] = bottom
495
550
  if "width" in kwargs:
496
551
  kwargs["thickness"] = kwargs.pop("width")
497
552
  kwargs["edge"] = kwargs.pop("bottom", 0)
498
553
 
499
554
  return self._common_bar(x, height, orientation="vertical", **kwargs)
500
555
 
501
- def barh(self, y, width, *args, **kwargs):
502
- kws = {"alpha", "height", "left", "align", "color", "c", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "tick_label", "label", "xerr", "yerr", "ecolor", "capsize", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
556
+ def barh(self, y, width, height=0.8, left=None, **kwargs):
557
+ kws = {"alpha", "align", "color", "c", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "tick_label", "label", "xerr", "yerr", "ecolor", "capsize", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
503
558
  kwargs = self._check_kwargs("barh", kws, **kwargs)
559
+ kwargs["height"] = height
560
+ if left is not None:
561
+ kwargs["left"] = left
504
562
  if "height" in kwargs:
505
563
  kwargs["thickness"] = kwargs.pop("height")
506
564
  kwargs["edge"] = kwargs.pop("left", 0)
507
565
 
508
566
  return self._common_bar(y, width, orientation="horizontal", **kwargs)
509
567
 
568
+ def grouped_bar(self, heights, **kwargs):
569
+ kws = {"positions", "tick_labels", "labels", "group_spacing", "bar_spacing", "orientation", "colors", "alpha", "edgecolor", "ec", "facecolor", "fc", "linewidth", "lw", "linestyle", "ls", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
570
+ kwargs = self._check_kwargs("grouped_bar", kws, **kwargs)
571
+ if isinstance(heights, dict):
572
+ if "labels" not in kwargs:
573
+ kwargs["labels"] = list(heights.keys())
574
+ heights = list(heights.values())
575
+ if isinstance(heights, (list, tuple)) and len(heights) > 0 and isinstance(heights[0], (list, tuple, _np.ndarray)):
576
+ try:
577
+ datasets = [_np.asarray(ds, dtype=_np.float64) for ds in heights]
578
+ if any(ds.ndim != 1 for ds in datasets):
579
+ raise ValueError("Nested datasets must all be 1-dimensional.")
580
+ except (ValueError, TypeError):
581
+ h_arr = _np.asarray(heights)
582
+ if h_arr.ndim == 2:
583
+ datasets = [col for col in h_arr.T]
584
+ else:
585
+ raise ValueError("Invalid dataset structure.")
586
+ else:
587
+ h_arr = _np.asarray(heights)
588
+ if h_arr.ndim == 1:
589
+ datasets = [h_arr]
590
+ elif h_arr.ndim == 2:
591
+ datasets = [col for col in h_arr.T]
592
+ else:
593
+ raise ValueError(f"Input must be 1D or 2D, got {h_arr.ndim}D.")
594
+
595
+ xs = kwargs.pop("positions", range(len(datasets)))
596
+ if len(xs) != len(datasets):
597
+ raise Warning("Length of positions does not match length of datasets.")
598
+ if _np.diff(xs).min() != _np.diff(xs).max():
599
+ raise Warning("Positions must be equidistant.")
600
+ dist = _np.diff(xs).min()
601
+ settings = {}
602
+ if "rwidth" not in kwargs:
603
+ kwargs["rwidth"] = 1
604
+ orientation = kwargs.pop("orientation", "vertical")
605
+ group_spacing = kwargs.pop("group_spacing", 1.5)
606
+ bar_spacing = kwargs.pop("bar_spacing", 0.0)
607
+ bar_widths_num = len(datasets) * (1 + bar_spacing) + group_spacing
608
+ bar_width = dist / bar_widths_num
609
+ settings["thickness"] = bar_width
610
+
611
+ if "tick_labels" in kwargs:
612
+ tick_labels = kwargs.pop("tick_labels")
613
+ if len(tick_labels) != len(xs):
614
+ raise Warning("Length of tick_labels does not match length of positions.")
615
+ if orientation == "vertical":
616
+ if isinstance(self, Axes):
617
+ self.set_xticks(xs, tick_labels)
618
+ elif isinstance(self, Secondary):
619
+ self._primary.set_xticks(xs, tick_labels)
620
+ else:
621
+ self.set_yticks(xs, tick_labels)
622
+
623
+ datas = {}
624
+ for kw in ["colors", "alpha", "facecolor", "fc", "edgecolor", "ec", "labels", "linestyle", "ls", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]:
625
+ if kw in kwargs:
626
+ if isinstance(kwargs[kw], (list)):
627
+ if len(kwargs[kw]) != len(datasets):
628
+ raise Warning(f"Length of {kw} does not match number of datasets.")
629
+ prop = kwargs.pop(kw)
630
+ else:
631
+ prop = [kwargs.pop(kw)] * len(datasets)
632
+ for i in range(len(datasets)):
633
+ if i not in datas:
634
+ datas[i] = {}
635
+ datas[i][kw.removesuffix("s")] = prop[i]
636
+ base_settings = settings.copy()
637
+ bars = []
638
+ for i in range(len(datasets)):
639
+ offset = (i - len(datasets)/2 + 0.5) * bar_width * (1 + bar_spacing)
640
+ data = datasets[i]
641
+ settings = base_settings.copy()
642
+ kws = datas.get(i, {})
643
+ passing_args = ["alpha", "facecolor", "fc", "color", "c", "edgecolor", "ec", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]
644
+ for a in passing_args:
645
+ if a in kws:
646
+ settings[a] = kws[a]
647
+ bars.append(self._common_bar(xs, data, orientation=orientation, **settings, group_offset=offset))
648
+ return bars
649
+
510
650
  def _common_bar(self, k, v, orientation="vertical", **kwargs):
511
651
  #if orientation == "vertical":
512
652
  # self._int_ymax = self._int_ymin = 0
@@ -593,7 +733,7 @@ class BaseAxes:
593
733
 
594
734
  if _np.count_nonzero(edge) > 0:
595
735
  lower = self._get_overlay()
596
- if self._overlay_special.get(lower, None) == bar_type + " stacked":
736
+ if bar_type + " stacked" in self._overlay_special.get(lower, {}):
597
737
  bedx = _np.zeros_like(edge, dtype=_np.float64)
598
738
  bedy = _np.zeros_like(edge, dtype=_np.float64)
599
739
  for el in self._elements[lower]:
@@ -612,16 +752,18 @@ class BaseAxes:
612
752
  if overlay is None:
613
753
  if len(self._elements[self._get_overlay()]) > 0 and self._elements[self._get_overlay()][-1]._check_equal(k, edge):
614
754
  if len(self._elements[lower]) == 1:
615
- self._overlay_special[lower] = bar_type + " stacked"
755
+ if lower not in self._overlay_special:
756
+ self._overlay_special[lower] = {}
757
+ self._overlay_special[lower].update({bar_type + " stacked": None})
616
758
  overlay = lower
617
759
  else:
618
760
  current = self._get_free_overlay()
619
761
  self._elements[current].append(self._elements[lower].pop()) # move the stacking bed to new overlay
620
- self._overlay_special[current] = bar_type + " stacked"
762
+ self._overlay_special[current] = {bar_type + " stacked": None}
621
763
  overlay = current
622
764
  else: # no bed, plot invisible bed
623
765
  current = self._get_free_overlay()
624
- self._overlay_special[current] = bar_type + " stacked"
766
+ self._overlay_special[current] = {bar_type + " stacked": None}
625
767
  self._plot(k, edge, alpha=0)
626
768
  overlay = current
627
769
 
@@ -638,7 +780,7 @@ class BaseAxes:
638
780
  for kw in ["label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]:
639
781
  if kw in kwargs:
640
782
  element_args[kw] = kwargs.pop(kw)
641
- note = self._overlay_special.get(overlay, None)
783
+ note = ",\n".join([self._parse_entry(k, v) for k, v in self._overlay_special.get(overlay, {}).items()]) if overlay in self._overlay_special else None
642
784
  if "label" in element_args:
643
785
  if orientation == "vertical":
644
786
  settings["ybar legend"] = None
@@ -648,7 +790,7 @@ class BaseAxes:
648
790
  return self._plot(k, v, yerr=kwargs.get("yerr", None), xerr=kwargs.get("xerr", None), overlay=overlay, settings=settings, note=note, **element_args)
649
791
 
650
792
  def bar_label(self, container, labels=None, **kwargs):
651
- kws = {"color", "c", "fontsize", "rotation", "fmt", "padding"}
793
+ kws = {"alpha", "color", "c", "fontsize", "rotation", "fmt", "padding"}
652
794
  kwargs = self._check_kwargs("bar_label", kws, **kwargs)
653
795
  if not isinstance(container, Graph):
654
796
  raise Warning("Container must be a Graph object.")
@@ -659,6 +801,8 @@ class BaseAxes:
659
801
  if "color" in kwargs or "c" in kwargs:
660
802
  c = kwargs.pop("color", kwargs.pop("c", None))
661
803
  kwargs["color_parsed"] = self._match_color(c)
804
+ if "alpha" in kwargs:
805
+ kwargs["opacity"] = kwargs.pop("alpha")
662
806
  if "fontsize" in kwargs:
663
807
  kwargs["fontsize_parsed"] = self._tex_fontsize(kwargs["fontsize"])
664
808
  o = self._get_element_overlay(container)
@@ -683,7 +827,68 @@ class BaseAxes:
683
827
  if labels is not None and labels != []:
684
828
  container._add_meta_column(labels)
685
829
  self._bar_labels[self._get_element_overlay(container)][container] = kwargs | {"bar_labels": labels}
686
-
830
+
831
+ def stackplot(self, x, *args, **kwargs):
832
+ kws = {"baseline", "labels", "colors", "alpha", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "linestyle", "ls", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
833
+ kwargs = self._check_kwargs("stackplot", kws, **kwargs)
834
+ n = len(x)
835
+ x = _np.asarray(x, dtype=_np.float64)
836
+ if len(args) == 1:
837
+ y = list(args[0])
838
+ else:
839
+ y = list(args)
840
+ if isinstance(list(y)[0], int | float):
841
+ y = [list(y)]
842
+ y = _np.asarray(y, dtype=_np.float64)
843
+ base_set = kwargs.pop("baseline", "zero")
844
+ if base_set not in ["zero", "sym", "wiggle", "weighted_wiggle"]:
845
+ raise Warning(f"Invalid baseline: {base_set}. Must be one of 'zero', 'sym', 'wiggle', or 'weighted_wiggle'.")
846
+ if base_set == "zero":
847
+ baseline = _np.zeros(n, dtype=_np.float64)
848
+ elif base_set == "sym":
849
+ baseline = -_np.sum(y, axis=0)/2
850
+ elif base_set == "wiggle": # from matplotlib source
851
+ m = y.shape[0]
852
+ baseline = (y * (m - 0.5 - _np.arange(m)[:, None])).sum(0)
853
+ baseline /= -m
854
+ else: # weighted_wiggle; from matplotlib source
855
+ total = _np.sum(y, 0)
856
+ inv_total = _np.zeros_like(total)
857
+ mask = total > 0
858
+ inv_total[mask] = 1.0 / total[mask]
859
+ increase = _np.hstack((y[:, 0:1], _np.diff(y)))
860
+ below_size = total - _np.cumsum(y, axis=0, dtype=_np.promote_types(y.dtype, _np.float32))
861
+ below_size += 0.5 * y
862
+ move_up = below_size * inv_total
863
+ move_up[:, 0] = 0.5
864
+ center = (move_up - 0.5) * increase
865
+ center = _np.cumsum(center.sum(0))
866
+ baseline = center - 0.5 * total
867
+ overlay = self._get_free_overlay()
868
+ self._overlay_special[overlay] = {"stack plots": "y", "area style": None}
869
+ if base_set != "zero":
870
+ self._plot(x, baseline, overlay=overlay, alpha=0)
871
+ datas = {}
872
+ for kw in ["labels", "colors", "alpha", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "linestyle", "ls", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]:
873
+ if kw in kwargs:
874
+ prop = kwargs.pop(kw)
875
+ try:
876
+ prop = list(prop)
877
+ except: pass
878
+ if isinstance(prop, (list, tuple)):
879
+ if len(prop) != len(y):
880
+ raise Warning(f"Length of {kw} does not match number of datasets.")
881
+ else:
882
+ prop = [prop] * len(y)
883
+ datas[kw.removesuffix("s")] = prop
884
+ if "label" in datas:
885
+ self._overlay_special[overlay]["area legend"] = None
886
+ output = []
887
+ for i in range(len(y)):
888
+ kws = {kw: datas[kw][i] for kw in datas}
889
+ kws["_endnotes"] = r"\closedcycle"
890
+ output.append(self._plot(x, y[i], overlay=overlay, settings={"fill": None}, **kws))
891
+ return output
687
892
 
688
893
  def step(self, x, y, *args, **kwargs):
689
894
  kws = {"fmt", "alpha", "color", "c", "linestyle", "ls", "linewidth", "lw", "marker", "markersize", "ms", "label", "where"}
@@ -876,8 +1081,6 @@ class BaseAxes:
876
1081
  Y_INV = {v: k for k, v in Y_POS_MAP.items()}
877
1082
  self._axis_options["xtick pos"] = X_INV[(xt_t, xt_b)]
878
1083
  self._axis_options["ytick pos"] = Y_INV[(yt_l, yt_r)]
879
- if self._axis_options["xtick pos"] == "both":
880
- self._axis_options.pop("xtick pos")
881
1084
  if "colors" in kwargs:
882
1085
  c = self._match_color(kwargs.pop("colors"))
883
1086
  self._update_axis_options(prefix + " tick style", {"draw": c})
@@ -897,6 +1100,14 @@ class BaseAxes:
897
1100
  raise Warning(f"Invalid direction: {direction}. Must be one of 'in', 'out', or 'inout'.")
898
1101
  TICK_DIR_MAP = {"in": "inside", "out": "outside", "inout": "center"}
899
1102
  self._update_axis_options(prefix + "tick align", TICK_DIR_MAP[direction])
1103
+ if self._axis_options["xtick pos"] == "both":
1104
+ self._axis_options.pop("xtick pos")
1105
+ elif self._axis_options["xtick pos"] == "none":
1106
+ self._update_axis_options("xtick style", {"draw": self._axis_options.pop("xtick pos")})
1107
+ if self._axis_options["ytick pos"] == "both":
1108
+ self._axis_options.pop("ytick pos")
1109
+ elif self._axis_options["ytick pos"] == "none":
1110
+ self._update_axis_options("ytick style", {"draw": self._axis_options.pop("ytick pos")})
900
1111
 
901
1112
  _LEGEND_LOC_MAP = ["best", "upper right", "upper left", "lower_left", "lower right", "right", "center left", "center right", "lower center", "upper center", "center"]
902
1113
  _ANCHOR_MAP = {"top": "north", "bottom": "south", "upper": "north", "lower": "south", "left": "west", "right": "east", "center": "center"}
@@ -1140,7 +1351,7 @@ class Axes(BaseAxes):
1140
1351
  self._style = self._fig._style
1141
1352
  self._imshow = None
1142
1353
 
1143
- self._defcol_counter = 0
1354
+ self._defcol_counter = {0: 0}
1144
1355
  self._colorbar = ""
1145
1356
  self._cbar_h = False
1146
1357
  self._polar = polar
@@ -1537,9 +1748,11 @@ class Axes(BaseAxes):
1537
1748
  return self._nrows
1538
1749
  def _get_ncols(self):
1539
1750
  return self._ncols
1540
- def _get_defcol(self):
1541
- self._defcol_counter += 1
1542
- return self._defcol_counter - 1
1751
+ def _get_defcol(self, index = 0):
1752
+ if index not in self._defcol_counter:
1753
+ self._defcol_counter[index] = 0
1754
+ self._defcol_counter[index] += 1
1755
+ return self._defcol_counter[index] - 1
1543
1756
  def _show_colorbar(self, cbar, horizontal=False):
1544
1757
  self._colorbar = ",\n" + cbar
1545
1758
  self._cbar_h = horizontal
@@ -1550,7 +1763,7 @@ class Axes(BaseAxes):
1550
1763
  for k,v in self._elements.items():
1551
1764
  if k in self._bar_labels and len(self._bar_labels[k]) > 0:
1552
1765
  if k in self._overlay_special and ("xbar stacked" in self._overlay_special[k] or "ybar stacked" in self._overlay_special[k]):
1553
- self._overlay_special[k] += ",\nnodes near coords"
1766
+ self._overlay_special[k].update({"nodes near coords": None})
1554
1767
  for e in self._elements[k].copy():
1555
1768
  if e not in self._bar_labels[k]:
1556
1769
  self.bar_label(e, labels=[])
@@ -1561,13 +1774,13 @@ class Axes(BaseAxes):
1561
1774
  """if isinstance(v, list) and len(v) > 1:
1562
1775
  reference = getattr(v[0], "_settings", {}) or {}
1563
1776
  common_style = {s: val for s, val in reference.items() if all((getattr(obj, "_settings", {}) or {}).get(s) == val for obj in v[1:])}
1564
- forbiden = {"draw", "fill", "xbar", "ybar"}
1565
- for f in forbiden:
1777
+ forbidden = {"draw", "fill", "xbar", "ybar"}
1778
+ for f in forbidden:
1566
1779
  common_style.pop(f, None)
1567
1780
  for key, val in common_style.items():
1568
1781
  for e in v:
1569
1782
  e._settings.pop(key, None)
1570
- self._overlay_special[k] += f",\n{self._parse_entry(key, val)}" """
1783
+ self._overlay_special[k].update({key: val}) """
1571
1784
  lines = []
1572
1785
  lines2 = []
1573
1786
  if self._polar:
@@ -1577,7 +1790,7 @@ class Axes(BaseAxes):
1577
1790
  if self._polar and TikzConfig.USE_GROUPPLOTS and not single:
1578
1791
  lines.append(f"\\nextgroupplot[alias={self._axis_options['alias']}, width={self._width}, height={self._height}, hide axis]")
1579
1792
  for i in self._elements.keys():
1580
- spec = self._overlay_special[i] if i in self._overlay_special else ""
1793
+ spec = ",\n".join([self._parse_entry(k, v) for k, v in self._overlay_special.get(i, {}).items()]) + ",\n" if i in self._overlay_special else ""
1581
1794
  lines2.append("\\begin{polaraxis}[")
1582
1795
  if i == self._get_overlay():
1583
1796
  lines2.append(f"{main_ax}{spec}\n]")
@@ -1593,18 +1806,18 @@ class Axes(BaseAxes):
1593
1806
  elif not TikzConfig.USE_GROUPPLOTS or (TikzConfig.USE_GROUPPLOTS and single):
1594
1807
  lines.append("\\begin{axis}[")
1595
1808
  if self._get_overlay() == 0:
1596
- spec = self._overlay_special[0] + ",\n" if 0 in self._overlay_special else ""
1809
+ spec = ",\n".join([self._parse_entry(k, v) for k, v in self._overlay_special.get(0, {}).items()]) + ",\n" if 0 in self._overlay_special else ""
1597
1810
  if self._secondary_y is not None or self._colorbar is not None:
1598
1811
  lines.append(f"{main_ax}{spec}alias={alias}\n]")
1599
1812
  else:
1600
1813
  lines.append(f"{main_ax}{spec}\n]")
1601
1814
  lines.append(contents[0])
1602
1815
  else:
1603
- spec = self._overlay_special[0] + ",\n" if 0 in self._overlay_special else ""
1816
+ spec = ",\n".join([self._parse_entry(k, v) for k, v in self._overlay_special.get(0, {}).items()]) + ",\n" if 0 in self._overlay_special else ""
1604
1817
  lines.append(f"{aux_ax}{spec}alias={alias}\n]")
1605
1818
  lines.append(contents[0])
1606
1819
  for i in self._elements.keys():
1607
- spec = self._overlay_special[i] + ",\n" if i in self._overlay_special else ""
1820
+ spec = ",\n".join([self._parse_entry(k, v) for k, v in self._overlay_special.get(i, {}).items()]) + ",\n" if i in self._overlay_special else ""
1608
1821
  if i == 0: continue
1609
1822
  lines2.append("\\begin{axis}[")
1610
1823
  if i == self._get_overlay():
@@ -1627,7 +1840,7 @@ class Axes(BaseAxes):
1627
1840
  contents2 = self._secondary_y._content_tex(filename)
1628
1841
  for i in self._secondary_y._elements.keys():
1629
1842
  lines2.append("\\begin{axis}[")
1630
- spec = self._secondary_y._overlay_special[i] if i in self._secondary_y._overlay_special else ""
1843
+ spec = ",\n".join([self._parse_entry(k, v) for k, v in self._secondary_y._overlay_special.get(i, {}).items()]) + ",\n" if i in self._secondary_y._overlay_special else ""
1631
1844
  if i == sorted(self._secondary_y._elements.keys())[-1]:
1632
1845
  lines2.append(f"{main_ax2}{spec}\n]")
1633
1846
  else:
@@ -1715,8 +1928,8 @@ class Secondary(BaseAxes):
1715
1928
  def _padding(self):
1716
1929
  return TikzConfig.SEC_Y_PADDING + TikzConfig.YTICK_PADDING * self._yticks + TikzConfig.SEC_Y_LABEL_PADDING * ("ylabel" in self._axis_options)
1717
1930
 
1718
- def _get_defcol(self):
1719
- return self._primary._get_defcol()
1931
+ def _get_defcol(self, index = 0):
1932
+ return self._primary._get_defcol(index)
1720
1933
 
1721
1934
  def _get_index(self):
1722
1935
  return self._primary._get_index()