tesorotools-python 0.0.49__tar.gz → 0.0.51__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.
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/PKG-INFO +3 -1
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/pyproject.toml +4 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/__init__.py +22 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/__init__.py +8 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/_common.py +1 -3
- tesorotools_python-0.0.51/src/tesorotools/artists/bar_line.py +362 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/barh_plot.py +20 -0
- tesorotools_python-0.0.51/src/tesorotools/artists/box_plot.py +517 -0
- tesorotools_python-0.0.51/src/tesorotools/artists/intraday_plot.py +309 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/line_plot.py +71 -13
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/matrix.py +10 -0
- tesorotools_python-0.0.51/src/tesorotools/artists/plotly_backend.py +1029 -0
- tesorotools_python-0.0.51/src/tesorotools/artists/shock_plot.py +355 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/stacked.py +26 -3
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/type_curve.py +23 -2
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/vector_plot.py +10 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/waterfall.py +10 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/plots.yaml +35 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/images.py +18 -9
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/.gitignore +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/compact.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/template.docx +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/providers/lseg.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/document.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/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.
|
|
3
|
+
Version: 0.0.51
|
|
4
4
|
Requires-Python: >=3.13
|
|
5
5
|
Requires-Dist: babel>=2.17
|
|
6
6
|
Requires-Dist: matplotlib>=3.10
|
|
@@ -19,5 +19,7 @@ Requires-Dist: requests>=2.31; extra == 'ecb'
|
|
|
19
19
|
Provides-Extra: imf
|
|
20
20
|
Requires-Dist: requests>=2.31; extra == 'imf'
|
|
21
21
|
Requires-Dist: truststore>=0.10; extra == 'imf'
|
|
22
|
+
Provides-Extra: interactive-plots
|
|
23
|
+
Requires-Dist: plotly>=5.0; extra == 'interactive-plots'
|
|
22
24
|
Provides-Extra: lseg
|
|
23
25
|
Requires-Dist: lseg-data>=2.1; extra == 'lseg'
|
|
@@ -29,6 +29,7 @@ bde = ["requests>=2.31"]
|
|
|
29
29
|
ecb = ["requests>=2.31"]
|
|
30
30
|
imf = ["requests>=2.31", "truststore>=0.10"]
|
|
31
31
|
lseg = ["lseg-data>=2.1"]
|
|
32
|
+
interactive_plots = ["plotly>=5.0"]
|
|
32
33
|
|
|
33
34
|
[dependency-groups]
|
|
34
35
|
dev = [
|
|
@@ -37,6 +38,9 @@ dev = [
|
|
|
37
38
|
"pre-commit>=4.0",
|
|
38
39
|
"pandas-stubs>=2.2",
|
|
39
40
|
"coverage>=7.0",
|
|
41
|
+
# the interactive Plotly backend is an optional extra, but the test
|
|
42
|
+
# suite exercises it (and the 100% coverage gate requires it present).
|
|
43
|
+
"plotly>=5.0",
|
|
40
44
|
]
|
|
41
45
|
|
|
42
46
|
[build-system]
|
|
@@ -17,8 +17,17 @@ plural variants). See ``docs/extending.md`` for the
|
|
|
17
17
|
recommended idioms.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
20
21
|
from typing import TYPE_CHECKING, Any
|
|
21
22
|
|
|
23
|
+
try:
|
|
24
|
+
#: Installed distribution version (set by hatch-vcs from the git tag).
|
|
25
|
+
#: Falls back to ``"0.0.0"`` when the package is not installed (e.g.
|
|
26
|
+
#: running straight from a source checkout with no metadata).
|
|
27
|
+
__version__ = version("tesorotools-python")
|
|
28
|
+
except PackageNotFoundError: # pragma: no cover - source-only checkout
|
|
29
|
+
__version__ = "0.0.0"
|
|
30
|
+
|
|
22
31
|
if TYPE_CHECKING:
|
|
23
32
|
from tesorotools.providers.bde import BdeProvider
|
|
24
33
|
from tesorotools.providers.ecb import EcbProvider
|
|
@@ -44,12 +53,16 @@ from tesorotools._registry import (
|
|
|
44
53
|
register_tags,
|
|
45
54
|
)
|
|
46
55
|
from tesorotools.artists import (
|
|
56
|
+
BarLinePlot,
|
|
57
|
+
BoxPlot,
|
|
47
58
|
Format,
|
|
48
59
|
GroupedBarChart,
|
|
49
60
|
HorizontalBarChart,
|
|
61
|
+
IntradayPlot,
|
|
50
62
|
Legend,
|
|
51
63
|
LinePlot,
|
|
52
64
|
MatrixChart,
|
|
65
|
+
ShockChart,
|
|
53
66
|
StackedAreaPlot,
|
|
54
67
|
StackedBarPlot,
|
|
55
68
|
TypeCurve,
|
|
@@ -78,6 +91,8 @@ from tesorotools.render import (
|
|
|
78
91
|
|
|
79
92
|
def _register_builtins() -> None:
|
|
80
93
|
register_artist("line_plot", LinePlot)
|
|
94
|
+
register_artist("bar_line", BarLinePlot)
|
|
95
|
+
register_artist("box_plot", BoxPlot)
|
|
81
96
|
register_artist("stacked_area", StackedAreaPlot)
|
|
82
97
|
register_artist("stacked_bar", StackedBarPlot)
|
|
83
98
|
register_artist("barh", HorizontalBarChart)
|
|
@@ -85,6 +100,8 @@ def _register_builtins() -> None:
|
|
|
85
100
|
register_artist("type_curve", TypeCurve)
|
|
86
101
|
register_artist("vector_plot", VectorPlot)
|
|
87
102
|
register_artist("matrix", MatrixChart)
|
|
103
|
+
register_artist("shock_plot", ShockChart)
|
|
104
|
+
register_artist("intraday", IntradayPlot)
|
|
88
105
|
register_artist("waterfall", Waterfall)
|
|
89
106
|
|
|
90
107
|
register_tag("format", Format)
|
|
@@ -103,7 +120,10 @@ _register_builtins()
|
|
|
103
120
|
|
|
104
121
|
|
|
105
122
|
__all__ = [
|
|
123
|
+
"__version__",
|
|
106
124
|
"Artist",
|
|
125
|
+
"BarLinePlot",
|
|
126
|
+
"BoxPlot",
|
|
107
127
|
"BdeProvider",
|
|
108
128
|
"BuildContext",
|
|
109
129
|
"CompositeRegistry",
|
|
@@ -115,6 +135,7 @@ __all__ = [
|
|
|
115
135
|
"HorizontalBarChart",
|
|
116
136
|
"Image",
|
|
117
137
|
"Images",
|
|
138
|
+
"IntradayPlot",
|
|
118
139
|
"Legend",
|
|
119
140
|
"LinePlot",
|
|
120
141
|
"LSEGProvider",
|
|
@@ -122,6 +143,7 @@ __all__ = [
|
|
|
122
143
|
"RegistryProtocol",
|
|
123
144
|
"Report",
|
|
124
145
|
"Section",
|
|
146
|
+
"ShockChart",
|
|
125
147
|
"StackedAreaPlot",
|
|
126
148
|
"StackedBarPlot",
|
|
127
149
|
"Subtitle",
|
|
@@ -26,9 +26,13 @@ strictly image output.
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
from tesorotools.artists._common import Artist, Format, Legend
|
|
29
|
+
from tesorotools.artists.bar_line import BarLinePlot
|
|
29
30
|
from tesorotools.artists.barh_plot import GroupedBarChart, HorizontalBarChart
|
|
31
|
+
from tesorotools.artists.box_plot import BoxPlot
|
|
32
|
+
from tesorotools.artists.intraday_plot import IntradayPlot
|
|
30
33
|
from tesorotools.artists.line_plot import LinePlot
|
|
31
34
|
from tesorotools.artists.matrix import MatrixChart
|
|
35
|
+
from tesorotools.artists.shock_plot import ShockChart
|
|
32
36
|
from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
|
|
33
37
|
from tesorotools.artists.type_curve import TypeCurve
|
|
34
38
|
from tesorotools.artists.vector_plot import VectorPlot
|
|
@@ -36,12 +40,16 @@ from tesorotools.artists.waterfall import Waterfall
|
|
|
36
40
|
|
|
37
41
|
__all__ = [
|
|
38
42
|
"Artist",
|
|
43
|
+
"BarLinePlot",
|
|
44
|
+
"BoxPlot",
|
|
39
45
|
"Format",
|
|
40
46
|
"GroupedBarChart",
|
|
41
47
|
"HorizontalBarChart",
|
|
48
|
+
"IntradayPlot",
|
|
42
49
|
"Legend",
|
|
43
50
|
"LinePlot",
|
|
44
51
|
"MatrixChart",
|
|
52
|
+
"ShockChart",
|
|
45
53
|
"StackedAreaPlot",
|
|
46
54
|
"StackedBarPlot",
|
|
47
55
|
"TypeCurve",
|
|
@@ -96,9 +96,7 @@ def dynamic_dpi(fig: Figure) -> int:
|
|
|
96
96
|
class Artist(Protocol):
|
|
97
97
|
"""Structural contract every image artist satisfies.
|
|
98
98
|
|
|
99
|
-
The
|
|
100
|
-
``StackedAreaPlot``, ``StackedBarPlot``, ``TypeCurve``,
|
|
101
|
-
``HorizontalBarChart``) and any third-party artist
|
|
99
|
+
The built-in artists and any third-party artist
|
|
102
100
|
registered via :func:`tesorotools.register_artist`
|
|
103
101
|
expose the same three-piece surface:
|
|
104
102
|
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Bar + line combo chart artist, registered as ``!bar_line``.
|
|
2
|
+
|
|
3
|
+
Defines :class:`BarLinePlot`: one time-series chart that draws some
|
|
4
|
+
series as **vertical bars** and others as **marked lines** on a single
|
|
5
|
+
shared y-axis. The canonical use is a secondary magnitude in bars (an
|
|
6
|
+
earnings *surprise* in p.p.) under a primary series as the highlighted
|
|
7
|
+
line (earnings *growth* in %), both read against the same 0 baseline.
|
|
8
|
+
|
|
9
|
+
Unlike :class:`LinePlot`, the x-axis is **categorical**: one slot per
|
|
10
|
+
observation (``np.arange``) with date-formatted tick labels, so the bars
|
|
11
|
+
sit at even spacing regardless of the calendar gaps between periods --
|
|
12
|
+
the layout every quarterly/monthly bar chart wants. Lines are drawn at
|
|
13
|
+
the same integer positions so they overlay the bars exactly.
|
|
14
|
+
|
|
15
|
+
This is deliberately *not* an extension of :class:`StackedBarPlot`'s
|
|
16
|
+
``overlay_series`` (a hardcoded black 1.5 px line, no markers): here the
|
|
17
|
+
line is a first-class, fully styleable series -- colour, marker,
|
|
18
|
+
linewidth -- and the bars are the secondary layer.
|
|
19
|
+
|
|
20
|
+
Layout and the shared ``Format`` / ``Legend`` config holders live in
|
|
21
|
+
:mod:`tesorotools.artists._common`; combo styling defaults come from
|
|
22
|
+
``PLOT_CONFIG['bar_line']``. Render lifecycle mirrors
|
|
23
|
+
:class:`LinePlot`: ``build()`` returns ``(Figure, Axes)`` in memory;
|
|
24
|
+
``save(fig)`` persists to ``out_path``; ``plot()`` does both.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import locale
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Self
|
|
32
|
+
|
|
33
|
+
import matplotlib.pyplot as plt
|
|
34
|
+
import numpy as np
|
|
35
|
+
import pandas as pd
|
|
36
|
+
from matplotlib.axes import Axes
|
|
37
|
+
from matplotlib.figure import Figure
|
|
38
|
+
from yaml.nodes import MappingNode
|
|
39
|
+
|
|
40
|
+
from tesorotools.artists._common import (
|
|
41
|
+
AX_CONFIG,
|
|
42
|
+
Format,
|
|
43
|
+
Legend,
|
|
44
|
+
adjust_figure_for_plot_size,
|
|
45
|
+
compact_figure_kwargs,
|
|
46
|
+
compact_font_pt,
|
|
47
|
+
dynamic_dpi,
|
|
48
|
+
place_legend,
|
|
49
|
+
resolve_data,
|
|
50
|
+
style_baseline,
|
|
51
|
+
style_compact_axes,
|
|
52
|
+
style_spines,
|
|
53
|
+
)
|
|
54
|
+
from tesorotools.utils.matplotlib import PLOT_CONFIG
|
|
55
|
+
from tesorotools.utils.template import TemplateLoader
|
|
56
|
+
|
|
57
|
+
locale.setlocale(locale.LC_ALL, "")
|
|
58
|
+
|
|
59
|
+
__all__ = ["BarLinePlot"]
|
|
60
|
+
|
|
61
|
+
BAR_LINE_CONFIG: dict[str, Any] = PLOT_CONFIG["bar_line"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BarLinePlot:
|
|
65
|
+
"""Combo chart: vertical bars and marked lines on one shared axis.
|
|
66
|
+
|
|
67
|
+
Parameters mirror the other time-series artists where they overlap
|
|
68
|
+
(``out_path``, ``data`` / ``data_path``, ``scale``, ``start_date`` /
|
|
69
|
+
``end_date``, ``fmt``, ``legend``, ``figsize``, ``plot_size``,
|
|
70
|
+
``compact`` / ``font_pt``). Combo-specific knobs:
|
|
71
|
+
|
|
72
|
+
* ``bars`` -- ``id -> label`` mapping drawn as vertical bars. Several
|
|
73
|
+
bar series are grouped (dodged) side by side within each slot.
|
|
74
|
+
* ``lines`` -- ``id -> label`` mapping drawn as marked lines, on top of
|
|
75
|
+
the bars. At least one of ``bars`` / ``lines`` must be non-empty.
|
|
76
|
+
* ``bar_styles`` / ``line_styles`` -- per-series matplotlib kwargs.
|
|
77
|
+
Lines default to a round marker (``marker: "o"``, configurable per
|
|
78
|
+
series) and the combo linewidth from ``plots.yaml``; bars default to
|
|
79
|
+
that section's alpha. Colours default to the house cycle -- lines
|
|
80
|
+
first (``C0``, ``C2``, ...), bars after them -- so the common one
|
|
81
|
+
line + one bar case lands on ``C0`` (line) over ``C1`` (bar).
|
|
82
|
+
* ``bar_width`` -- total width of each slot's bar group (fraction of the
|
|
83
|
+
unit spacing).
|
|
84
|
+
* ``baseline`` -- draw the 0 line (default ``True``; bars grow from it).
|
|
85
|
+
* ``date_format`` / ``x_rotation`` / ``max_xticks`` -- x tick labelling.
|
|
86
|
+
``date_format`` is applied via ``strftime`` (lower-cased, e.g.
|
|
87
|
+
``"dic-21"``) to a DatetimeIndex; ``max_xticks`` thins the labels
|
|
88
|
+
(``None`` labels every slot).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
out_path: Path,
|
|
94
|
+
*,
|
|
95
|
+
data: pd.DataFrame | None = None,
|
|
96
|
+
data_path: Path | None = None,
|
|
97
|
+
bars: dict[str, str] | None = None,
|
|
98
|
+
lines: dict[str, str] | None = None,
|
|
99
|
+
scale: float = 1,
|
|
100
|
+
start_date: str | None = None,
|
|
101
|
+
end_date: str | None = None,
|
|
102
|
+
baseline: bool = True,
|
|
103
|
+
fmt: Format | None = None,
|
|
104
|
+
legend: Legend | None = None,
|
|
105
|
+
figsize: tuple[float, float] | None = None,
|
|
106
|
+
plot_size: tuple[float, float] | None = None,
|
|
107
|
+
bar_styles: dict[str, dict[str, Any]] | None = None,
|
|
108
|
+
line_styles: dict[str, dict[str, Any]] | None = None,
|
|
109
|
+
bar_width: float = 0.7,
|
|
110
|
+
date_format: str = "%b-%y",
|
|
111
|
+
x_rotation: float = 45,
|
|
112
|
+
max_xticks: int | None = None,
|
|
113
|
+
compact: bool = False,
|
|
114
|
+
font_pt: float | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
if out_path.suffix != ".png":
|
|
117
|
+
raise ValueError(f"out_path must be .png: {out_path}")
|
|
118
|
+
self.bars = bars or {}
|
|
119
|
+
self.lines = lines or {}
|
|
120
|
+
if not self.bars and not self.lines:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"BarLinePlot needs at least one series in `bars` or `lines`"
|
|
123
|
+
)
|
|
124
|
+
self.out_path = out_path
|
|
125
|
+
self.data = resolve_data(data, data_path)
|
|
126
|
+
self.scale = scale
|
|
127
|
+
self.start_date = start_date
|
|
128
|
+
self.end_date = end_date
|
|
129
|
+
self.baseline = baseline
|
|
130
|
+
self.fmt = fmt or Format()
|
|
131
|
+
self.legend = legend
|
|
132
|
+
self.figsize = figsize
|
|
133
|
+
self.plot_size = plot_size
|
|
134
|
+
self.bar_styles = bar_styles or {}
|
|
135
|
+
self.line_styles = line_styles or {}
|
|
136
|
+
self.bar_width = bar_width
|
|
137
|
+
self.date_format = date_format
|
|
138
|
+
self.x_rotation = x_rotation
|
|
139
|
+
self.max_xticks = max_xticks
|
|
140
|
+
self.compact = compact
|
|
141
|
+
self.font_pt = font_pt
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
145
|
+
"""Build a :class:`BarLinePlot` from the ``!bar_line`` YAML tag.
|
|
146
|
+
|
|
147
|
+
Required keys
|
|
148
|
+
-------------
|
|
149
|
+
``out_path``
|
|
150
|
+
``.png`` destination path.
|
|
151
|
+
``data_path``
|
|
152
|
+
``.feather`` file with the time-series DataFrame
|
|
153
|
+
(DatetimeIndex × series columns).
|
|
154
|
+
``bars`` and/or ``lines``
|
|
155
|
+
``id -> display_label`` mappings; at least one must be
|
|
156
|
+
non-empty and every id must be a column of the DataFrame.
|
|
157
|
+
|
|
158
|
+
Optional keys (forwarded as-is to ``__init__``)
|
|
159
|
+
----------------------------------------------
|
|
160
|
+
``scale``, ``start_date`` / ``end_date``, ``baseline``,
|
|
161
|
+
``fmt`` (``!format``), ``legend`` (``!legend``), ``figsize``,
|
|
162
|
+
``plot_size``, ``bar_styles`` / ``line_styles``, ``bar_width``,
|
|
163
|
+
``date_format``, ``x_rotation``, ``max_xticks``, ``compact``,
|
|
164
|
+
``font_pt``.
|
|
165
|
+
|
|
166
|
+
Example
|
|
167
|
+
-------
|
|
168
|
+
.. code-block:: yaml
|
|
169
|
+
|
|
170
|
+
stoxx_earnings: !bar_line
|
|
171
|
+
out_path: out/stoxx_earnings.png
|
|
172
|
+
data_path: data/earnings.feather
|
|
173
|
+
lines:
|
|
174
|
+
growth: "Crecimiento beneficios (%)"
|
|
175
|
+
bars:
|
|
176
|
+
surprise: "Sorpresa (pp)"
|
|
177
|
+
fmt: !format {decimals: 0}
|
|
178
|
+
legend: !legend {}
|
|
179
|
+
"""
|
|
180
|
+
cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
|
|
181
|
+
node, deep=True
|
|
182
|
+
)
|
|
183
|
+
cfg.pop("id")
|
|
184
|
+
cfg["out_path"] = Path(cfg["out_path"])
|
|
185
|
+
if "data_path" in cfg:
|
|
186
|
+
cfg["data_path"] = Path(cfg["data_path"])
|
|
187
|
+
return cls(**cfg)
|
|
188
|
+
|
|
189
|
+
def _prepare_data(self) -> pd.DataFrame:
|
|
190
|
+
"""Slice by date and scale -- the backend-neutral IR.
|
|
191
|
+
|
|
192
|
+
Returns the sliced, scaled frame carrying every bar and line
|
|
193
|
+
column in dict order. NaNs are preserved (lines skip missing
|
|
194
|
+
points, bars zero them at draw time), so the transform is shared
|
|
195
|
+
byte-for-byte by :meth:`build` and :meth:`to_plotly`.
|
|
196
|
+
"""
|
|
197
|
+
start = (
|
|
198
|
+
pd.Timestamp(self.start_date)
|
|
199
|
+
if self.start_date
|
|
200
|
+
else self.data.index.min()
|
|
201
|
+
)
|
|
202
|
+
end = (
|
|
203
|
+
pd.Timestamp(self.end_date)
|
|
204
|
+
if self.end_date
|
|
205
|
+
else self.data.index.max()
|
|
206
|
+
)
|
|
207
|
+
cols = list(self.bars.keys()) + list(self.lines.keys())
|
|
208
|
+
plot_data = self.data.loc[start:end, cols]
|
|
209
|
+
plot_data = plot_data.dropna(how="all")
|
|
210
|
+
return plot_data * self.scale
|
|
211
|
+
|
|
212
|
+
def _check_columns(self) -> None:
|
|
213
|
+
missing = [
|
|
214
|
+
c for c in (*self.bars, *self.lines) if c not in self.data.columns
|
|
215
|
+
]
|
|
216
|
+
if missing:
|
|
217
|
+
raise KeyError(
|
|
218
|
+
f"BarLinePlot columns not found in data: {missing}. "
|
|
219
|
+
f"Available: {list(self.data.columns)}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _fmt_date(self, value: Any) -> str:
|
|
223
|
+
"""Format one x label: ``strftime`` (lower-cased) or ``str``."""
|
|
224
|
+
strftime = getattr(value, "strftime", None)
|
|
225
|
+
if strftime is None:
|
|
226
|
+
return str(value)
|
|
227
|
+
return strftime(self.date_format).lower().rstrip(".")
|
|
228
|
+
|
|
229
|
+
def _format_xticks(
|
|
230
|
+
self,
|
|
231
|
+
ax: Axes,
|
|
232
|
+
index: pd.Index,
|
|
233
|
+
x: np.ndarray[tuple[int], np.dtype[np.intp]],
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Place date-formatted x ticks, thinned to ``max_xticks``."""
|
|
236
|
+
n = len(index)
|
|
237
|
+
divisor = self.max_xticks or (6 if self.compact else n)
|
|
238
|
+
step = max(1, -(-n // max(1, divisor))) # ceil(n / divisor)
|
|
239
|
+
positions = list(range(0, n, step))
|
|
240
|
+
labels = [self._fmt_date(index[i]) for i in positions]
|
|
241
|
+
ax.set_xticks( # type: ignore[reportUnknownMemberType]
|
|
242
|
+
positions
|
|
243
|
+
)
|
|
244
|
+
ax.set_xticklabels( # type: ignore[reportUnknownMemberType]
|
|
245
|
+
labels,
|
|
246
|
+
rotation=self.x_rotation,
|
|
247
|
+
ha="right" if self.x_rotation else "center",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def to_plotly(self) -> Any:
|
|
251
|
+
"""Render the same combo chart as an interactive Plotly figure.
|
|
252
|
+
|
|
253
|
+
Experimental sibling of :meth:`build`; requires the optional
|
|
254
|
+
``interactive_plots`` extra (``plotly``), imported lazily.
|
|
255
|
+
"""
|
|
256
|
+
from tesorotools.artists.plotly_backend import bar_line_to_figure
|
|
257
|
+
|
|
258
|
+
return bar_line_to_figure(self)
|
|
259
|
+
|
|
260
|
+
def build(self) -> tuple[Figure, Axes]:
|
|
261
|
+
"""Render the chart in memory; return ``(fig, ax)`` without saving."""
|
|
262
|
+
self._check_columns()
|
|
263
|
+
plot_data = self._prepare_data()
|
|
264
|
+
x = np.arange(len(plot_data))
|
|
265
|
+
cycle: list[str] = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
|
266
|
+
|
|
267
|
+
font_pt = compact_font_pt(self.font_pt) if self.compact else None
|
|
268
|
+
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
269
|
+
**compact_figure_kwargs(self.compact, self.figsize)
|
|
270
|
+
)
|
|
271
|
+
ax: Axes = fig.add_subplot()
|
|
272
|
+
|
|
273
|
+
# Bars first (behind the lines). Several bar series share each slot,
|
|
274
|
+
# dodged side by side; a single series is centred.
|
|
275
|
+
n_bars = len(self.bars)
|
|
276
|
+
each = self.bar_width / n_bars if n_bars else self.bar_width
|
|
277
|
+
default_alpha: float = float(BAR_LINE_CONFIG["bar"].get("alpha", 0.9))
|
|
278
|
+
for j, (col, label) in enumerate(self.bars.items()):
|
|
279
|
+
style = dict(self.bar_styles.get(col, {}))
|
|
280
|
+
style.setdefault("color", cycle[(len(self.lines) + j) % len(cycle)])
|
|
281
|
+
style.setdefault("alpha", default_alpha)
|
|
282
|
+
offset = (j - (n_bars - 1) / 2) * each
|
|
283
|
+
values = plot_data[col].fillna(0).to_numpy(dtype=np.float64)
|
|
284
|
+
ax.bar( # type: ignore[reportUnknownMemberType]
|
|
285
|
+
x + offset,
|
|
286
|
+
values,
|
|
287
|
+
width=each,
|
|
288
|
+
label=label,
|
|
289
|
+
zorder=2,
|
|
290
|
+
**style,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Lines on top, marked. Defaults (marker, linewidth, markersize) come
|
|
294
|
+
# from the bar_line config; per-series line_styles override them.
|
|
295
|
+
line_cfg: dict[str, Any] = BAR_LINE_CONFIG["line"]
|
|
296
|
+
for i, (col, label) in enumerate(self.lines.items()):
|
|
297
|
+
style = dict(self.line_styles.get(col, {}))
|
|
298
|
+
style.setdefault("color", cycle[i % len(cycle)])
|
|
299
|
+
style.setdefault("marker", line_cfg.get("marker", "o"))
|
|
300
|
+
style.setdefault("linewidth", line_cfg.get("linewidth", 2))
|
|
301
|
+
style.setdefault("markersize", line_cfg.get("markersize", 5))
|
|
302
|
+
ax.plot( # type: ignore[reportUnknownMemberType]
|
|
303
|
+
x,
|
|
304
|
+
plot_data[col].to_numpy(dtype=np.float64),
|
|
305
|
+
label=label,
|
|
306
|
+
zorder=3,
|
|
307
|
+
**style,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
self._format_xticks(ax, plot_data.index, x)
|
|
311
|
+
|
|
312
|
+
style_spines(
|
|
313
|
+
ax,
|
|
314
|
+
decimals=self.fmt.decimals,
|
|
315
|
+
units=self.fmt.units,
|
|
316
|
+
**AX_CONFIG["spines"],
|
|
317
|
+
)
|
|
318
|
+
# style_spines enables the y grid only (axis="y"), which is what these
|
|
319
|
+
# charts read against; the categorical x-axis stays gridless.
|
|
320
|
+
if self.baseline:
|
|
321
|
+
style_baseline(ax, 0, **AX_CONFIG["baseline"])
|
|
322
|
+
|
|
323
|
+
if self.compact:
|
|
324
|
+
style_compact_axes(ax, font_pt, side="right")
|
|
325
|
+
|
|
326
|
+
legend_ncol = self.legend.ncol if self.legend else None
|
|
327
|
+
if self.legend is not None:
|
|
328
|
+
place_legend(fig, ax, compact=self.compact, ncol=legend_ncol)
|
|
329
|
+
|
|
330
|
+
if self.plot_size is not None and not self.compact:
|
|
331
|
+
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
|
332
|
+
|
|
333
|
+
return fig, ax
|
|
334
|
+
|
|
335
|
+
def save(
|
|
336
|
+
self,
|
|
337
|
+
fig: Figure,
|
|
338
|
+
*,
|
|
339
|
+
path: Path | None = None,
|
|
340
|
+
dpi: int | None = None,
|
|
341
|
+
) -> Path:
|
|
342
|
+
"""Persist *fig* as a PNG and return the path written.
|
|
343
|
+
|
|
344
|
+
Mirrors :meth:`LinePlot.save`: defaults to ``self.out_path``;
|
|
345
|
+
``dpi`` overrides the save resolution, derived from the figure
|
|
346
|
+
size via :func:`dynamic_dpi` when omitted.
|
|
347
|
+
"""
|
|
348
|
+
target: Path = path if path is not None else self.out_path
|
|
349
|
+
save_dpi: int = dpi if dpi is not None else dynamic_dpi(fig)
|
|
350
|
+
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
351
|
+
target, dpi=save_dpi
|
|
352
|
+
)
|
|
353
|
+
return target
|
|
354
|
+
|
|
355
|
+
def plot(self) -> Axes:
|
|
356
|
+
"""Build the chart and persist it to ``self.out_path``."""
|
|
357
|
+
fig, ax = self.build()
|
|
358
|
+
try:
|
|
359
|
+
self.save(fig)
|
|
360
|
+
finally:
|
|
361
|
+
plt.close(fig)
|
|
362
|
+
return ax
|
{tesorotools_python-0.0.49 → tesorotools_python-0.0.51}/src/tesorotools/artists/barh_plot.py
RENAMED
|
@@ -604,6 +604,16 @@ class HorizontalBarChart:
|
|
|
604
604
|
cfg["data_path"] = Path(cfg["data_path"])
|
|
605
605
|
return cls(**cfg)
|
|
606
606
|
|
|
607
|
+
def to_plotly(self) -> Any:
|
|
608
|
+
"""Render the same horizontal bars as an interactive Plotly figure.
|
|
609
|
+
|
|
610
|
+
Experimental sibling of :meth:`plot`; requires the optional
|
|
611
|
+
``interactive_plots`` extra (``plotly``), imported lazily.
|
|
612
|
+
"""
|
|
613
|
+
from tesorotools.artists.plotly_backend import horizontal_bar_to_figure
|
|
614
|
+
|
|
615
|
+
return horizontal_bar_to_figure(self)
|
|
616
|
+
|
|
607
617
|
def _build_frame(self) -> pd.DataFrame:
|
|
608
618
|
"""Stage the per-bar value/colour/alpha DataFrame."""
|
|
609
619
|
ids = list(self.alias.keys())
|
|
@@ -846,6 +856,16 @@ class GroupedBarChart:
|
|
|
846
856
|
cfg["data_path"] = Path(cfg["data_path"])
|
|
847
857
|
return cls(**cfg)
|
|
848
858
|
|
|
859
|
+
def to_plotly(self) -> Any:
|
|
860
|
+
"""Render the same grouped bars as an interactive Plotly figure.
|
|
861
|
+
|
|
862
|
+
Experimental sibling of :meth:`plot`; requires the optional
|
|
863
|
+
``interactive_plots`` extra (``plotly``), imported lazily.
|
|
864
|
+
"""
|
|
865
|
+
from tesorotools.artists.plotly_backend import grouped_bar_to_figure
|
|
866
|
+
|
|
867
|
+
return grouped_bar_to_figure(self)
|
|
868
|
+
|
|
849
869
|
def plot(self) -> Axes:
|
|
850
870
|
"""Render the chart and persist it to ``self.out_path``."""
|
|
851
871
|
cat_ids = list(self.categories.keys())
|