tesorotools-python 0.0.34__tar.gz → 0.0.36__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 (75) hide show
  1. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/PKG-INFO +1 -1
  2. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/pyproject.toml +1 -1
  3. tesorotools_python-0.0.36/src/tesorotools/__init__.py +115 -0
  4. tesorotools_python-0.0.36/src/tesorotools/_registry.py +117 -0
  5. tesorotools_python-0.0.36/src/tesorotools/artists/__init__.py +25 -0
  6. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/artists/line_plot.py +31 -38
  7. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/artists/stacked.py +25 -27
  8. tesorotools_python-0.0.36/src/tesorotools/artists/type_curve.py +229 -0
  9. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/plots.yaml +0 -2
  10. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/convert.py +17 -3
  11. tesorotools_python-0.0.36/src/tesorotools/driver.py +138 -0
  12. tesorotools_python-0.0.36/src/tesorotools/providers/__init__.py +32 -0
  13. tesorotools_python-0.0.36/src/tesorotools/render/__init__.py +22 -0
  14. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/content/table.py +42 -7
  15. tesorotools_python-0.0.36/src/tesorotools/utils/config.py +38 -0
  16. tesorotools_python-0.0.34/src/tesorotools/artists/__init__.py +0 -5
  17. tesorotools_python-0.0.34/src/tesorotools/artists/type_curve.py +0 -199
  18. tesorotools_python-0.0.34/src/tesorotools/providers/__init__.py +0 -0
  19. tesorotools_python-0.0.34/src/tesorotools/render/__init__.py +0 -0
  20. tesorotools_python-0.0.34/src/tesorotools/render/content/__init__.py +0 -0
  21. tesorotools_python-0.0.34/src/tesorotools/utils/config.py +0 -98
  22. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/.gitignore +0 -0
  23. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/artists/barh.md +0 -0
  24. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/artists/barh_plot.py +0 -0
  25. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/artists/table.py +0 -0
  26. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/README.md +0 -0
  27. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  28. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  29. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  30. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  31. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  32. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  33. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  34. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  35. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/README.md +0 -0
  36. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  37. {tesorotools_python-0.0.34/src/tesorotools → tesorotools_python-0.0.36/src/tesorotools/data_sources}/__init__.py +0 -0
  38. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/data_sources/debug.py +0 -0
  39. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/database/__init__.py +0 -0
  40. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/database/local.py +0 -0
  41. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/database/push.py +0 -0
  42. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/database/shared.py +0 -0
  43. {tesorotools_python-0.0.34/src/tesorotools/data_sources → tesorotools_python-0.0.36/src/tesorotools/dependencies}/__init__.py +0 -0
  44. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/dependencies/node.py +0 -0
  45. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/dependencies/resolution.py +0 -0
  46. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/main.py +0 -0
  47. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/manifest.py +0 -0
  48. {tesorotools_python-0.0.34/src/tesorotools/dependencies → tesorotools_python-0.0.36/src/tesorotools/offsets}/__init__.py +0 -0
  49. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/offsets/offsets.py +0 -0
  50. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/offsets/outliers.py +0 -0
  51. {tesorotools_python-0.0.34/src/tesorotools/offsets → tesorotools_python-0.0.36/src/tesorotools/pipeline}/__init__.py +0 -0
  52. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/pipeline/diagnose.py +0 -0
  53. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/pipeline/engine.py +0 -0
  54. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/pipeline/rules.py +0 -0
  55. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/providers/base.py +0 -0
  56. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/providers/bde.py +0 -0
  57. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/providers/ecb.py +0 -0
  58. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/py.typed +0 -0
  59. {tesorotools_python-0.0.34/src/tesorotools/pipeline → tesorotools_python-0.0.36/src/tesorotools/render/content}/__init__.py +0 -0
  60. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/content/content.py +0 -0
  61. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/content/images.py +0 -0
  62. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/content/section.py +0 -0
  63. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/content/subtitle.py +0 -0
  64. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/content/text.py +0 -0
  65. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/content/title.py +0 -0
  66. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/render/report.py +0 -0
  67. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/testing/__init__.py +0 -0
  68. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/testing/compare.py +0 -0
  69. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/utils/__init__.py +0 -0
  70. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/utils/format.py +0 -0
  71. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/utils/globals.py +0 -0
  72. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/utils/matplotlib.py +0 -0
  73. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/utils/series.py +0 -0
  74. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/utils/shortcuts.py +0 -0
  75. {tesorotools_python-0.0.34 → tesorotools_python-0.0.36}/src/tesorotools/utils/template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tesorotools-python
3
- Version: 0.0.34
3
+ Version: 0.0.36
4
4
  Requires-Python: >=3.13
5
5
  Requires-Dist: babel>=2.17
6
6
  Requires-Dist: matplotlib>=3.10
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "tesorotools-python"
3
3
  requires-python = ">=3.13"
4
- version = "0.0.34"
4
+ version = "0.0.36"
5
5
  dependencies = [
6
6
  # database and ORM
7
7
  "psycopg[binary]>=3.1",
@@ -0,0 +1,115 @@
1
+ """tesorotools public API.
2
+
3
+ Importing ``tesorotools`` eagerly loads artist and render
4
+ classes (their modules run matplotlib/locale setup as side
5
+ effects) and registers their YAML tags via
6
+ ``_register_builtins``.
7
+
8
+ Provider subclasses gated by optional extras
9
+ (``BdeProvider`` requires ``[bde]``, ``EcbProvider``
10
+ requires ``[ecb]``) are exposed lazily through
11
+ ``__getattr__``; importing this module does not require the
12
+ extras to be installed.
13
+
14
+ Third parties extend the package via ``register_artist``,
15
+ ``register_tag``, and ``register_provider``.
16
+ """
17
+
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ if TYPE_CHECKING:
21
+ from tesorotools.providers.bde import BdeProvider
22
+ from tesorotools.providers.ecb import EcbProvider
23
+
24
+ from tesorotools._registry import (
25
+ Artist,
26
+ get_artist,
27
+ get_provider,
28
+ register_artist,
29
+ register_provider,
30
+ register_tag,
31
+ )
32
+ from tesorotools.artists import (
33
+ Format,
34
+ HorizontalBarChart,
35
+ Legend,
36
+ LinePlot,
37
+ StackedAreaPlot,
38
+ StackedBarPlot,
39
+ TypeCurve,
40
+ )
41
+ from tesorotools.providers.base import DataProvider
42
+ from tesorotools.render import (
43
+ Content,
44
+ Image,
45
+ Images,
46
+ Report,
47
+ Section,
48
+ Subtitle,
49
+ Table,
50
+ Text,
51
+ Title,
52
+ )
53
+
54
+
55
+ def _register_builtins() -> None:
56
+ register_artist("line_plot", LinePlot)
57
+ register_artist("stacked_area", StackedAreaPlot)
58
+ register_artist("stacked_bar", StackedBarPlot)
59
+ register_artist("barh", HorizontalBarChart)
60
+ register_artist("type_curve", TypeCurve)
61
+
62
+ register_tag("format", Format)
63
+ register_tag("legend", Legend)
64
+ register_tag("report", Report)
65
+ register_tag("section", Section)
66
+ register_tag("image", Image)
67
+ register_tag("images", Images)
68
+ register_tag("table", Table)
69
+ register_tag("text", Text)
70
+ register_tag("title", Title)
71
+ register_tag("subtitle", Subtitle)
72
+
73
+
74
+ _register_builtins()
75
+
76
+
77
+ __all__ = [
78
+ "Artist",
79
+ "BdeProvider",
80
+ "Content",
81
+ "DataProvider",
82
+ "EcbProvider",
83
+ "Format",
84
+ "HorizontalBarChart",
85
+ "Image",
86
+ "Images",
87
+ "Legend",
88
+ "LinePlot",
89
+ "Report",
90
+ "Section",
91
+ "StackedAreaPlot",
92
+ "StackedBarPlot",
93
+ "Subtitle",
94
+ "Table",
95
+ "Text",
96
+ "Title",
97
+ "TypeCurve",
98
+ "get_artist",
99
+ "get_provider",
100
+ "register_artist",
101
+ "register_provider",
102
+ "register_tag",
103
+ ]
104
+
105
+
106
+ def __getattr__(name: str) -> Any:
107
+ if name == "BdeProvider":
108
+ from tesorotools.providers.bde import BdeProvider
109
+
110
+ return BdeProvider
111
+ if name == "EcbProvider":
112
+ from tesorotools.providers.ecb import EcbProvider
113
+
114
+ return EcbProvider
115
+ raise AttributeError(f"module 'tesorotools' has no attribute {name!r}")
@@ -0,0 +1,117 @@
1
+ """Registries for artists, providers, and YAML tags.
2
+
3
+ Single source of truth for "name -> class" lookups used both
4
+ by the YAML loader (``TemplateLoader``) and by code that
5
+ dispatches by string name.
6
+
7
+ Three public entry points:
8
+
9
+ ``register_artist(name, cls)``
10
+ Adds ``cls`` to the artist registry **and** registers the
11
+ YAML tag ``!{name}`` via ``cls.from_yaml``. Use for
12
+ chart/figure classes (LinePlot, StackedAreaPlot, etc.).
13
+
14
+ ``register_tag(name, constructor)``
15
+ Registers a YAML tag ``!{name}`` only. Use for things
16
+ that exist as YAML constructors but are not artists or
17
+ providers (Format, Legend, Title, Subtitle, Section,
18
+ Image, Text, Table, Report).
19
+
20
+ ``register_provider(name, cls)``
21
+ Adds ``cls`` to the provider registry. Programmatic
22
+ only -- providers do not appear in YAML today.
23
+
24
+ Look up registered classes via ``get_artist`` / ``get_provider``;
25
+ both raise ``KeyError`` listing the available names.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import Any, Callable, Protocol, cast
31
+
32
+ from yaml.nodes import MappingNode
33
+
34
+ from tesorotools.providers.base import DataProvider
35
+ from tesorotools.utils.template import TemplateLoader
36
+
37
+
38
+ class Artist(Protocol):
39
+ """Anything constructible from a YAML mapping node.
40
+
41
+ Concrete artists satisfy this Protocol structurally;
42
+ inheriting from it is not required.
43
+
44
+ The ``loader`` parameter is typed as ``Any`` because some
45
+ in-tree ``from_yaml`` classmethods are declared with
46
+ ``yaml.Loader`` (the base class) and others with
47
+ ``TemplateLoader``; both work at runtime since the loader
48
+ is always a ``TemplateLoader``.
49
+ """
50
+
51
+ @classmethod
52
+ def from_yaml(cls, loader: Any, node: MappingNode) -> Any: ...
53
+
54
+
55
+ YamlConstructor = Callable[[Any, MappingNode], Any]
56
+
57
+
58
+ _ARTIST_REGISTRY: dict[str, type[Artist]] = {}
59
+ _PROVIDER_REGISTRY: dict[str, type[DataProvider]] = {}
60
+
61
+
62
+ def register_artist(name: str, cls: type[Artist]) -> None:
63
+ """Register ``cls`` as the artist for ``name``.
64
+
65
+ Adds ``cls`` to the artist registry and binds the YAML
66
+ tag ``!{name}`` to ``cls.from_yaml``. Re-registering the
67
+ same name overrides both bindings.
68
+ """
69
+ _ARTIST_REGISTRY[name] = cls
70
+ TemplateLoader.add_constructor(
71
+ f"!{name}", cast(YamlConstructor, cls.from_yaml)
72
+ )
73
+
74
+
75
+ def get_artist(name: str) -> type[Artist]:
76
+ try:
77
+ return _ARTIST_REGISTRY[name]
78
+ except KeyError:
79
+ available = sorted(_ARTIST_REGISTRY)
80
+ raise KeyError(
81
+ f"No artist registered as {name!r}. Available: {available}"
82
+ ) from None
83
+
84
+
85
+ def register_provider(name: str, cls: type[DataProvider]) -> None:
86
+ """Register ``cls`` as the provider for ``name``."""
87
+ _PROVIDER_REGISTRY[name] = cls
88
+
89
+
90
+ def get_provider(name: str) -> type[DataProvider]:
91
+ try:
92
+ return _PROVIDER_REGISTRY[name]
93
+ except KeyError:
94
+ available = sorted(_PROVIDER_REGISTRY)
95
+ raise KeyError(
96
+ f"No provider registered as {name!r}. Available: {available}"
97
+ ) from None
98
+
99
+
100
+ def register_tag(
101
+ name: str,
102
+ constructor: type[Artist] | YamlConstructor,
103
+ ) -> None:
104
+ """Register a YAML tag ``!{name}``.
105
+
106
+ Accepts either a class with a ``from_yaml`` classmethod
107
+ or a bare callable matching the loader signature. Use
108
+ ``register_artist`` / ``register_provider`` instead when
109
+ the registered name should also be looked up by code.
110
+ """
111
+ if isinstance(constructor, type):
112
+ cls = cast(type[Artist], constructor)
113
+ TemplateLoader.add_constructor(
114
+ f"!{name}", cast(YamlConstructor, cls.from_yaml)
115
+ )
116
+ else:
117
+ TemplateLoader.add_constructor(f"!{name}", constructor)
@@ -0,0 +1,25 @@
1
+ """Public artist API.
2
+
3
+ Importing this module applies the matplotlib stylesheet
4
+ defined by ``tesorotools.utils.globals.STYLE_SHEET``.
5
+ """
6
+
7
+ import matplotlib.style
8
+
9
+ from tesorotools.artists.barh_plot import HorizontalBarChart
10
+ from tesorotools.artists.line_plot import Format, Legend, LinePlot
11
+ from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
12
+ from tesorotools.artists.type_curve import TypeCurve
13
+ from tesorotools.utils.globals import STYLE_SHEET
14
+
15
+ matplotlib.style.use(STYLE_SHEET)
16
+
17
+ __all__ = [
18
+ "Format",
19
+ "HorizontalBarChart",
20
+ "Legend",
21
+ "LinePlot",
22
+ "StackedAreaPlot",
23
+ "StackedBarPlot",
24
+ "TypeCurve",
25
+ ]
@@ -310,21 +310,20 @@ def style_baseline(
310
310
  reference: float = 0,
311
311
  **baseline_config: Any,
312
312
  ) -> None:
313
- color: str = baseline_config["color"]
313
+ """Draw a horizontal baseline at *reference*.
314
+
315
+ Always uses ``axhline`` (with high zorder) so callers that
316
+ later restyle the spines do not silently erase the baseline.
317
+ """
314
318
  bottom_lim, top_lim = ax.get_ylim()
315
319
  ax.set_ylim(
316
320
  bottom=min(reference, bottom_lim),
317
321
  top=max(reference, top_lim),
318
322
  )
319
- bottom_lim, top_lim = ax.get_ylim()
320
- if bottom_lim == reference:
321
- ax.spines["bottom"].set_edgecolor(color)
322
- elif top_lim == reference:
323
- ax.spines["top"].set_edgecolor(color)
324
- else:
325
- ax.axhline( # type: ignore[reportUnknownMemberType]
326
- y=reference, **baseline_config
327
- )
323
+ baseline_config.setdefault("zorder", 2.5)
324
+ ax.axhline( # type: ignore[reportUnknownMemberType]
325
+ y=reference, **baseline_config
326
+ )
328
327
 
329
328
 
330
329
  def plot_line_chart(
@@ -333,12 +332,12 @@ def plot_line_chart(
333
332
  *,
334
333
  base_100: bool,
335
334
  annotate: bool,
336
- format: dict[str, Any],
335
+ fmt: dict[str, Any],
337
336
  **kwargs: Any,
338
337
  ) -> None:
339
338
  if base_100:
340
339
  data = data / data.iloc[0, :] * 100
341
- if format["units"] == "p.b.":
340
+ if fmt["units"] == "p.b.":
342
341
  data = data * 100
343
342
  fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
344
343
  **FIG_CONFIG
@@ -349,11 +348,13 @@ def plot_line_chart(
349
348
  pass
350
349
 
351
350
  reference = 100 if base_100 else 0
352
- style_spines(ax, **format, **AX_CONFIG["spines"])
351
+ style_spines(ax, **fmt, **AX_CONFIG["spines"])
353
352
  style_baseline(ax, reference, **AX_CONFIG["baseline"])
354
- ax.legend( # type: ignore[reportUnknownMemberType]
355
- loc="upper center",
356
- bbox_to_anchor=(0.5, LINE_PLOT_CONFIG["legend_sep"]),
353
+ handles, label_strs = ax.get_legend_handles_labels()
354
+ fig.legend( # type: ignore[reportUnknownMemberType]
355
+ handles,
356
+ label_strs,
357
+ loc="outside lower center",
357
358
  ncol=(
358
359
  kwargs["legend"]["ncol"]
359
360
  if kwargs.get("legend", None) is not None
@@ -407,10 +408,8 @@ class Legend:
407
408
  def __init__(
408
409
  self,
409
410
  ncol: int | None = None,
410
- sep: float | None = None,
411
411
  ) -> None:
412
412
  self.ncol = ncol
413
- self.sep = sep
414
413
 
415
414
  @classmethod
416
415
  def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
@@ -436,7 +435,7 @@ class LinePlot:
436
435
  annotate: bool = False,
437
436
  annotate_color: str | None = None,
438
437
  baseline: bool = False,
439
- format: Format | None = None,
438
+ fmt: Format | None = None,
440
439
  legend: Legend | None = None,
441
440
  data: pd.DataFrame | None = None,
442
441
  figsize: tuple[float, float] | None = None,
@@ -469,7 +468,7 @@ class LinePlot:
469
468
  self.base_100_date = base_100_date
470
469
  self.annotate = annotate
471
470
  self.annotate_color = annotate_color
472
- self.format = format
471
+ self.fmt = fmt
473
472
  self.start_date = start_date
474
473
  self.end_date = end_date
475
474
  self.series = series
@@ -545,13 +544,13 @@ class LinePlot:
545
544
  if self.vlines:
546
545
  draw_vlines(ax, self.vlines)
547
546
 
548
- assert self.format is not None
547
+ assert self.fmt is not None
549
548
  if self.annotate:
550
549
  annotate_last_values(
551
550
  ax,
552
551
  plot_data,
553
- decimals=self.format.decimals,
554
- units=self.format.units,
552
+ decimals=self.fmt.decimals,
553
+ units=self.fmt.units,
555
554
  labels=self.series,
556
555
  series_styles=self.series_styles,
557
556
  annotate_color=self.annotate_color,
@@ -559,8 +558,8 @@ class LinePlot:
559
558
 
560
559
  style_spines( # maybe make this function accept a Format object
561
560
  ax,
562
- decimals=self.format.decimals,
563
- units=self.format.units,
561
+ decimals=self.fmt.decimals,
562
+ units=self.fmt.units,
564
563
  **AX_CONFIG["spines"],
565
564
  )
566
565
  if self.baseline:
@@ -569,25 +568,19 @@ class LinePlot:
569
568
 
570
569
  if self.legend is not None:
571
570
  labels = [self.series[c] for c in plot_data.columns]
571
+ handles, label_strs = ax.get_legend_handles_labels()
572
+ fig_width_px: float = fig.get_size_inches()[0] * fig.dpi
572
573
  ncol = (
573
574
  self.legend.ncol
574
575
  if self.legend.ncol is not None
575
- else auto_ncol(ax, labels)
576
- )
577
- sep = (
578
- self.legend.sep
579
- if self.legend.sep is not None
580
- else LINE_PLOT_CONFIG["legend_sep"]
576
+ else auto_ncol(ax, labels, available_width_px=fig_width_px)
581
577
  )
582
- ax.legend( # type: ignore[reportUnknownMemberType]
583
- loc="upper center",
584
- bbox_to_anchor=(0.5, sep),
578
+ fig.legend( # type: ignore[reportUnknownMemberType]
579
+ handles,
580
+ label_strs,
581
+ loc="outside lower center",
585
582
  ncol=ncol,
586
583
  )
587
- else:
588
- ax.legend().set_visible( # type: ignore[reportUnknownMemberType]
589
- False
590
- )
591
584
 
592
585
  if self.plot_size is not None:
593
586
  adjust_figure_for_plot_size(fig, ax, self.plot_size)
@@ -22,8 +22,6 @@ from tesorotools.artists.line_plot import (
22
22
  )
23
23
  from tesorotools.utils.config import TemplateLoader
24
24
 
25
- _DEFAULT_SEP = -0.125
26
-
27
25
 
28
26
  class StackedAreaPlot:
29
27
  """Stacked area chart with the tesorotools visual style.
@@ -43,7 +41,7 @@ class StackedAreaPlot:
43
41
  start_date: str | None = None,
44
42
  end_date: str | None = None,
45
43
  baseline: bool = False,
46
- format: Format | None = None,
44
+ fmt: Format | None = None,
47
45
  legend: Legend | None = None,
48
46
  figsize: tuple[float, float] | None = None,
49
47
  plot_size: tuple[float, float] | None = None,
@@ -57,7 +55,7 @@ class StackedAreaPlot:
57
55
  self.start_date = start_date
58
56
  self.end_date = end_date
59
57
  self.baseline = baseline
60
- self.format = format or Format()
58
+ self.fmt = fmt or Format()
61
59
  self.legend = legend
62
60
  self.figsize = figsize
63
61
  self.plot_size = plot_size
@@ -109,23 +107,25 @@ class StackedAreaPlot:
109
107
 
110
108
  style_spines(
111
109
  ax,
112
- decimals=self.format.decimals,
113
- units=self.format.units,
110
+ decimals=self.fmt.decimals,
111
+ units=self.fmt.units,
114
112
  **AX_CONFIG["spines"],
115
113
  )
116
114
  if self.baseline:
117
115
  style_baseline(ax, 0, **AX_CONFIG["baseline"])
118
116
 
117
+ fig_width_px: float = fig.get_size_inches()[0] * fig.dpi
119
118
  legend_ncol = self.legend.ncol if self.legend else None
120
- ncol = legend_ncol if legend_ncol is not None else auto_ncol(ax, labels)
121
- sep = (
122
- self.legend.sep
123
- if self.legend and self.legend.sep is not None
124
- else _DEFAULT_SEP
119
+ ncol = (
120
+ legend_ncol
121
+ if legend_ncol is not None
122
+ else auto_ncol(ax, labels, available_width_px=fig_width_px)
125
123
  )
126
- ax.legend( # type: ignore[reportUnknownMemberType]
127
- loc="upper center",
128
- bbox_to_anchor=(0.5, sep),
124
+ handles, label_strs = ax.get_legend_handles_labels()
125
+ fig.legend( # type: ignore[reportUnknownMemberType]
126
+ handles,
127
+ label_strs,
128
+ loc="outside lower center",
129
129
  ncol=ncol,
130
130
  )
131
131
 
@@ -163,7 +163,7 @@ class StackedBarPlot:
163
163
  start_date: str | None = None,
164
164
  end_date: str | None = None,
165
165
  baseline: bool = True,
166
- format: Format | None = None,
166
+ fmt: Format | None = None,
167
167
  legend: Legend | None = None,
168
168
  figsize: tuple[float, float] | None = None,
169
169
  overlay_series: dict[str, str] | None = None,
@@ -180,7 +180,7 @@ class StackedBarPlot:
180
180
  self.start_date = start_date
181
181
  self.end_date = end_date
182
182
  self.baseline = baseline
183
- self.format = format or Format()
183
+ self.fmt = fmt or Format()
184
184
  self.legend = legend
185
185
  self.plot_size = plot_size
186
186
  self.figsize = figsize
@@ -313,8 +313,8 @@ class StackedBarPlot:
313
313
 
314
314
  style_spines(
315
315
  ax,
316
- decimals=self.format.decimals,
317
- units=self.format.units,
316
+ decimals=self.fmt.decimals,
317
+ units=self.fmt.units,
318
318
  **AX_CONFIG["spines"],
319
319
  )
320
320
  if self.baseline:
@@ -323,20 +323,18 @@ class StackedBarPlot:
323
323
  all_labels = list(self.series.values()) + list(
324
324
  self.overlay_series.values()
325
325
  )
326
+ fig_width_px: float = fig.get_size_inches()[0] * fig.dpi
326
327
  legend_ncol = self.legend.ncol if self.legend else None
327
328
  ncol = (
328
329
  legend_ncol
329
330
  if legend_ncol is not None
330
- else auto_ncol(ax, all_labels)
331
- )
332
- sep = (
333
- self.legend.sep
334
- if self.legend and self.legend.sep is not None
335
- else _DEFAULT_SEP
331
+ else auto_ncol(ax, all_labels, available_width_px=fig_width_px)
336
332
  )
337
- ax.legend( # type: ignore[reportUnknownMemberType]
338
- loc="upper center",
339
- bbox_to_anchor=(0.5, sep),
333
+ handles, label_strs = ax.get_legend_handles_labels()
334
+ fig.legend( # type: ignore[reportUnknownMemberType]
335
+ handles,
336
+ label_strs,
337
+ loc="outside lower center",
340
338
  ncol=ncol,
341
339
  )
342
340