tikzplot42 0.5.1__tar.gz → 0.5.2__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 (49) hide show
  1. {tikzplot42-0.5.1/src/tikzplot42.egg-info → tikzplot42-0.5.2}/PKG-INFO +14 -1
  2. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/README.md +13 -0
  3. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/pyproject.toml +1 -1
  4. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/axes.py +248 -37
  5. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/axes.pyi +128 -21
  6. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/elements.py +73 -51
  7. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/figure.pyi +1 -1
  8. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/plots.py +14 -0
  9. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/plots.pyi +102 -8
  10. {tikzplot42-0.5.1 → tikzplot42-0.5.2/src/tikzplot42.egg-info}/PKG-INFO +14 -1
  11. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot42.egg-info/SOURCES.txt +1 -0
  12. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test14.py +1 -1
  13. tikzplot42-0.5.2/tests/test16.py +24 -0
  14. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/LICENSE +0 -0
  15. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/setup.cfg +0 -0
  16. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/__init__.py +0 -0
  17. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/__init__.pyi +0 -0
  18. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/axes3d.py +0 -0
  19. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/axes3d.pyi +0 -0
  20. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/colorbar.py +0 -0
  21. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/colorbar.pyi +0 -0
  22. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/colors.py +0 -0
  23. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/config.py +0 -0
  24. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/config.pyi +0 -0
  25. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/elements.pyi +0 -0
  26. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/figure.py +0 -0
  27. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/latex_special.py +0 -0
  28. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/py.typed +0 -0
  29. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/state.py +0 -0
  30. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/styles.py +0 -0
  31. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/styles.pyi +0 -0
  32. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot/texts.py +0 -0
  33. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot42.egg-info/dependency_links.txt +0 -0
  34. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot42.egg-info/requires.txt +0 -0
  35. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/src/tikzplot42.egg-info/top_level.txt +0 -0
  36. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test1.py +0 -0
  37. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test10.py +0 -0
  38. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test11.py +0 -0
  39. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test12.py +0 -0
  40. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test13.py +0 -0
  41. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test15.py +0 -0
  42. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test2.py +0 -0
  43. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test3.py +0 -0
  44. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test4.py +0 -0
  45. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test5.py +0 -0
  46. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test6.py +0 -0
  47. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test7.py +0 -0
  48. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/tests/test8.py +0 -0
  49. {tikzplot42-0.5.1 → tikzplot42-0.5.2}/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.2
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()`,
@@ -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()`,
@@ -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.2"
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" }
@@ -4,6 +4,7 @@ import copy
4
4
 
5
5
  import numpy as _np
6
6
  import matplotlib.pyplot as _plt
7
+ from scipy import datasets
7
8
 
8
9
  from .elements import Graph
9
10
  from .texts import Text
@@ -21,7 +22,7 @@ class BaseAxes:
21
22
  self._legend_on = False
22
23
  self._overlay_legend = False
23
24
  self._overlay_legend_entries = []
24
- self._overlay_special = {}
25
+ self._overlay_special: dict[int, dict[str, Any]] = {}
25
26
  self._yticks = True
26
27
  self._fig = None
27
28
  if TikzConfig.USE_DECIMAL_COMMA:
@@ -145,10 +146,17 @@ class BaseAxes:
145
146
  self._axis_options[key].update(value)
146
147
  self._axis_options[key] = value
147
148
 
149
+ def _parse_entry(self, k, v):
150
+ if v is None:
151
+ return f"{k}"
152
+ if isinstance(v, dict):
153
+ return f"{k}={{" + ",\n".join(f"{kk}={vv}" for kk, vv in v.items() if vv != {}) + "}"
154
+ return f"{k}={v}"
155
+
148
156
  def _plot(self, x, y, settings={}, xerr=None, yerr=None, overlay=None, note=None, **style):
149
157
  spec = None
150
158
  if self._get_overlay() in self._overlay_special:
151
- spec = self._overlay_special[self._get_overlay()]
159
+ spec = ",\n".join([self._parse_entry(k, v) for k, v in self._overlay_special[self._get_overlay()].items()])
152
160
  if note != spec:
153
161
  self._get_free_overlay()
154
162
 
@@ -256,7 +264,7 @@ class BaseAxes:
256
264
  return self._plot(y,x,settings={"xcomb": None}, **kwargs)
257
265
 
258
266
  def fill_between(self, x, y1, y2=None, **kwargs):
259
- kws = {"fmt", "alpha", "color", "c", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
267
+ kws = {"fmt", "alpha", "color", "c", "facecolor", "fc", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
260
268
  kwargs = self._check_kwargs("fill_between", kws, **kwargs)
261
269
  def _check_instance(xs, ys, pname):
262
270
  for el in self._elements[self._get_overlay()]:
@@ -277,7 +285,7 @@ class BaseAxes:
277
285
  name1 = inst
278
286
 
279
287
  if y2 is not None:
280
- if isinstance(y1, (int, float)):
288
+ if isinstance(y2, (int, float)):
281
289
  y2 = _np.asarray([y2] * len(x))
282
290
  inst = _check_instance(x,y2,name2)
283
291
  if inst is None:
@@ -292,6 +300,59 @@ class BaseAxes:
292
300
  self._plot(xs,ys,path_name=name2, alpha=0)
293
301
  else:
294
302
  name2 = inst
303
+ if "facecolor" in kwargs or "fc" in kwargs:
304
+ kwargs["color"] = kwargs.pop("facecolor", kwargs.pop("fc", None))
305
+ kwargs.pop("fc", None)
306
+ if not("color" in kwargs or "c" in kwargs):
307
+ assert isinstance(self, Axes) or isinstance(self, Secondary)
308
+ kwargs["color"] = f"C{self._get_defcol(1)}"
309
+ e = Graph(self, f"fill between [of={name1} and {name2}]",settings={}, xerr=None, yerr=None, **kwargs)
310
+ self._elements[self._get_overlay()].append(e)
311
+ return e
312
+
313
+ def fill_betweenx(self, y, x1, x2=None, **kwargs):
314
+ kws = {"fmt", "alpha", "color", "c", "facecolor", "fc", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
315
+ kwargs = self._check_kwargs("fill_between", kws, **kwargs)
316
+ def _check_instance(xs, ys, pname):
317
+ for el in self._elements[self._get_overlay()]:
318
+ if el._check_equal(xs,ys):
319
+ return el._try_set_pname(pname)
320
+ else:
321
+ return None
322
+ assert self._fig is not None
323
+ self._fig._add_required_package("\\usepgfplotslibrary{fillbetween}")
324
+ name1 = self._fig._get_free_path_name()
325
+ name2 = self._fig._get_free_path_name()
326
+ if isinstance(x1, (int, float)):
327
+ x1 = _np.asarray([x1] * len(y))
328
+ inst = _check_instance(x1,y,name1)
329
+ if inst is None:
330
+ self._plot(x1,y,path_name=name1, alpha=0)
331
+ else:
332
+ name1 = inst
333
+
334
+ if x2 is not None:
335
+ if isinstance(x2, (int, float)):
336
+ x2 = _np.asarray([x2] * len(y))
337
+ inst = _check_instance(x2,y,name2)
338
+ if inst is None:
339
+ self._plot(x2,y,path_name=name2, alpha=0)
340
+ else:
341
+ name2 = inst
342
+ else:
343
+ xs = [0, 0]
344
+ ys = [min(y), max(y)]
345
+ inst = _check_instance(xs,ys,name2)
346
+ if inst is None:
347
+ self._plot(xs,ys,path_name=name2, alpha=0)
348
+ else:
349
+ name2 = inst
350
+ if "facecolor" in kwargs or "fc" in kwargs:
351
+ kwargs["color"] = kwargs.pop("facecolor", kwargs.pop("fc", None))
352
+ kwargs.pop("fc", None)
353
+ if not("color" in kwargs or "c" in kwargs):
354
+ assert isinstance(self, Axes) or isinstance(self, Secondary)
355
+ kwargs["color"] = f"C{self._get_defcol(1)}"
295
356
  e = Graph(self, f"fill between [of={name1} and {name2}]",settings={}, xerr=None, yerr=None, **kwargs)
296
357
  self._elements[self._get_overlay()].append(e)
297
358
  return e
@@ -409,13 +470,6 @@ class BaseAxes:
409
470
  offset = widths.mean() / (len(datasets) + 1)
410
471
  else:
411
472
  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
473
  base_settings = settings.copy()
420
474
  outputs = []
421
475
  totals, _ = _np.histogram(all_data, edges, density=False, weights=kwargs.get("weights", None), range=kwargs.get("range", None))
@@ -489,24 +543,112 @@ class BaseAxes:
489
543
  outputs.append(e)
490
544
  return outputs
491
545
 
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"}
546
+ def bar(self, x, height, width=0.8, bottom=None, **kwargs):
547
+ 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
548
  kwargs = self._check_kwargs("bar", kws, **kwargs)
549
+ kwargs["width"] = width
550
+ if bottom is not None:
551
+ kwargs["bottom"] = bottom
495
552
  if "width" in kwargs:
496
553
  kwargs["thickness"] = kwargs.pop("width")
497
554
  kwargs["edge"] = kwargs.pop("bottom", 0)
498
555
 
499
556
  return self._common_bar(x, height, orientation="vertical", **kwargs)
500
557
 
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"}
558
+ def barh(self, y, width, height=0.8, left=None, **kwargs):
559
+ 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
560
  kwargs = self._check_kwargs("barh", kws, **kwargs)
561
+ kwargs["height"] = height
562
+ if left is not None:
563
+ kwargs["left"] = left
504
564
  if "height" in kwargs:
505
565
  kwargs["thickness"] = kwargs.pop("height")
506
566
  kwargs["edge"] = kwargs.pop("left", 0)
507
567
 
508
568
  return self._common_bar(y, width, orientation="horizontal", **kwargs)
509
569
 
570
+ def grouped_bar(self, heights, **kwargs):
571
+ 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"}
572
+ kwargs = self._check_kwargs("grouped_bar", kws, **kwargs)
573
+ if isinstance(heights, dict):
574
+ if "labels" not in kwargs:
575
+ kwargs["labels"] = list(heights.keys())
576
+ heights = list(heights.values())
577
+ if isinstance(heights, (list, tuple)) and len(heights) > 0 and isinstance(heights[0], (list, tuple, _np.ndarray)):
578
+ try:
579
+ datasets = [_np.asarray(ds, dtype=_np.float64) for ds in heights]
580
+ if any(ds.ndim != 1 for ds in datasets):
581
+ raise ValueError("Nested datasets must all be 1-dimensional.")
582
+ except (ValueError, TypeError):
583
+ h_arr = _np.asarray(heights)
584
+ if h_arr.ndim == 2:
585
+ datasets = [h_arr[:, i] for i in range(h_arr.shape[1])]
586
+ else:
587
+ raise ValueError("Invalid dataset structure.")
588
+ else:
589
+ h_arr = _np.asarray(heights)
590
+ if h_arr.ndim == 1:
591
+ datasets = [h_arr]
592
+ elif h_arr.ndim == 2:
593
+ datasets = [h_arr[:, i] for i in range(h_arr.shape[1])]
594
+ else:
595
+ raise ValueError(f"Input must be 1D or 2D, got {h_arr.ndim}D.")
596
+
597
+ xs = kwargs.pop("positions", range(len(datasets[0])))
598
+ if len(xs) != len(datasets[0]):
599
+ raise Warning("Length of positions does not match length of datasets.")
600
+ if _np.diff(xs).min() != _np.diff(xs).max():
601
+ raise Warning("Positions must be equidistant.")
602
+ dist = _np.diff(xs).min()
603
+ settings = {}
604
+ if "rwidth" not in kwargs:
605
+ kwargs["rwidth"] = 1
606
+ orientation = kwargs.pop("orientation", "vertical")
607
+ group_spacing = kwargs.pop("group_spacing", 1.5)
608
+ bar_spacing = kwargs.pop("bar_spacing", 0.0)
609
+ bar_widths_num = len(datasets) * (1 + bar_spacing) + group_spacing
610
+ bar_width = dist / bar_widths_num
611
+ settings["thickness"] = bar_width
612
+
613
+ if "tick_labels" in kwargs:
614
+ tick_labels = kwargs.pop("tick_labels")
615
+ if len(tick_labels) != len(xs):
616
+ raise Warning("Length of tick_labels does not match length of positions.")
617
+ if orientation == "vertical":
618
+ if isinstance(self, Axes):
619
+ self.set_xticks(xs, tick_labels)
620
+ elif isinstance(self, Secondary):
621
+ self._primary.set_xticks(xs, tick_labels)
622
+ else:
623
+ self.set_yticks(xs, tick_labels)
624
+
625
+ datas = {}
626
+ for kw in ["colors", "alpha", "facecolor", "fc", "edgecolor", "ec", "labels", "linestyle", "ls", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]:
627
+ if kw in kwargs:
628
+ if isinstance(kwargs[kw], (list)):
629
+ if len(kwargs[kw]) != len(datasets):
630
+ raise Warning(f"Length of {kw} does not match number of datasets.")
631
+ prop = kwargs.pop(kw)
632
+ else:
633
+ prop = [kwargs.pop(kw)] * len(datasets)
634
+ for i in range(len(datasets)):
635
+ if i not in datas:
636
+ datas[i] = {}
637
+ datas[i][kw.removesuffix("s")] = prop[i]
638
+ base_settings = settings.copy()
639
+ bars = []
640
+ for i in range(len(datasets)):
641
+ offset = (i - len(datasets)/2 + 0.5) * bar_width * (1 + bar_spacing)
642
+ data = datasets[i]
643
+ settings = base_settings.copy()
644
+ kws = datas.get(i, {})
645
+ passing_args = ["alpha", "facecolor", "fc", "color", "c", "edgecolor", "ec", "label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]
646
+ for a in passing_args:
647
+ if a in kws:
648
+ settings[a] = kws[a]
649
+ bars.append(self._common_bar(xs, data, orientation=orientation, **settings, group_offset=offset))
650
+ return bars
651
+
510
652
  def _common_bar(self, k, v, orientation="vertical", **kwargs):
511
653
  #if orientation == "vertical":
512
654
  # self._int_ymax = self._int_ymin = 0
@@ -593,7 +735,7 @@ class BaseAxes:
593
735
 
594
736
  if _np.count_nonzero(edge) > 0:
595
737
  lower = self._get_overlay()
596
- if self._overlay_special.get(lower, None) == bar_type + " stacked":
738
+ if bar_type + " stacked" in self._overlay_special.get(lower, {}):
597
739
  bedx = _np.zeros_like(edge, dtype=_np.float64)
598
740
  bedy = _np.zeros_like(edge, dtype=_np.float64)
599
741
  for el in self._elements[lower]:
@@ -612,16 +754,18 @@ class BaseAxes:
612
754
  if overlay is None:
613
755
  if len(self._elements[self._get_overlay()]) > 0 and self._elements[self._get_overlay()][-1]._check_equal(k, edge):
614
756
  if len(self._elements[lower]) == 1:
615
- self._overlay_special[lower] = bar_type + " stacked"
757
+ if lower not in self._overlay_special:
758
+ self._overlay_special[lower] = {}
759
+ self._overlay_special[lower].update({bar_type + " stacked": None})
616
760
  overlay = lower
617
761
  else:
618
762
  current = self._get_free_overlay()
619
763
  self._elements[current].append(self._elements[lower].pop()) # move the stacking bed to new overlay
620
- self._overlay_special[current] = bar_type + " stacked"
764
+ self._overlay_special[current] = {bar_type + " stacked": None}
621
765
  overlay = current
622
766
  else: # no bed, plot invisible bed
623
767
  current = self._get_free_overlay()
624
- self._overlay_special[current] = bar_type + " stacked"
768
+ self._overlay_special[current] = {bar_type + " stacked": None}
625
769
  self._plot(k, edge, alpha=0)
626
770
  overlay = current
627
771
 
@@ -638,7 +782,7 @@ class BaseAxes:
638
782
  for kw in ["label", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]:
639
783
  if kw in kwargs:
640
784
  element_args[kw] = kwargs.pop(kw)
641
- note = self._overlay_special.get(overlay, None)
785
+ 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
786
  if "label" in element_args:
643
787
  if orientation == "vertical":
644
788
  settings["ybar legend"] = None
@@ -648,7 +792,7 @@ class BaseAxes:
648
792
  return self._plot(k, v, yerr=kwargs.get("yerr", None), xerr=kwargs.get("xerr", None), overlay=overlay, settings=settings, note=note, **element_args)
649
793
 
650
794
  def bar_label(self, container, labels=None, **kwargs):
651
- kws = {"color", "c", "fontsize", "rotation", "fmt", "padding"}
795
+ kws = {"alpha", "color", "c", "fontsize", "rotation", "fmt", "padding"}
652
796
  kwargs = self._check_kwargs("bar_label", kws, **kwargs)
653
797
  if not isinstance(container, Graph):
654
798
  raise Warning("Container must be a Graph object.")
@@ -659,6 +803,8 @@ class BaseAxes:
659
803
  if "color" in kwargs or "c" in kwargs:
660
804
  c = kwargs.pop("color", kwargs.pop("c", None))
661
805
  kwargs["color_parsed"] = self._match_color(c)
806
+ if "alpha" in kwargs:
807
+ kwargs["opacity"] = kwargs.pop("alpha")
662
808
  if "fontsize" in kwargs:
663
809
  kwargs["fontsize_parsed"] = self._tex_fontsize(kwargs["fontsize"])
664
810
  o = self._get_element_overlay(container)
@@ -683,7 +829,68 @@ class BaseAxes:
683
829
  if labels is not None and labels != []:
684
830
  container._add_meta_column(labels)
685
831
  self._bar_labels[self._get_element_overlay(container)][container] = kwargs | {"bar_labels": labels}
686
-
832
+
833
+ def stackplot(self, x, *args, **kwargs):
834
+ kws = {"baseline", "labels", "colors", "alpha", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "linestyle", "ls", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"}
835
+ kwargs = self._check_kwargs("stackplot", kws, **kwargs)
836
+ n = len(x)
837
+ x = _np.asarray(x, dtype=_np.float64)
838
+ if len(args) == 1:
839
+ y = list(args[0])
840
+ else:
841
+ y = list(args)
842
+ if isinstance(list(y)[0], int | float):
843
+ y = [list(y)]
844
+ y = _np.asarray(y, dtype=_np.float64)
845
+ base_set = kwargs.pop("baseline", "zero")
846
+ if base_set not in ["zero", "sym", "wiggle", "weighted_wiggle"]:
847
+ raise Warning(f"Invalid baseline: {base_set}. Must be one of 'zero', 'sym', 'wiggle', or 'weighted_wiggle'.")
848
+ if base_set == "zero":
849
+ baseline = _np.zeros(n, dtype=_np.float64)
850
+ elif base_set == "sym":
851
+ baseline = -_np.sum(y, axis=0)/2
852
+ elif base_set == "wiggle": # from matplotlib source
853
+ m = y.shape[0]
854
+ baseline = (y * (m - 0.5 - _np.arange(m)[:, None])).sum(0)
855
+ baseline /= -m
856
+ else: # weighted_wiggle; from matplotlib source
857
+ total = _np.sum(y, 0)
858
+ inv_total = _np.zeros_like(total)
859
+ mask = total > 0
860
+ inv_total[mask] = 1.0 / total[mask]
861
+ increase = _np.hstack((y[:, 0:1], _np.diff(y)))
862
+ below_size = total - _np.cumsum(y, axis=0, dtype=_np.promote_types(y.dtype, _np.float32))
863
+ below_size += 0.5 * y
864
+ move_up = below_size * inv_total
865
+ move_up[:, 0] = 0.5
866
+ center = (move_up - 0.5) * increase
867
+ center = _np.cumsum(center.sum(0))
868
+ baseline = center - 0.5 * total
869
+ overlay = self._get_free_overlay()
870
+ self._overlay_special[overlay] = {"stack plots": "y", "area style": None}
871
+ if base_set != "zero":
872
+ self._plot(x, baseline, overlay=overlay, alpha=0)
873
+ datas = {}
874
+ for kw in ["labels", "colors", "alpha", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw", "linestyle", "ls", "hatch", "hatch_color", "hatch_linewidth", "hatch_distance"]:
875
+ if kw in kwargs:
876
+ prop = kwargs.pop(kw)
877
+ try:
878
+ prop = list(prop)
879
+ except: pass
880
+ if isinstance(prop, (list, tuple)):
881
+ if len(prop) != len(y):
882
+ raise Warning(f"Length of {kw} does not match number of datasets.")
883
+ else:
884
+ prop = [prop] * len(y)
885
+ datas[kw.removesuffix("s")] = prop
886
+ if "label" in datas:
887
+ self._overlay_special[overlay]["area legend"] = None
888
+ output = []
889
+ for i in range(len(y)):
890
+ kws = {kw: datas[kw][i] for kw in datas}
891
+ kws["_endnotes"] = r"\closedcycle"
892
+ output.append(self._plot(x, y[i], overlay=overlay, settings={"fill": None}, **kws))
893
+ return output
687
894
 
688
895
  def step(self, x, y, *args, **kwargs):
689
896
  kws = {"fmt", "alpha", "color", "c", "linestyle", "ls", "linewidth", "lw", "marker", "markersize", "ms", "label", "where"}
@@ -878,6 +1085,8 @@ class BaseAxes:
878
1085
  self._axis_options["ytick pos"] = Y_INV[(yt_l, yt_r)]
879
1086
  if self._axis_options["xtick pos"] == "both":
880
1087
  self._axis_options.pop("xtick pos")
1088
+ if self._axis_options["ytick pos"] == "both":
1089
+ self._axis_options.pop("ytick pos")
881
1090
  if "colors" in kwargs:
882
1091
  c = self._match_color(kwargs.pop("colors"))
883
1092
  self._update_axis_options(prefix + " tick style", {"draw": c})
@@ -1140,7 +1349,7 @@ class Axes(BaseAxes):
1140
1349
  self._style = self._fig._style
1141
1350
  self._imshow = None
1142
1351
 
1143
- self._defcol_counter = 0
1352
+ self._defcol_counter = {0: 0}
1144
1353
  self._colorbar = ""
1145
1354
  self._cbar_h = False
1146
1355
  self._polar = polar
@@ -1537,9 +1746,11 @@ class Axes(BaseAxes):
1537
1746
  return self._nrows
1538
1747
  def _get_ncols(self):
1539
1748
  return self._ncols
1540
- def _get_defcol(self):
1541
- self._defcol_counter += 1
1542
- return self._defcol_counter - 1
1749
+ def _get_defcol(self, index = 0):
1750
+ if index not in self._defcol_counter:
1751
+ self._defcol_counter[index] = 0
1752
+ self._defcol_counter[index] += 1
1753
+ return self._defcol_counter[index] - 1
1543
1754
  def _show_colorbar(self, cbar, horizontal=False):
1544
1755
  self._colorbar = ",\n" + cbar
1545
1756
  self._cbar_h = horizontal
@@ -1550,7 +1761,7 @@ class Axes(BaseAxes):
1550
1761
  for k,v in self._elements.items():
1551
1762
  if k in self._bar_labels and len(self._bar_labels[k]) > 0:
1552
1763
  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"
1764
+ self._overlay_special[k].update({"nodes near coords": None})
1554
1765
  for e in self._elements[k].copy():
1555
1766
  if e not in self._bar_labels[k]:
1556
1767
  self.bar_label(e, labels=[])
@@ -1561,13 +1772,13 @@ class Axes(BaseAxes):
1561
1772
  """if isinstance(v, list) and len(v) > 1:
1562
1773
  reference = getattr(v[0], "_settings", {}) or {}
1563
1774
  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:
1775
+ forbidden = {"draw", "fill", "xbar", "ybar"}
1776
+ for f in forbidden:
1566
1777
  common_style.pop(f, None)
1567
1778
  for key, val in common_style.items():
1568
1779
  for e in v:
1569
1780
  e._settings.pop(key, None)
1570
- self._overlay_special[k] += f",\n{self._parse_entry(key, val)}" """
1781
+ self._overlay_special[k].update({key: val}) """
1571
1782
  lines = []
1572
1783
  lines2 = []
1573
1784
  if self._polar:
@@ -1577,7 +1788,7 @@ class Axes(BaseAxes):
1577
1788
  if self._polar and TikzConfig.USE_GROUPPLOTS and not single:
1578
1789
  lines.append(f"\\nextgroupplot[alias={self._axis_options['alias']}, width={self._width}, height={self._height}, hide axis]")
1579
1790
  for i in self._elements.keys():
1580
- spec = self._overlay_special[i] if i in self._overlay_special else ""
1791
+ 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
1792
  lines2.append("\\begin{polaraxis}[")
1582
1793
  if i == self._get_overlay():
1583
1794
  lines2.append(f"{main_ax}{spec}\n]")
@@ -1593,18 +1804,18 @@ class Axes(BaseAxes):
1593
1804
  elif not TikzConfig.USE_GROUPPLOTS or (TikzConfig.USE_GROUPPLOTS and single):
1594
1805
  lines.append("\\begin{axis}[")
1595
1806
  if self._get_overlay() == 0:
1596
- spec = self._overlay_special[0] + ",\n" if 0 in self._overlay_special else ""
1807
+ 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
1808
  if self._secondary_y is not None or self._colorbar is not None:
1598
1809
  lines.append(f"{main_ax}{spec}alias={alias}\n]")
1599
1810
  else:
1600
1811
  lines.append(f"{main_ax}{spec}\n]")
1601
1812
  lines.append(contents[0])
1602
1813
  else:
1603
- spec = self._overlay_special[0] + ",\n" if 0 in self._overlay_special else ""
1814
+ 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
1815
  lines.append(f"{aux_ax}{spec}alias={alias}\n]")
1605
1816
  lines.append(contents[0])
1606
1817
  for i in self._elements.keys():
1607
- spec = self._overlay_special[i] + ",\n" if i in self._overlay_special else ""
1818
+ 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
1819
  if i == 0: continue
1609
1820
  lines2.append("\\begin{axis}[")
1610
1821
  if i == self._get_overlay():
@@ -1627,7 +1838,7 @@ class Axes(BaseAxes):
1627
1838
  contents2 = self._secondary_y._content_tex(filename)
1628
1839
  for i in self._secondary_y._elements.keys():
1629
1840
  lines2.append("\\begin{axis}[")
1630
- spec = self._secondary_y._overlay_special[i] if i in self._secondary_y._overlay_special else ""
1841
+ 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
1842
  if i == sorted(self._secondary_y._elements.keys())[-1]:
1632
1843
  lines2.append(f"{main_ax2}{spec}\n]")
1633
1844
  else:
@@ -1715,8 +1926,8 @@ class Secondary(BaseAxes):
1715
1926
  def _padding(self):
1716
1927
  return TikzConfig.SEC_Y_PADDING + TikzConfig.YTICK_PADDING * self._yticks + TikzConfig.SEC_Y_LABEL_PADDING * ("ylabel" in self._axis_options)
1717
1928
 
1718
- def _get_defcol(self):
1719
- return self._primary._get_defcol()
1929
+ def _get_defcol(self, index = 0):
1930
+ return self._primary._get_defcol(index)
1720
1931
 
1721
1932
  def _get_index(self):
1722
1933
  return self._primary._get_index()