tesorotools-python 0.0.33__tar.gz → 0.0.34__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.33 → tesorotools_python-0.0.34}/PKG-INFO +1 -1
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/pyproject.toml +1 -1
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/line_plot.py +80 -5
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/table.py +255 -225
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/images.py +159 -152
- tesorotools_python-0.0.34/src/tesorotools/testing/__init__.py +5 -0
- tesorotools_python-0.0.34/src/tesorotools/testing/compare.py +147 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/format.py +21 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/.gitignore +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/barh.md +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/barh_plot.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/stacked.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/type_curve.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/plots.yaml +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/convert.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/main.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/utils/template.py +0 -0
{tesorotools_python-0.0.33 → tesorotools_python-0.0.34}/src/tesorotools/artists/line_plot.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import locale
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any, Self
|
|
4
|
+
from typing import Any, Self, cast
|
|
5
5
|
|
|
6
6
|
import matplotlib.pyplot as plt
|
|
7
7
|
import pandas as pd
|
|
@@ -218,6 +218,27 @@ def annotate_last_values(
|
|
|
218
218
|
ax.set_xlim(xmin, xmax + (x1 - x0))
|
|
219
219
|
|
|
220
220
|
|
|
221
|
+
def draw_vlines(ax: Axes, vlines: list[dict[str, Any]]) -> None:
|
|
222
|
+
"""Draw labelled vertical event markers on *ax*.
|
|
223
|
+
|
|
224
|
+
Each entry must provide ``x`` (date-like). Optional keys
|
|
225
|
+
(``label``, ``color``, ``linestyle``, ``linewidth``, ...) are
|
|
226
|
+
forwarded to ``ax.axvline``. ``linestyle`` defaults to
|
|
227
|
+
``"dashed"`` so markers are visually distinct from data lines.
|
|
228
|
+
"""
|
|
229
|
+
for vline in vlines:
|
|
230
|
+
kwargs: dict[str, Any] = dict(vline)
|
|
231
|
+
x_raw: Any = kwargs.pop("x")
|
|
232
|
+
# matplotlib's stub types axvline's x as float, but at runtime
|
|
233
|
+
# it accepts any date-like recognised by the date converter.
|
|
234
|
+
x_dt = cast(Any, pd.to_datetime(x_raw)) # type: ignore[reportUnknownMemberType]
|
|
235
|
+
kwargs.setdefault("linestyle", "dashed")
|
|
236
|
+
ax.axvline( # type: ignore[reportUnknownMemberType]
|
|
237
|
+
x=x_dt,
|
|
238
|
+
**kwargs,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
221
242
|
def style_spines(
|
|
222
243
|
ax: Axes,
|
|
223
244
|
decimals: int,
|
|
@@ -411,6 +432,7 @@ class LinePlot:
|
|
|
411
432
|
start_date: datetime.datetime | None = None,
|
|
412
433
|
end_date: datetime.datetime | None = None,
|
|
413
434
|
base_100: bool = False,
|
|
435
|
+
base_100_date: datetime.datetime | str | None = None,
|
|
414
436
|
annotate: bool = False,
|
|
415
437
|
annotate_color: str | None = None,
|
|
416
438
|
baseline: bool = False,
|
|
@@ -420,6 +442,7 @@ class LinePlot:
|
|
|
420
442
|
figsize: tuple[float, float] | None = None,
|
|
421
443
|
series_styles: dict[str, dict[str, Any]] | None = None,
|
|
422
444
|
plot_size: tuple[float, float] | None = None,
|
|
445
|
+
vlines: list[dict[str, Any]] | None = None,
|
|
423
446
|
) -> None:
|
|
424
447
|
|
|
425
448
|
if out_path.suffix != ".png":
|
|
@@ -443,6 +466,7 @@ class LinePlot:
|
|
|
443
466
|
raise ValueError("series is required")
|
|
444
467
|
|
|
445
468
|
self.base_100 = base_100
|
|
469
|
+
self.base_100_date = base_100_date
|
|
446
470
|
self.annotate = annotate
|
|
447
471
|
self.annotate_color = annotate_color
|
|
448
472
|
self.format = format
|
|
@@ -455,6 +479,7 @@ class LinePlot:
|
|
|
455
479
|
self.figsize = figsize
|
|
456
480
|
self.series_styles = series_styles or {}
|
|
457
481
|
self.plot_size = plot_size
|
|
482
|
+
self.vlines = vlines or []
|
|
458
483
|
|
|
459
484
|
@classmethod
|
|
460
485
|
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
@@ -466,7 +491,13 @@ class LinePlot:
|
|
|
466
491
|
line_plot_cfg["data_path"] = Path(line_plot_cfg["data_path"])
|
|
467
492
|
return cls(**line_plot_cfg)
|
|
468
493
|
|
|
469
|
-
def
|
|
494
|
+
def build(self) -> tuple[Figure, Axes]:
|
|
495
|
+
"""Render the chart in memory without writing to disk.
|
|
496
|
+
|
|
497
|
+
Returns the ``(Figure, Axes)`` so callers can post-process
|
|
498
|
+
before saving (extra annotations, override DPI, embed in a
|
|
499
|
+
composite figure). Pair with :meth:`save` to persist.
|
|
500
|
+
"""
|
|
470
501
|
start_date: pd.Timestamp = (
|
|
471
502
|
self.data.index.min()
|
|
472
503
|
if self.start_date is None
|
|
@@ -485,8 +516,19 @@ class LinePlot:
|
|
|
485
516
|
|
|
486
517
|
plot_data = plot_data * self.scale
|
|
487
518
|
|
|
488
|
-
if self.base_100:
|
|
489
|
-
|
|
519
|
+
if self.base_100:
|
|
520
|
+
if self.base_100_date is None:
|
|
521
|
+
anchor: pd.Series[float] = plot_data.iloc[0, :]
|
|
522
|
+
else:
|
|
523
|
+
anchor_ts: pd.Timestamp = pd.to_datetime(self.base_100_date)
|
|
524
|
+
idx_pos: int = self.data.index.get_indexer(
|
|
525
|
+
pd.Index([anchor_ts]), method="nearest"
|
|
526
|
+
)[0]
|
|
527
|
+
anchor = (
|
|
528
|
+
self.data.iloc[idx_pos, :].loc[list(self.series.keys())]
|
|
529
|
+
* self.scale
|
|
530
|
+
)
|
|
531
|
+
plot_data = plot_data / anchor * 100
|
|
490
532
|
|
|
491
533
|
fig_kw = dict(FIG_CONFIG)
|
|
492
534
|
if self.figsize is not None:
|
|
@@ -500,6 +542,9 @@ class LinePlot:
|
|
|
500
542
|
style = styles.get(col, {}) if styles else {}
|
|
501
543
|
plot_data[col].plot(ax=ax, label=self.series[col], **style)
|
|
502
544
|
|
|
545
|
+
if self.vlines:
|
|
546
|
+
draw_vlines(ax, self.vlines)
|
|
547
|
+
|
|
503
548
|
assert self.format is not None
|
|
504
549
|
if self.annotate:
|
|
505
550
|
annotate_last_values(
|
|
@@ -547,7 +592,37 @@ class LinePlot:
|
|
|
547
592
|
if self.plot_size is not None:
|
|
548
593
|
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
|
549
594
|
|
|
595
|
+
return fig, ax
|
|
596
|
+
|
|
597
|
+
def save(
|
|
598
|
+
self,
|
|
599
|
+
fig: Figure,
|
|
600
|
+
*,
|
|
601
|
+
path: Path | None = None,
|
|
602
|
+
dpi: int | None = None,
|
|
603
|
+
) -> Path:
|
|
604
|
+
"""Persist *fig* as a PNG. Returns the path written.
|
|
605
|
+
|
|
606
|
+
Defaults to ``self.out_path``; pass ``path`` to redirect or
|
|
607
|
+
``dpi`` to override the figure DPI for the saved file
|
|
608
|
+
(useful when embedding in Word at fixed widths, where the
|
|
609
|
+
default 500 dpi balloons file sizes).
|
|
610
|
+
"""
|
|
611
|
+
target: Path = path if path is not None else self.out_path
|
|
612
|
+
save_kwargs: dict[str, Any] = {}
|
|
613
|
+
if dpi is not None:
|
|
614
|
+
save_kwargs["dpi"] = dpi
|
|
550
615
|
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
551
|
-
|
|
616
|
+
target, **save_kwargs
|
|
552
617
|
)
|
|
618
|
+
return target
|
|
619
|
+
|
|
620
|
+
def plot(self) -> Axes:
|
|
621
|
+
"""Build the chart and persist it to ``self.out_path``.
|
|
622
|
+
|
|
623
|
+
Kept for backwards compatibility; new callers should prefer
|
|
624
|
+
:meth:`build` + :meth:`save` for finer control.
|
|
625
|
+
"""
|
|
626
|
+
fig, ax = self.build()
|
|
627
|
+
self.save(fig)
|
|
553
628
|
return ax
|
|
@@ -1,225 +1,255 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from math import floor
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import pandas as pd
|
|
8
|
-
|
|
9
|
-
from tesorotools.dependencies.resolution import collect_series
|
|
10
|
-
from tesorotools.offsets.outliers import flag_outliers
|
|
11
|
-
from tesorotools.utils.matplotlib import format_annotation, is_zero
|
|
12
|
-
|
|
13
|
-
# this file is by far the worst and most spaghettified, must be rewritten
|
|
14
|
-
|
|
15
|
-
# to global config
|
|
16
|
-
GOOD: str = "00c800"
|
|
17
|
-
BAD: str = "c80000"
|
|
18
|
-
THRESHOLD: float = 1
|
|
19
|
-
SHADE_LEVELS: int = 2
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _shade_intensity(
|
|
23
|
-
ratio: float,
|
|
24
|
-
shade_levels: int = 2,
|
|
25
|
-
continuous: bool = False,
|
|
26
|
-
) -> str:
|
|
27
|
-
# intensity may vary from 150 (highest) to 255 (lowest), a grand difference of 105
|
|
28
|
-
# there are SHADE_LEVELS levels, so increments will be of 105/SHADE_LEVELS
|
|
29
|
-
corrected_ratio: float = min(ratio, shade_levels)
|
|
30
|
-
corrected_ratio = (
|
|
31
|
-
floor(corrected_ratio) if not continuous else corrected_ratio
|
|
32
|
-
)
|
|
33
|
-
increment: float = (corrected_ratio - 1) * (105 / shade_levels)
|
|
34
|
-
intensity: float = 255 - increment
|
|
35
|
-
intensity_hex: str = f"{int(intensity):x}"
|
|
36
|
-
return intensity_hex
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
]
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
outliers_flags
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from math import floor
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from tesorotools.dependencies.resolution import collect_series
|
|
10
|
+
from tesorotools.offsets.outliers import flag_outliers
|
|
11
|
+
from tesorotools.utils.matplotlib import format_annotation, is_zero
|
|
12
|
+
|
|
13
|
+
# this file is by far the worst and most spaghettified, must be rewritten
|
|
14
|
+
|
|
15
|
+
# to global config
|
|
16
|
+
GOOD: str = "00c800"
|
|
17
|
+
BAD: str = "c80000"
|
|
18
|
+
THRESHOLD: float = 1
|
|
19
|
+
SHADE_LEVELS: int = 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _shade_intensity(
|
|
23
|
+
ratio: float,
|
|
24
|
+
shade_levels: int = 2,
|
|
25
|
+
continuous: bool = False,
|
|
26
|
+
) -> str:
|
|
27
|
+
# intensity may vary from 150 (highest) to 255 (lowest), a grand difference of 105
|
|
28
|
+
# there are SHADE_LEVELS levels, so increments will be of 105/SHADE_LEVELS
|
|
29
|
+
corrected_ratio: float = min(ratio, shade_levels)
|
|
30
|
+
corrected_ratio = (
|
|
31
|
+
floor(corrected_ratio) if not continuous else corrected_ratio
|
|
32
|
+
)
|
|
33
|
+
increment: float = (corrected_ratio - 1) * (105 / shade_levels)
|
|
34
|
+
intensity: float = 255 - increment
|
|
35
|
+
intensity_hex: str = f"{int(intensity):x}"
|
|
36
|
+
return intensity_hex
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_shade_fn(
|
|
40
|
+
levels: int = 2,
|
|
41
|
+
*,
|
|
42
|
+
cap: float | None = None,
|
|
43
|
+
continuous: bool = False,
|
|
44
|
+
) -> Callable[[float], str]:
|
|
45
|
+
"""Build a closure that maps an outlier ratio to a hex intensity.
|
|
46
|
+
|
|
47
|
+
``levels`` controls how many discrete steps the intensity has
|
|
48
|
+
between 255 (lightest) and 150 (darkest); ``cap`` is the ratio at
|
|
49
|
+
which intensity saturates (defaults to ``levels``); ``continuous``
|
|
50
|
+
skips the discretisation step and yields a smooth gradient.
|
|
51
|
+
|
|
52
|
+
The returned callable is suitable as a per-cell shade resolver in
|
|
53
|
+
a custom rendering pipeline that does not go through the
|
|
54
|
+
``_generate_column`` cube path.
|
|
55
|
+
"""
|
|
56
|
+
cap_value: float = float(levels if cap is None else cap)
|
|
57
|
+
|
|
58
|
+
def _shade(ratio: float) -> str:
|
|
59
|
+
corrected: float = min(ratio, cap_value)
|
|
60
|
+
if not continuous:
|
|
61
|
+
corrected = float(floor(corrected))
|
|
62
|
+
increment: float = (corrected - 1) * (105 / levels)
|
|
63
|
+
intensity: float = 255 - increment
|
|
64
|
+
return f"{int(intensity):x}"
|
|
65
|
+
|
|
66
|
+
return _shade
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _generate_column(
|
|
70
|
+
column_data: pd.Series[Any],
|
|
71
|
+
column_cfg: dict[str, Any],
|
|
72
|
+
outliers_flags: pd.Series[Any] | None = None,
|
|
73
|
+
) -> tuple[pd.Series[Any], pd.Series[Any], pd.Series[Any]]:
|
|
74
|
+
# TODO: factor out
|
|
75
|
+
# data
|
|
76
|
+
if column_cfg["show_units_in_title"]:
|
|
77
|
+
column_data.name = f"{column_cfg['name']} ({column_cfg['unit']})"
|
|
78
|
+
else:
|
|
79
|
+
column_data.name = column_cfg["name"]
|
|
80
|
+
column_cfg["formatted_name"] = column_data.name
|
|
81
|
+
|
|
82
|
+
unit = (
|
|
83
|
+
column_cfg["unit"]
|
|
84
|
+
if column_cfg["show_units_in_cell"] and column_cfg["unit"] is not None
|
|
85
|
+
else ""
|
|
86
|
+
)
|
|
87
|
+
scaled_data: pd.Series[Any] = column_data * column_cfg["scale"]
|
|
88
|
+
|
|
89
|
+
def _fmt(x: float) -> str:
|
|
90
|
+
return format_annotation(x, decimals=column_cfg["decimals"], units=unit)
|
|
91
|
+
|
|
92
|
+
formatted_data: pd.Series[Any] = scaled_data.apply(_fmt)
|
|
93
|
+
|
|
94
|
+
def _check_zero(x: float) -> bool:
|
|
95
|
+
return is_zero(x, decimals=column_cfg["decimals"])
|
|
96
|
+
|
|
97
|
+
zeros: pd.Series[Any] = scaled_data.apply(_check_zero)
|
|
98
|
+
positives: pd.Series[Any] = scaled_data > 0
|
|
99
|
+
negatives: pd.Series[Any] = scaled_data < 0
|
|
100
|
+
|
|
101
|
+
# colors
|
|
102
|
+
colors_cfg: bool = column_cfg["colors"]
|
|
103
|
+
color_data: pd.Series[Any] = pd.Series(
|
|
104
|
+
index=formatted_data.index,
|
|
105
|
+
name=column_data.name,
|
|
106
|
+
dtype=str,
|
|
107
|
+
)
|
|
108
|
+
positive_good: bool = False
|
|
109
|
+
if colors_cfg:
|
|
110
|
+
positive_good = column_cfg["positive_good"]
|
|
111
|
+
color_data[positives] = GOOD if positive_good else BAD
|
|
112
|
+
color_data.loc[negatives] = BAD if positive_good else GOOD
|
|
113
|
+
color_data.loc[zeros.values] = pd.NA # type: ignore[index]
|
|
114
|
+
|
|
115
|
+
# shades
|
|
116
|
+
shade_data: pd.Series[Any] = pd.Series(
|
|
117
|
+
index=formatted_data.index,
|
|
118
|
+
name=column_data.name,
|
|
119
|
+
dtype=str,
|
|
120
|
+
)
|
|
121
|
+
if outliers_flags is not None:
|
|
122
|
+
thresholds: pd.Series[Any] = abs(outliers_flags / THRESHOLD)
|
|
123
|
+
|
|
124
|
+
def _shade(x: float) -> str:
|
|
125
|
+
return _shade_intensity(x, SHADE_LEVELS)
|
|
126
|
+
|
|
127
|
+
intensities: pd.Series[Any] = thresholds.apply(_shade)
|
|
128
|
+
|
|
129
|
+
def _color_pos(x: str) -> str:
|
|
130
|
+
return f"00{x}00" if positive_good else f"{x}0000"
|
|
131
|
+
|
|
132
|
+
def _color_neg(x: str) -> str:
|
|
133
|
+
return f"{x}0000" if positive_good else f"00{x}00"
|
|
134
|
+
|
|
135
|
+
shade_data[(thresholds >= 1) & (outliers_flags > 0)] = intensities[
|
|
136
|
+
(thresholds >= 1) & (outliers_flags > 0)
|
|
137
|
+
].apply(_color_pos)
|
|
138
|
+
shade_data[(thresholds >= 1) & (outliers_flags < 0)] = intensities[
|
|
139
|
+
(thresholds >= 1) & (outliers_flags < 0)
|
|
140
|
+
].apply(_color_neg)
|
|
141
|
+
|
|
142
|
+
return formatted_data, color_data, shade_data
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _generate_block(
|
|
146
|
+
block_data: pd.DataFrame, block_cfg: dict[str, Any]
|
|
147
|
+
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
|
148
|
+
columns: dict[str, Any] = block_cfg["columns"]
|
|
149
|
+
formatted_columns: list[pd.Series[Any]] = []
|
|
150
|
+
color_columns: list[pd.Series[Any]] = []
|
|
151
|
+
shade_columns: list[pd.Series[Any]] = []
|
|
152
|
+
sort_idx: pd.Index[Any] | None = None
|
|
153
|
+
for column_name, column_cfg in columns.items():
|
|
154
|
+
last_date: pd.Timestamp = block_data.index.get_level_values(
|
|
155
|
+
level=0
|
|
156
|
+
).max()
|
|
157
|
+
block_data = block_data.rename(columns=block_cfg["series"])
|
|
158
|
+
offset: str = column_cfg["offset"]
|
|
159
|
+
difference: str = column_cfg["difference"]
|
|
160
|
+
stat: str = column_cfg["stat"]
|
|
161
|
+
outliers: bool = column_cfg["outliers"]
|
|
162
|
+
|
|
163
|
+
stat_data: pd.DataFrame = block_data.loc[
|
|
164
|
+
(last_date, offset, difference, slice(None)), :
|
|
165
|
+
]
|
|
166
|
+
stat_data.index = stat_data.index.get_level_values(level=-1)
|
|
167
|
+
|
|
168
|
+
outliers_flags: pd.Series[Any] | None = None
|
|
169
|
+
if outliers:
|
|
170
|
+
outliers_flags = flag_outliers(stat_data.T)
|
|
171
|
+
|
|
172
|
+
column_data: pd.Series[Any] = stat_data.loc[stat, :] # type: ignore[assignment]
|
|
173
|
+
# sort capability
|
|
174
|
+
sort: str | None = block_cfg.get("sort", None)
|
|
175
|
+
if sort is not None and column_name == sort:
|
|
176
|
+
column_data = column_data.sort_values(ascending=False)
|
|
177
|
+
sort_idx = column_data.index
|
|
178
|
+
formatted_column, color_column, shade_column = _generate_column(
|
|
179
|
+
column_data, column_cfg, outliers_flags
|
|
180
|
+
)
|
|
181
|
+
formatted_columns.append(formatted_column)
|
|
182
|
+
color_columns.append(color_column)
|
|
183
|
+
shade_columns.append(shade_column)
|
|
184
|
+
|
|
185
|
+
if sort_idx is not None:
|
|
186
|
+
formatted_columns = [s.reindex(sort_idx) for s in formatted_columns]
|
|
187
|
+
color_columns = [s.reindex(sort_idx) for s in color_columns]
|
|
188
|
+
shade_columns = [s.reindex(sort_idx) for s in shade_columns]
|
|
189
|
+
|
|
190
|
+
formatted_block: pd.DataFrame = pd.concat(formatted_columns, axis=1)
|
|
191
|
+
color_block: pd.DataFrame = pd.concat(color_columns, axis=1)
|
|
192
|
+
shade_block: pd.DataFrame = pd.concat(shade_columns, axis=1)
|
|
193
|
+
|
|
194
|
+
formatted_block.columns.name = block_cfg["title"]
|
|
195
|
+
color_block.columns.name = block_cfg["title"]
|
|
196
|
+
shade_block.columns.name = block_cfg["title"]
|
|
197
|
+
return formatted_block, color_block, shade_block
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def generate_table(
|
|
201
|
+
table_data: pd.DataFrame, table_cfg: dict[str, Any]
|
|
202
|
+
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
|
203
|
+
blocks: dict[str, dict[str, Any]] = table_cfg["blocks"]
|
|
204
|
+
formatted_blocks: list[pd.DataFrame] = []
|
|
205
|
+
color_blocks: list[pd.DataFrame] = []
|
|
206
|
+
shade_blocks: list[pd.DataFrame] = []
|
|
207
|
+
axis = 0 if table_cfg["axis"] == "vertical" else 1
|
|
208
|
+
# sorting capabilities
|
|
209
|
+
for _block_name, block_cfg in blocks.items():
|
|
210
|
+
series_dict: dict[str, str] = block_cfg["series"]
|
|
211
|
+
block_data: pd.DataFrame = table_data.loc[:, series_dict.keys()]
|
|
212
|
+
formatted_block, color_block, shade_block = _generate_block(
|
|
213
|
+
block_data, block_cfg
|
|
214
|
+
)
|
|
215
|
+
formatted_blocks.append(formatted_block)
|
|
216
|
+
color_blocks.append(color_block)
|
|
217
|
+
shade_blocks.append(shade_block)
|
|
218
|
+
formatted_table: pd.DataFrame = pd.concat(
|
|
219
|
+
formatted_blocks,
|
|
220
|
+
axis=axis,
|
|
221
|
+
keys=[
|
|
222
|
+
formatted_block.columns.name for formatted_block in formatted_blocks
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
color_table: pd.DataFrame = pd.concat(
|
|
226
|
+
color_blocks,
|
|
227
|
+
axis=axis,
|
|
228
|
+
keys=[
|
|
229
|
+
formatted_block.columns.name for formatted_block in formatted_blocks
|
|
230
|
+
],
|
|
231
|
+
)
|
|
232
|
+
shade_table: pd.DataFrame = pd.concat(
|
|
233
|
+
shade_blocks,
|
|
234
|
+
axis=axis,
|
|
235
|
+
keys=[
|
|
236
|
+
formatted_block.columns.name for formatted_block in formatted_blocks
|
|
237
|
+
],
|
|
238
|
+
)
|
|
239
|
+
return formatted_table, color_table, shade_table
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def generate_tables_from_flash(
|
|
243
|
+
out_path: Path,
|
|
244
|
+
flash: pd.DataFrame,
|
|
245
|
+
config_dicts: dict[str, dict[str, Any]],
|
|
246
|
+
) -> None:
|
|
247
|
+
for table_name, table_cfg in config_dicts.items():
|
|
248
|
+
series: list[str] = list(collect_series(table_cfg))
|
|
249
|
+
table_data: pd.DataFrame = flash.loc[:, series]
|
|
250
|
+
formatted_table, color_table, shade_table = generate_table(
|
|
251
|
+
table_data, table_cfg
|
|
252
|
+
)
|
|
253
|
+
formatted_table.to_feather(out_path / f"{table_name}.feather")
|
|
254
|
+
color_table.to_feather(out_path / f"{table_name}_color.feather")
|
|
255
|
+
shade_table.to_feather(out_path / f"{table_name}_shade.feather")
|