tesorotools-python 0.0.25__tar.gz → 0.0.26__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.25 → tesorotools_python-0.0.26}/PKG-INFO +1 -1
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/pyproject.toml +1 -1
- tesorotools_python-0.0.26/src/tesorotools/artists/__init__.py +5 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/line_plot.py +131 -4
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/stacked.py +50 -6
- tesorotools_python-0.0.26/src/tesorotools/pipeline/diagnose.py +54 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/pipeline/rules.py +47 -3
- tesorotools_python-0.0.26/src/tesorotools/render/__init__.py +0 -0
- tesorotools_python-0.0.26/src/tesorotools/render/content/__init__.py +0 -0
- tesorotools_python-0.0.26/src/tesorotools/utils/config.py +98 -0
- tesorotools_python-0.0.25/src/tesorotools/__init__.py +0 -9
- tesorotools_python-0.0.25/src/tesorotools/artists/__init__.py +0 -9
- tesorotools_python-0.0.25/src/tesorotools/render/__init__.py +0 -17
- tesorotools_python-0.0.25/src/tesorotools/utils/config.py +0 -38
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/.gitignore +0 -0
- {tesorotools_python-0.0.25/src/tesorotools/data_sources → tesorotools_python-0.0.26/src/tesorotools}/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/barh.md +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/barh_plot.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/table.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/type_curve.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/plots.yaml +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/convert.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/data_sources/README.md +0 -0
- {tesorotools_python-0.0.25/src/tesorotools/dependencies → tesorotools_python-0.0.26/src/tesorotools/data_sources}/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/data_sources/lseg.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.0.25/src/tesorotools/offsets → tesorotools_python-0.0.26/src/tesorotools/dependencies}/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/main.py +0 -0
- {tesorotools_python-0.0.25/src/tesorotools/pipeline → tesorotools_python-0.0.26/src/tesorotools/offsets}/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.0.25/src/tesorotools/providers → tesorotools_python-0.0.26/src/tesorotools/pipeline}/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.0.25/src/tesorotools/render/content → tesorotools_python-0.0.26/src/tesorotools/providers}/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/utils/template.py +0 -0
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/line_plot.py
RENAMED
|
@@ -26,6 +26,76 @@ AX_CONFIG: dict[str, Any] = PLOT_CONFIG["ax"]
|
|
|
26
26
|
FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def adjust_figure_for_plot_size(
|
|
30
|
+
fig: Figure,
|
|
31
|
+
ax: Axes,
|
|
32
|
+
plot_size: tuple[float, float],
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Resize the figure so the axes area matches *plot_size*.
|
|
35
|
+
|
|
36
|
+
Call this **after** adding the legend. The figure
|
|
37
|
+
height grows to accommodate the legend while keeping
|
|
38
|
+
the plot area at the requested size.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
fig
|
|
43
|
+
The figure to resize.
|
|
44
|
+
ax
|
|
45
|
+
The axes whose area should match *plot_size*.
|
|
46
|
+
plot_size
|
|
47
|
+
Desired ``(width, height)`` of the axes area in
|
|
48
|
+
inches.
|
|
49
|
+
"""
|
|
50
|
+
fig.canvas.draw() # type: ignore[reportUnknownMemberType]
|
|
51
|
+
renderer = fig.canvas.get_renderer() # type: ignore[reportUnknownMemberType]
|
|
52
|
+
ax_bbox = ax.get_tightbbox(renderer) # type: ignore[reportUnknownArgumentType]
|
|
53
|
+
fig_bbox = fig.get_tightbbox(renderer) # type: ignore[reportUnknownArgumentType]
|
|
54
|
+
if ax_bbox is None or fig_bbox is None: # type: ignore[reportUnnecessaryComparison]
|
|
55
|
+
return
|
|
56
|
+
fig_w, fig_h = fig.get_size_inches()
|
|
57
|
+
ax_w_in = ax_bbox.width / fig.dpi
|
|
58
|
+
ax_h_in = ax_bbox.height / fig.dpi
|
|
59
|
+
if ax_w_in <= 0 or ax_h_in <= 0:
|
|
60
|
+
return
|
|
61
|
+
new_w = fig_w * (plot_size[0] / ax_w_in)
|
|
62
|
+
new_h = fig_h * (plot_size[1] / ax_h_in)
|
|
63
|
+
fig.set_size_inches(new_w, new_h)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def auto_ncol(ax: Axes, labels: list[str]) -> int:
|
|
67
|
+
"""Choose the maximum legend ncol that fits the axes.
|
|
68
|
+
|
|
69
|
+
Renders each label as a temporary Text to measure its
|
|
70
|
+
width, then packs as many columns as the axes width
|
|
71
|
+
allows, with padding for the legend handle + spacing.
|
|
72
|
+
Falls back to ``len(labels)`` (single row) when all
|
|
73
|
+
labels are short enough.
|
|
74
|
+
"""
|
|
75
|
+
fig = ax.get_figure()
|
|
76
|
+
if fig is None:
|
|
77
|
+
return len(labels)
|
|
78
|
+
renderer = fig.canvas.get_renderer() # type: ignore[reportUnknownMemberType]
|
|
79
|
+
ax_width_px = ax.get_window_extent(renderer).width # type: ignore[reportUnknownArgumentType]
|
|
80
|
+
|
|
81
|
+
# Measure widest label in pixels.
|
|
82
|
+
handle_pad_px = 40.0 # handle icon + spacing estimate
|
|
83
|
+
max_label_px = 0.0
|
|
84
|
+
for label in labels:
|
|
85
|
+
t = ax.text( # type: ignore[reportUnknownMemberType]
|
|
86
|
+
0, 0, label
|
|
87
|
+
)
|
|
88
|
+
bbox = t.get_window_extent(renderer) # type: ignore[reportUnknownArgumentType]
|
|
89
|
+
max_label_px = max(max_label_px, bbox.width)
|
|
90
|
+
t.remove()
|
|
91
|
+
|
|
92
|
+
col_width = max_label_px + handle_pad_px
|
|
93
|
+
if col_width <= 0:
|
|
94
|
+
return len(labels)
|
|
95
|
+
ncol = max(1, int(ax_width_px / col_width))
|
|
96
|
+
return min(ncol, len(labels))
|
|
97
|
+
|
|
98
|
+
|
|
29
99
|
def style_spines(
|
|
30
100
|
ax: Axes,
|
|
31
101
|
decimals: int,
|
|
@@ -62,6 +132,36 @@ def style_spines(
|
|
|
62
132
|
tick.set_markeredgecolor(color)
|
|
63
133
|
|
|
64
134
|
|
|
135
|
+
def export_legend(
|
|
136
|
+
ax: Axes,
|
|
137
|
+
out_path: Path,
|
|
138
|
+
*,
|
|
139
|
+
ncol: int = 5,
|
|
140
|
+
dpi: int = 500,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Save the legend of *ax* as a standalone PNG.
|
|
143
|
+
|
|
144
|
+
The plot's own legend is removed after export.
|
|
145
|
+
"""
|
|
146
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
147
|
+
fig_leg = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
148
|
+
figsize=(6, 0.5), dpi=dpi
|
|
149
|
+
)
|
|
150
|
+
fig_leg.legend( # type: ignore[reportUnknownMemberType]
|
|
151
|
+
handles,
|
|
152
|
+
labels,
|
|
153
|
+
loc="center",
|
|
154
|
+
ncol=ncol,
|
|
155
|
+
)
|
|
156
|
+
fig_leg.savefig( # type: ignore[reportUnknownMemberType]
|
|
157
|
+
out_path, bbox_inches="tight"
|
|
158
|
+
)
|
|
159
|
+
plt.close(fig_leg)
|
|
160
|
+
legend = ax.get_legend()
|
|
161
|
+
if legend is not None:
|
|
162
|
+
legend.remove()
|
|
163
|
+
|
|
164
|
+
|
|
65
165
|
def style_baseline(
|
|
66
166
|
ax: Axes,
|
|
67
167
|
reference: float = 0,
|
|
@@ -161,7 +261,11 @@ class Format:
|
|
|
161
261
|
|
|
162
262
|
|
|
163
263
|
class Legend:
|
|
164
|
-
def __init__(
|
|
264
|
+
def __init__(
|
|
265
|
+
self,
|
|
266
|
+
ncol: int | None = None,
|
|
267
|
+
sep: float = -0.125,
|
|
268
|
+
) -> None:
|
|
165
269
|
self.ncol = ncol
|
|
166
270
|
self.sep = sep
|
|
167
271
|
|
|
@@ -190,6 +294,9 @@ class LinePlot:
|
|
|
190
294
|
format: Format | None = None,
|
|
191
295
|
legend: Legend | None = None,
|
|
192
296
|
data: pd.DataFrame | None = None,
|
|
297
|
+
figsize: tuple[float, float] | None = None,
|
|
298
|
+
series_styles: dict[str, dict[str, Any]] | None = None,
|
|
299
|
+
plot_size: tuple[float, float] | None = None,
|
|
193
300
|
) -> None:
|
|
194
301
|
|
|
195
302
|
if out_path.suffix != ".png":
|
|
@@ -221,6 +328,9 @@ class LinePlot:
|
|
|
221
328
|
self.legend = legend
|
|
222
329
|
self.baseline = baseline
|
|
223
330
|
self.scale = scale
|
|
331
|
+
self.figsize = figsize
|
|
332
|
+
self.series_styles = series_styles or {}
|
|
333
|
+
self.plot_size = plot_size
|
|
224
334
|
|
|
225
335
|
@classmethod
|
|
226
336
|
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
@@ -255,11 +365,19 @@ class LinePlot:
|
|
|
255
365
|
if self.base_100: # maybe more flexible in the future
|
|
256
366
|
plot_data = plot_data / plot_data.iloc[0, :] * 100
|
|
257
367
|
|
|
368
|
+
fig_kw = dict(FIG_CONFIG)
|
|
369
|
+
if self.figsize is not None:
|
|
370
|
+
fig_kw["figsize"] = self.figsize
|
|
258
371
|
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
259
|
-
**
|
|
372
|
+
**fig_kw
|
|
260
373
|
)
|
|
261
374
|
ax = fig.add_subplot()
|
|
262
|
-
|
|
375
|
+
if self.series_styles:
|
|
376
|
+
for col in plot_data.columns:
|
|
377
|
+
style = self.series_styles.get(col, {})
|
|
378
|
+
plot_data[col].plot(ax=ax, label=col, **style)
|
|
379
|
+
else:
|
|
380
|
+
plot_data.plot(ax=ax)
|
|
263
381
|
|
|
264
382
|
if self.annotate: # not implemented yet
|
|
265
383
|
pass
|
|
@@ -276,19 +394,28 @@ class LinePlot:
|
|
|
276
394
|
style_baseline(ax, reference, **AX_CONFIG["baseline"])
|
|
277
395
|
|
|
278
396
|
if self.legend is not None:
|
|
397
|
+
labels = list(plot_data.columns)
|
|
398
|
+
ncol = (
|
|
399
|
+
self.legend.ncol
|
|
400
|
+
if self.legend.ncol is not None
|
|
401
|
+
else auto_ncol(ax, labels)
|
|
402
|
+
)
|
|
279
403
|
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
280
404
|
loc="upper center",
|
|
281
405
|
bbox_to_anchor=(
|
|
282
406
|
0.5,
|
|
283
407
|
LINE_PLOT_CONFIG["legend_sep"],
|
|
284
408
|
),
|
|
285
|
-
ncol=
|
|
409
|
+
ncol=ncol,
|
|
286
410
|
)
|
|
287
411
|
else:
|
|
288
412
|
ax.legend().set_visible( # type: ignore[reportUnknownMemberType]
|
|
289
413
|
False
|
|
290
414
|
)
|
|
291
415
|
|
|
416
|
+
if self.plot_size is not None:
|
|
417
|
+
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
|
418
|
+
|
|
292
419
|
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
293
420
|
self.out_path
|
|
294
421
|
)
|
|
@@ -15,12 +15,13 @@ from tesorotools.artists.line_plot import (
|
|
|
15
15
|
FIG_CONFIG,
|
|
16
16
|
Format,
|
|
17
17
|
Legend,
|
|
18
|
+
adjust_figure_for_plot_size,
|
|
19
|
+
auto_ncol,
|
|
18
20
|
style_baseline,
|
|
19
21
|
style_spines,
|
|
20
22
|
)
|
|
21
23
|
from tesorotools.utils.config import TemplateLoader
|
|
22
24
|
|
|
23
|
-
_DEFAULT_NCOL = 5
|
|
24
25
|
_DEFAULT_SEP = -0.125
|
|
25
26
|
|
|
26
27
|
|
|
@@ -44,6 +45,8 @@ class StackedAreaPlot:
|
|
|
44
45
|
baseline: bool = False,
|
|
45
46
|
format: Format | None = None,
|
|
46
47
|
legend: Legend | None = None,
|
|
48
|
+
figsize: tuple[float, float] | None = None,
|
|
49
|
+
plot_size: tuple[float, float] | None = None,
|
|
47
50
|
) -> None:
|
|
48
51
|
if out_path.suffix != ".png":
|
|
49
52
|
raise ValueError(f"out_path must be .png: {out_path}")
|
|
@@ -56,6 +59,8 @@ class StackedAreaPlot:
|
|
|
56
59
|
self.baseline = baseline
|
|
57
60
|
self.format = format or Format()
|
|
58
61
|
self.legend = legend
|
|
62
|
+
self.figsize = figsize
|
|
63
|
+
self.plot_size = plot_size
|
|
59
64
|
|
|
60
65
|
@classmethod
|
|
61
66
|
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
@@ -82,8 +87,11 @@ class StackedAreaPlot:
|
|
|
82
87
|
plot_data = self.data.loc[start:end, list(self.series.keys())].dropna()
|
|
83
88
|
plot_data = plot_data * self.scale
|
|
84
89
|
|
|
90
|
+
fig_kw = dict(FIG_CONFIG)
|
|
91
|
+
if self.figsize is not None:
|
|
92
|
+
fig_kw["figsize"] = self.figsize
|
|
85
93
|
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
86
|
-
**
|
|
94
|
+
**fig_kw
|
|
87
95
|
)
|
|
88
96
|
ax: Axes = fig.add_subplot()
|
|
89
97
|
|
|
@@ -107,7 +115,8 @@ class StackedAreaPlot:
|
|
|
107
115
|
if self.baseline:
|
|
108
116
|
style_baseline(ax, 0, **AX_CONFIG["baseline"])
|
|
109
117
|
|
|
110
|
-
|
|
118
|
+
legend_ncol = self.legend.ncol if self.legend else None
|
|
119
|
+
ncol = legend_ncol if legend_ncol is not None else auto_ncol(ax, labels)
|
|
111
120
|
sep = self.legend.sep if self.legend else _DEFAULT_SEP
|
|
112
121
|
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
113
122
|
loc="upper center",
|
|
@@ -115,6 +124,9 @@ class StackedAreaPlot:
|
|
|
115
124
|
ncol=ncol,
|
|
116
125
|
)
|
|
117
126
|
|
|
127
|
+
if self.plot_size is not None:
|
|
128
|
+
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
|
129
|
+
|
|
118
130
|
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
119
131
|
self.out_path
|
|
120
132
|
)
|
|
@@ -141,6 +153,9 @@ class StackedBarPlot:
|
|
|
141
153
|
baseline: bool = True,
|
|
142
154
|
format: Format | None = None,
|
|
143
155
|
legend: Legend | None = None,
|
|
156
|
+
figsize: tuple[float, float] | None = None,
|
|
157
|
+
overlay_series: dict[str, str] | None = None,
|
|
158
|
+
plot_size: tuple[float, float] | None = None,
|
|
144
159
|
) -> None:
|
|
145
160
|
if out_path.suffix != ".png":
|
|
146
161
|
raise ValueError(f"out_path must be .png: {out_path}")
|
|
@@ -153,6 +168,9 @@ class StackedBarPlot:
|
|
|
153
168
|
self.baseline = baseline
|
|
154
169
|
self.format = format or Format()
|
|
155
170
|
self.legend = legend
|
|
171
|
+
self.plot_size = plot_size
|
|
172
|
+
self.figsize = figsize
|
|
173
|
+
self.overlay_series = overlay_series or {}
|
|
156
174
|
|
|
157
175
|
@classmethod
|
|
158
176
|
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
@@ -176,11 +194,15 @@ class StackedBarPlot:
|
|
|
176
194
|
else self.data.index.max()
|
|
177
195
|
)
|
|
178
196
|
|
|
179
|
-
|
|
197
|
+
all_cols = list(self.series.keys()) + list(self.overlay_series.keys())
|
|
198
|
+
plot_data = self.data.loc[start:end, all_cols].dropna()
|
|
180
199
|
plot_data = plot_data * self.scale
|
|
181
200
|
|
|
201
|
+
fig_kw = dict(FIG_CONFIG)
|
|
202
|
+
if self.figsize is not None:
|
|
203
|
+
fig_kw["figsize"] = self.figsize
|
|
182
204
|
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
183
|
-
|
|
205
|
+
**fig_kw
|
|
184
206
|
)
|
|
185
207
|
ax: Axes = fig.add_subplot()
|
|
186
208
|
|
|
@@ -229,6 +251,17 @@ class StackedBarPlot:
|
|
|
229
251
|
pos_bottom = pos_bottom + pos
|
|
230
252
|
neg_bottom = neg_bottom + neg
|
|
231
253
|
|
|
254
|
+
for o_col, o_label in self.overlay_series.items():
|
|
255
|
+
o_vals = plot_data[o_col].to_numpy(dtype=np.float64)
|
|
256
|
+
ax.plot( # type: ignore[reportUnknownMemberType]
|
|
257
|
+
x,
|
|
258
|
+
o_vals,
|
|
259
|
+
color="black",
|
|
260
|
+
linewidth=1.5,
|
|
261
|
+
label=o_label,
|
|
262
|
+
zorder=10,
|
|
263
|
+
)
|
|
264
|
+
|
|
232
265
|
dates = plot_data.index
|
|
233
266
|
step = max(1, len(dates) // 12)
|
|
234
267
|
tick_pos = list(range(0, len(dates), step))
|
|
@@ -249,7 +282,15 @@ class StackedBarPlot:
|
|
|
249
282
|
if self.baseline:
|
|
250
283
|
style_baseline(ax, 0, **AX_CONFIG["baseline"])
|
|
251
284
|
|
|
252
|
-
|
|
285
|
+
all_labels = list(self.series.values()) + list(
|
|
286
|
+
self.overlay_series.values()
|
|
287
|
+
)
|
|
288
|
+
legend_ncol = self.legend.ncol if self.legend else None
|
|
289
|
+
ncol = (
|
|
290
|
+
legend_ncol
|
|
291
|
+
if legend_ncol is not None
|
|
292
|
+
else auto_ncol(ax, all_labels)
|
|
293
|
+
)
|
|
253
294
|
sep = self.legend.sep if self.legend else _DEFAULT_SEP
|
|
254
295
|
ax.legend( # type: ignore[reportUnknownMemberType]
|
|
255
296
|
loc="upper center",
|
|
@@ -257,6 +298,9 @@ class StackedBarPlot:
|
|
|
257
298
|
ncol=ncol,
|
|
258
299
|
)
|
|
259
300
|
|
|
301
|
+
if self.plot_size is not None:
|
|
302
|
+
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
|
303
|
+
|
|
260
304
|
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
261
305
|
self.out_path
|
|
262
306
|
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Diagnostic utilities for pipeline configuration.
|
|
2
|
+
|
|
3
|
+
Helps identify unused series in a catalog — series that
|
|
4
|
+
are downloaded but never appear as a dependency of any
|
|
5
|
+
rule or in any chart configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from tesorotools.dependencies.resolution import (
|
|
13
|
+
collect_document_series,
|
|
14
|
+
)
|
|
15
|
+
from tesorotools.pipeline.engine import TransformationRule
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def unused_series(
|
|
19
|
+
catalog: dict[str, Any],
|
|
20
|
+
rules: list[TransformationRule],
|
|
21
|
+
charts: dict[str, Any] | list[dict[str, Any]] | None = None,
|
|
22
|
+
) -> set[str]:
|
|
23
|
+
"""Find catalog series not referenced by rules or charts.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
catalog
|
|
28
|
+
Instrument catalog (canonical_id -> metadata).
|
|
29
|
+
rules
|
|
30
|
+
All transformation rules for the project.
|
|
31
|
+
charts
|
|
32
|
+
Chart config dict(s). Can be a single dict or a
|
|
33
|
+
list of dicts (barh, line, type_curve, tables).
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
set[str]
|
|
38
|
+
Canonical IDs present in the catalog but not used
|
|
39
|
+
as a dependency of any rule nor referenced in any
|
|
40
|
+
chart config.
|
|
41
|
+
"""
|
|
42
|
+
all_ids = set(catalog.keys())
|
|
43
|
+
|
|
44
|
+
used: set[str] = set()
|
|
45
|
+
for rule in rules:
|
|
46
|
+
used.add(rule.output_name)
|
|
47
|
+
used.update(rule.dependencies)
|
|
48
|
+
|
|
49
|
+
if charts is not None:
|
|
50
|
+
chart_list = charts if isinstance(charts, list) else [charts]
|
|
51
|
+
for name in collect_document_series(chart_list):
|
|
52
|
+
used.add(name)
|
|
53
|
+
|
|
54
|
+
return all_ids - used
|
|
@@ -91,8 +91,10 @@ def inverse_rule(
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def
|
|
95
|
-
|
|
94
|
+
def pct_change_rule(
|
|
95
|
+
output: str, source: str, periods: int = 12
|
|
96
|
+
) -> TransformationRule:
|
|
97
|
+
"""Periodic percentage change: source_t / source_{t-N} - 1.
|
|
96
98
|
|
|
97
99
|
Drops NaN before shifting so that *periods* counts
|
|
98
100
|
actual observations, not DataFrame rows.
|
|
@@ -137,6 +139,46 @@ def rolling_sum_rule(
|
|
|
137
139
|
)
|
|
138
140
|
|
|
139
141
|
|
|
142
|
+
def delta_rule(
|
|
143
|
+
output: str, source: str, periods: int = 1
|
|
144
|
+
) -> TransformationRule:
|
|
145
|
+
"""Level change: source_t - source_{t-periods}.
|
|
146
|
+
|
|
147
|
+
Drops NaN before shifting (mixed-frequency safe).
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def _compute(
|
|
151
|
+
df: pd.DataFrame,
|
|
152
|
+
s: str = source,
|
|
153
|
+
p: int = periods,
|
|
154
|
+
) -> pd.Series[float]:
|
|
155
|
+
clean: pd.Series[float] = df[s].dropna()
|
|
156
|
+
return clean - clean.shift(p)
|
|
157
|
+
|
|
158
|
+
return TransformationRule(
|
|
159
|
+
output_name=output,
|
|
160
|
+
dependencies=[source],
|
|
161
|
+
compute=_compute,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cumsum_rule(output: str, source: str) -> TransformationRule:
|
|
166
|
+
"""Cumulative sum of a column.
|
|
167
|
+
|
|
168
|
+
Drops NaN before cumsum (mixed-frequency safe).
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def _compute(df: pd.DataFrame, s: str = source) -> pd.Series[float]:
|
|
172
|
+
clean: pd.Series[float] = df[s].dropna()
|
|
173
|
+
return clean.cumsum()
|
|
174
|
+
|
|
175
|
+
return TransformationRule(
|
|
176
|
+
output_name=output,
|
|
177
|
+
dependencies=[source],
|
|
178
|
+
compute=_compute,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
140
182
|
#: Registry of factory functions, keyed by YAML function name.
|
|
141
183
|
#: Projects can add custom factories at runtime.
|
|
142
184
|
FACTORIES: dict[
|
|
@@ -148,6 +190,8 @@ FACTORIES: dict[
|
|
|
148
190
|
"ratio": ratio_rule,
|
|
149
191
|
"difference": difference_rule,
|
|
150
192
|
"inverse": inverse_rule,
|
|
151
|
-
"
|
|
193
|
+
"pct_change": pct_change_rule,
|
|
152
194
|
"rolling_sum": rolling_sum_rule,
|
|
195
|
+
"delta": delta_rule,
|
|
196
|
+
"cumsum": cumsum_rule,
|
|
153
197
|
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from tesorotools.utils.template import TemplateLoader
|
|
7
|
+
|
|
8
|
+
_tags_registered = False
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _register_all_tags() -> None:
|
|
12
|
+
"""Register every YAML tag on TemplateLoader.
|
|
13
|
+
|
|
14
|
+
Called once, lazily, the first time ``read_config`` is
|
|
15
|
+
invoked with ``loader=TemplateLoader``. This removes
|
|
16
|
+
the need for consumers to import ``tesorotools.render``
|
|
17
|
+
or ``tesorotools.artists`` just for their side effects.
|
|
18
|
+
"""
|
|
19
|
+
global _tags_registered # noqa: PLW0603
|
|
20
|
+
if _tags_registered:
|
|
21
|
+
return
|
|
22
|
+
_tags_registered = True
|
|
23
|
+
|
|
24
|
+
# -- artists tags --
|
|
25
|
+
from tesorotools.artists.barh_plot import (
|
|
26
|
+
HorizontalBarChart,
|
|
27
|
+
)
|
|
28
|
+
from tesorotools.artists.line_plot import (
|
|
29
|
+
Format,
|
|
30
|
+
Legend,
|
|
31
|
+
LinePlot,
|
|
32
|
+
)
|
|
33
|
+
from tesorotools.artists.stacked import (
|
|
34
|
+
StackedAreaPlot,
|
|
35
|
+
StackedBarPlot,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
TemplateLoader.add_constructor("!line_plot", LinePlot.from_yaml)
|
|
39
|
+
TemplateLoader.add_constructor("!format", Format.from_yaml)
|
|
40
|
+
TemplateLoader.add_constructor("!legend", Legend.from_yaml)
|
|
41
|
+
TemplateLoader.add_constructor("!stacked_area", StackedAreaPlot.from_yaml)
|
|
42
|
+
TemplateLoader.add_constructor("!stacked_bar", StackedBarPlot.from_yaml)
|
|
43
|
+
TemplateLoader.add_constructor("!barh", HorizontalBarChart.from_yaml)
|
|
44
|
+
|
|
45
|
+
# -- render tags --
|
|
46
|
+
from tesorotools.render.content.images import (
|
|
47
|
+
Image,
|
|
48
|
+
Images,
|
|
49
|
+
)
|
|
50
|
+
from tesorotools.render.content.section import Section
|
|
51
|
+
from tesorotools.render.content.subtitle import Subtitle
|
|
52
|
+
from tesorotools.render.content.table import Table
|
|
53
|
+
from tesorotools.render.content.text import Text
|
|
54
|
+
from tesorotools.render.content.title import Title
|
|
55
|
+
from tesorotools.render.report import Report
|
|
56
|
+
|
|
57
|
+
TemplateLoader.add_constructor("!report", Report.from_yaml)
|
|
58
|
+
TemplateLoader.add_constructor("!section", Section.from_yaml)
|
|
59
|
+
TemplateLoader.add_constructor("!image", Image.from_yaml)
|
|
60
|
+
TemplateLoader.add_constructor("!images", Images.from_yaml)
|
|
61
|
+
TemplateLoader.add_constructor("!table", Table.from_yaml)
|
|
62
|
+
TemplateLoader.add_constructor("!text", Text.from_yaml)
|
|
63
|
+
TemplateLoader.add_constructor("!title", Title.from_yaml)
|
|
64
|
+
TemplateLoader.add_constructor("!subtitle", Subtitle.from_yaml)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def clean_config_dicts(
|
|
68
|
+
config_dicts: dict[str, Any],
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
return {k: v for k, v in config_dicts.items() if not k.startswith(".")}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_config(
|
|
74
|
+
config_file: Path,
|
|
75
|
+
loader: type[yaml.FullLoader] | None = None,
|
|
76
|
+
clean: bool = True,
|
|
77
|
+
) -> Any:
|
|
78
|
+
actual_loader: type[yaml.FullLoader] = (
|
|
79
|
+
yaml.FullLoader if loader is None else TemplateLoader
|
|
80
|
+
)
|
|
81
|
+
if actual_loader is TemplateLoader:
|
|
82
|
+
_register_all_tags()
|
|
83
|
+
with open(config_file, encoding="utf8") as file:
|
|
84
|
+
config_dict: Any = yaml.load(file, Loader=actual_loader)
|
|
85
|
+
if clean and isinstance(config_dict, dict):
|
|
86
|
+
config_dict = clean_config_dicts(config_dict) # type: ignore[reportUnknownArgumentType]
|
|
87
|
+
return config_dict
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def merge(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
|
91
|
+
# a overrides
|
|
92
|
+
for key in b:
|
|
93
|
+
if key in a:
|
|
94
|
+
if isinstance(a[key], dict) and isinstance(b[key], dict):
|
|
95
|
+
merge(a[key], b[key])
|
|
96
|
+
else:
|
|
97
|
+
a[key] = b[key]
|
|
98
|
+
return a
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from tesorotools.artists.line_plot import Format, Legend, LinePlot
|
|
2
|
-
from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
|
|
3
|
-
from tesorotools.utils.config import TemplateLoader
|
|
4
|
-
|
|
5
|
-
TemplateLoader.add_constructor("!line_plot", LinePlot.from_yaml)
|
|
6
|
-
TemplateLoader.add_constructor("!format", Format.from_yaml)
|
|
7
|
-
TemplateLoader.add_constructor("!legend", Legend.from_yaml)
|
|
8
|
-
TemplateLoader.add_constructor("!stacked_area", StackedAreaPlot.from_yaml)
|
|
9
|
-
TemplateLoader.add_constructor("!stacked_bar", StackedBarPlot.from_yaml)
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import matplotlib.style
|
|
2
|
-
|
|
3
|
-
from tesorotools.artists.barh_plot import HorizontalBarChart
|
|
4
|
-
from tesorotools.utils.config import TemplateLoader
|
|
5
|
-
|
|
6
|
-
from ..utils.globals import STYLE_SHEET
|
|
7
|
-
|
|
8
|
-
matplotlib.style.use(STYLE_SHEET)
|
|
9
|
-
TemplateLoader.add_constructor("!barh", HorizontalBarChart.from_yaml)
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from tesorotools.render.content.images import Image, Images
|
|
2
|
-
from tesorotools.render.content.section import Section
|
|
3
|
-
from tesorotools.render.content.subtitle import Subtitle
|
|
4
|
-
from tesorotools.render.content.table import Table
|
|
5
|
-
from tesorotools.render.content.text import Text
|
|
6
|
-
from tesorotools.render.content.title import Title
|
|
7
|
-
from tesorotools.render.report import Report
|
|
8
|
-
from tesorotools.utils.template import TemplateLoader
|
|
9
|
-
|
|
10
|
-
TemplateLoader.add_constructor("!report", Report.from_yaml)
|
|
11
|
-
TemplateLoader.add_constructor("!section", Section.from_yaml)
|
|
12
|
-
TemplateLoader.add_constructor("!image", Image.from_yaml)
|
|
13
|
-
TemplateLoader.add_constructor("!images", Images.from_yaml)
|
|
14
|
-
TemplateLoader.add_constructor("!table", Table.from_yaml)
|
|
15
|
-
TemplateLoader.add_constructor("!text", Text.from_yaml)
|
|
16
|
-
TemplateLoader.add_constructor("!title", Title.from_yaml)
|
|
17
|
-
TemplateLoader.add_constructor("!subtitle", Subtitle.from_yaml)
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
import yaml
|
|
5
|
-
|
|
6
|
-
from tesorotools.utils.template import TemplateLoader
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def clean_config_dicts(
|
|
10
|
-
config_dicts: dict[str, Any],
|
|
11
|
-
) -> dict[str, Any]:
|
|
12
|
-
return {k: v for k, v in config_dicts.items() if not k.startswith(".")}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def read_config(
|
|
16
|
-
config_file: Path,
|
|
17
|
-
loader: type[yaml.FullLoader] | None = None,
|
|
18
|
-
clean: bool = True,
|
|
19
|
-
) -> Any:
|
|
20
|
-
actual_loader: type[yaml.FullLoader] = (
|
|
21
|
-
yaml.FullLoader if loader is None else TemplateLoader
|
|
22
|
-
)
|
|
23
|
-
with open(config_file, encoding="utf8") as file:
|
|
24
|
-
config_dict: Any = yaml.load(file, Loader=actual_loader)
|
|
25
|
-
if clean and isinstance(config_dict, dict):
|
|
26
|
-
config_dict = clean_config_dicts(config_dict) # type: ignore[reportUnknownArgumentType]
|
|
27
|
-
return config_dict
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def merge(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
-
# a overrides
|
|
32
|
-
for key in b:
|
|
33
|
-
if key in a:
|
|
34
|
-
if isinstance(a[key], dict) and isinstance(b[key], dict):
|
|
35
|
-
merge(a[key], b[key])
|
|
36
|
-
else:
|
|
37
|
-
a[key] = b[key]
|
|
38
|
-
return a
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/barh_plot.py
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/artists/type_curve.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/fonts/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/assets/tesoro.mplstyle
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/data_sources/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/data_sources/debug.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/data_sources/lseg.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/database/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/dependencies/node.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/dependencies/resolution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/content.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/images.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/section.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/subtitle.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/table.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/text.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.25 → tesorotools_python-0.0.26}/src/tesorotools/render/content/title.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|