tesorotools-python 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl
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/__init__.py +2 -0
- tesorotools/artists/__init__.py +5 -0
- tesorotools/artists/_common.py +25 -2
- tesorotools/artists/line_plot.py +32 -1
- tesorotools/artists/plotly_backend.py +3 -0
- tesorotools/artists/sharing.py +59 -0
- {tesorotools_python-0.1.1.dist-info → tesorotools_python-0.1.2.dist-info}/METADATA +1 -1
- {tesorotools_python-0.1.1.dist-info → tesorotools_python-0.1.2.dist-info}/RECORD +10 -9
- {tesorotools_python-0.1.1.dist-info → tesorotools_python-0.1.2.dist-info}/WHEEL +0 -0
- {tesorotools_python-0.1.1.dist-info → tesorotools_python-0.1.2.dist-info}/entry_points.txt +0 -0
tesorotools/__init__.py
CHANGED
|
@@ -72,6 +72,7 @@ from tesorotools.artists import (
|
|
|
72
72
|
TypeCurve,
|
|
73
73
|
VectorPlot,
|
|
74
74
|
Waterfall,
|
|
75
|
+
share_y_axes,
|
|
75
76
|
)
|
|
76
77
|
from tesorotools.orchestration import CompositeRegistry, iter_contexts
|
|
77
78
|
from tesorotools.providers.base import (
|
|
@@ -177,6 +178,7 @@ __all__ = [
|
|
|
177
178
|
"register_providers",
|
|
178
179
|
"register_tag",
|
|
179
180
|
"register_tags",
|
|
181
|
+
"share_y_axes",
|
|
180
182
|
]
|
|
181
183
|
|
|
182
184
|
|
tesorotools/artists/__init__.py
CHANGED
|
@@ -20,6 +20,9 @@ Each class follows the same shape:
|
|
|
20
20
|
Plotly figure (all but ``IntradayPlot``);
|
|
21
21
|
:func:`plotly_unsupported` lists the keys it drops.
|
|
22
22
|
|
|
23
|
+
:func:`share_y_axes` gives the charts that declare the same
|
|
24
|
+
``y_group`` one common y scale.
|
|
25
|
+
|
|
23
26
|
Shared layout, annotation and styling helpers (plus the
|
|
24
27
|
``Format`` and ``Legend`` config holders) live in
|
|
25
28
|
:mod:`tesorotools.artists._common`.
|
|
@@ -42,6 +45,7 @@ from tesorotools.artists.box_plot import BoxPlot
|
|
|
42
45
|
from tesorotools.artists.intraday_plot import IntradayPlot
|
|
43
46
|
from tesorotools.artists.line_plot import LinePlot
|
|
44
47
|
from tesorotools.artists.matrix import MatrixChart
|
|
48
|
+
from tesorotools.artists.sharing import share_y_axes
|
|
45
49
|
from tesorotools.artists.shock_plot import ShockChart
|
|
46
50
|
from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
|
|
47
51
|
from tesorotools.artists.type_curve import TypeCurve
|
|
@@ -67,4 +71,5 @@ __all__ = [
|
|
|
67
71
|
"VectorPlot",
|
|
68
72
|
"Waterfall",
|
|
69
73
|
"plotly_unsupported",
|
|
74
|
+
"share_y_axes",
|
|
70
75
|
]
|
tesorotools/artists/_common.py
CHANGED
|
@@ -23,6 +23,7 @@ overrides only need to update ``plots.yaml``.
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
+
from collections.abc import Sequence
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
from typing import Any, Literal, Protocol, Self, cast, runtime_checkable
|
|
28
29
|
|
|
@@ -1299,6 +1300,26 @@ def style_spines(
|
|
|
1299
1300
|
)
|
|
1300
1301
|
|
|
1301
1302
|
|
|
1303
|
+
def check_y_limits(
|
|
1304
|
+
limits: Sequence[float] | None,
|
|
1305
|
+
) -> tuple[float, float] | None:
|
|
1306
|
+
"""Validate a ``y_limits`` pair and return it as ``(bottom, top)``.
|
|
1307
|
+
|
|
1308
|
+
YAML hands the pair over as a list, so any two-item sequence is
|
|
1309
|
+
accepted; ``None`` (no fixed range) passes through. Raises
|
|
1310
|
+
``ValueError`` unless there are exactly two numbers with
|
|
1311
|
+
``bottom < top``.
|
|
1312
|
+
"""
|
|
1313
|
+
if limits is None:
|
|
1314
|
+
return None
|
|
1315
|
+
if len(limits) != 2:
|
|
1316
|
+
raise ValueError(f"y_limits must be (bottom, top): {limits!r}")
|
|
1317
|
+
bottom, top = float(limits[0]), float(limits[1])
|
|
1318
|
+
if not bottom < top:
|
|
1319
|
+
raise ValueError(f"y_limits bottom must be below top: {limits!r}")
|
|
1320
|
+
return bottom, top
|
|
1321
|
+
|
|
1322
|
+
|
|
1302
1323
|
def style_baseline(
|
|
1303
1324
|
ax: Axes,
|
|
1304
1325
|
reference: float = 0,
|
|
@@ -1339,6 +1360,7 @@ def annotate_last_values(
|
|
|
1339
1360
|
fontsize: float | None = None,
|
|
1340
1361
|
right_pad_px: float = 10.0,
|
|
1341
1362
|
headroom_pad_pt: float = 0.0,
|
|
1363
|
+
grow_top: bool = True,
|
|
1342
1364
|
) -> None:
|
|
1343
1365
|
"""Label the last non-NaN value of each column on the right.
|
|
1344
1366
|
|
|
@@ -1365,7 +1387,8 @@ def annotate_last_values(
|
|
|
1365
1387
|
y-limit, that limit grows enough to keep this many points
|
|
1366
1388
|
free and the labels are re-packed at the new scale, so a
|
|
1367
1389
|
tall stack never spills into a legend above the axes (nor
|
|
1368
|
-
is clipped at the top).
|
|
1390
|
+
is clipped at the top). ``grow_top=False`` keeps the top
|
|
1391
|
+
y-limit as it is (a fixed or shared y range must not move).
|
|
1369
1392
|
"""
|
|
1370
1393
|
fig = ax.get_figure()
|
|
1371
1394
|
if fig is None:
|
|
@@ -1437,7 +1460,7 @@ def annotate_last_values(
|
|
|
1437
1460
|
pad_px = headroom_pad_pt * fig.dpi / 72.0
|
|
1438
1461
|
top_axes_px: float = trans.transform((0, ymax))[1] # type: ignore[reportUnknownArgumentType]
|
|
1439
1462
|
needed_px = placements[-1] + text_height / 2 + pad_px
|
|
1440
|
-
if needed_px > top_axes_px:
|
|
1463
|
+
if grow_top and needed_px > top_axes_px:
|
|
1441
1464
|
new_top: float = trans.inverted().transform( # type: ignore[reportUnknownArgumentType]
|
|
1442
1465
|
(0, needed_px)
|
|
1443
1466
|
)[1]
|
tesorotools/artists/line_plot.py
CHANGED
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
13
13
|
import locale
|
|
14
|
+
from collections.abc import Sequence
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any, ClassVar, Self
|
|
16
17
|
|
|
@@ -30,6 +31,7 @@ from tesorotools.artists._common import (
|
|
|
30
31
|
adjust_figure_for_plot_size,
|
|
31
32
|
annotate_last_values,
|
|
32
33
|
apply_date_axis,
|
|
34
|
+
check_y_limits,
|
|
33
35
|
compact_date_axis,
|
|
34
36
|
compact_figure_kwargs,
|
|
35
37
|
compact_font_pt,
|
|
@@ -160,6 +162,15 @@ class LinePlot:
|
|
|
160
162
|
**height** (keeping the width) so the plot area is not
|
|
161
163
|
squashed by a large legend or rotated date labels. Skipped
|
|
162
164
|
when ``plot_size`` is set (that already fixes the axes size).
|
|
165
|
+
* ``y_limits`` -- fix the y-axis to ``(bottom, top)`` on both
|
|
166
|
+
backends. The range is strict: the baseline, the reference
|
|
167
|
+
lines and the end-of-series labels no longer widen it.
|
|
168
|
+
* ``y_group`` -- name of a group of charts that must share the
|
|
169
|
+
same y scale (e.g. US and European equities, both base 100,
|
|
170
|
+
in two separate charts). It does nothing on its own:
|
|
171
|
+
:func:`tesorotools.artists.share_y_axes` measures every chart
|
|
172
|
+
of each group and sets the union of their ranges as their
|
|
173
|
+
``y_limits``. Mutually exclusive with ``y_limits``.
|
|
163
174
|
|
|
164
175
|
Render lifecycle: ``build()`` returns ``(Figure, Axes)``
|
|
165
176
|
in memory; ``save(fig)`` persists to ``out_path``;
|
|
@@ -223,9 +234,16 @@ class LinePlot:
|
|
|
223
234
|
ref_lines: list[dict[str, Any]] | None = None,
|
|
224
235
|
show_means: bool = False,
|
|
225
236
|
means_window: tuple[Any, Any] | None = None,
|
|
237
|
+
y_limits: Sequence[float] | None = None,
|
|
238
|
+
y_group: str | None = None,
|
|
226
239
|
) -> None:
|
|
227
240
|
if out_path.suffix != ".png":
|
|
228
241
|
raise ValueError(f"out_path must be .png: {out_path}")
|
|
242
|
+
if y_limits is not None and y_group is not None:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
"y_limits and y_group are mutually exclusive: a group "
|
|
245
|
+
"computes its shared y_limits (see share_y_axes)"
|
|
246
|
+
)
|
|
229
247
|
if base_100 and base_0:
|
|
230
248
|
raise ValueError(
|
|
231
249
|
"base_100 and base_0 are mutually exclusive: a series is "
|
|
@@ -269,6 +287,8 @@ class LinePlot:
|
|
|
269
287
|
self.ref_lines = ref_lines or []
|
|
270
288
|
self.show_means = show_means
|
|
271
289
|
self.means_window = means_window
|
|
290
|
+
self.y_limits = check_y_limits(y_limits)
|
|
291
|
+
self.y_group = y_group
|
|
272
292
|
|
|
273
293
|
@property
|
|
274
294
|
def _pin_start_date(self) -> bool:
|
|
@@ -324,7 +344,8 @@ class LinePlot:
|
|
|
324
344
|
``axis_decimals``, ``axis_units``, ``annotate_units``,
|
|
325
345
|
``date_density``, ``date_rotation``, ``date_show_start``,
|
|
326
346
|
``adapt_height``,
|
|
327
|
-
``ref_lines``, ``show_means``, ``means_window
|
|
347
|
+
``ref_lines``, ``show_means``, ``means_window``,
|
|
348
|
+
``y_limits``, ``y_group``.
|
|
328
349
|
|
|
329
350
|
Example
|
|
330
351
|
-------
|
|
@@ -462,6 +483,11 @@ class LinePlot:
|
|
|
462
483
|
# data point. annotate_last_values reopens room on the right.
|
|
463
484
|
ax.margins(x=0)
|
|
464
485
|
|
|
486
|
+
# a fixed (or group-shared) y range goes in BEFORE the annotations:
|
|
487
|
+
# they pack their labels at the final scale.
|
|
488
|
+
if self.y_limits is not None:
|
|
489
|
+
ax.set_ylim(*self.y_limits)
|
|
490
|
+
|
|
465
491
|
if self.vlines:
|
|
466
492
|
draw_vlines(ax, self.vlines)
|
|
467
493
|
|
|
@@ -494,6 +520,7 @@ class LinePlot:
|
|
|
494
520
|
headroom_pad_pt=(
|
|
495
521
|
compact_headroom_pad_pt() if self.compact else 0.0
|
|
496
522
|
),
|
|
523
|
+
grow_top=self.y_limits is None,
|
|
497
524
|
)
|
|
498
525
|
|
|
499
526
|
style_spines(
|
|
@@ -536,6 +563,10 @@ class LinePlot:
|
|
|
536
563
|
)
|
|
537
564
|
lo, hi = ax.get_ylim()
|
|
538
565
|
ax.set_ylim(min(ref_line["y"], lo), max(ref_line["y"], hi))
|
|
566
|
+
if self.y_limits is not None:
|
|
567
|
+
# the baseline and the reference lines widen the range to show
|
|
568
|
+
# themselves; a fixed range wins over them.
|
|
569
|
+
ax.set_ylim(*self.y_limits)
|
|
539
570
|
|
|
540
571
|
# per-series historical mean as a dotted same-colour line (the old
|
|
541
572
|
# `medias=True`): the mean is taken over means_window (or the whole
|
|
@@ -302,6 +302,9 @@ def line_plot_to_figure(chart: LinePlot) -> go.Figure:
|
|
|
302
302
|
# toggles (mirrors the matplotlib artist).
|
|
303
303
|
if chart.fmt.units and chart.axis_units:
|
|
304
304
|
fig.update_yaxes(ticksuffix=chart.fmt.units)
|
|
305
|
+
# fixed or group-shared y range (see share_y_axes)
|
|
306
|
+
if chart.y_limits is not None:
|
|
307
|
+
fig.update_yaxes(range=list(chart.y_limits))
|
|
305
308
|
_legend_side(fig, chart.legend)
|
|
306
309
|
return fig
|
|
307
310
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""One y scale shared by several separate charts.
|
|
2
|
+
|
|
3
|
+
Two charts that are read side by side (US and European equities, both
|
|
4
|
+
base 100) mislead when each picks its own y range: the same move looks
|
|
5
|
+
bigger in the flatter one. Charts that declare the same ``y_group``
|
|
6
|
+
are measured together here and all get the union of their ranges as a
|
|
7
|
+
fixed ``y_limits``, which both backends honour (``build()`` for the PNG,
|
|
8
|
+
``to_plotly()`` for the interactive figure).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
__all__ = ["share_y_axes"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def share_y_axes(charts: Iterable[Any]) -> dict[str, tuple[float, float]]:
|
|
22
|
+
"""Give every chart of each ``y_group`` the same ``y_limits``.
|
|
23
|
+
|
|
24
|
+
*charts* is any iterable of artists, typically the values of the
|
|
25
|
+
mapping loaded from a charts YAML; the ones without a ``y_group``
|
|
26
|
+
(or with ``None``) are left untouched. Each grouped chart is built
|
|
27
|
+
once with its own natural range (the one it would draw alone,
|
|
28
|
+
including the room its end-of-series labels open), and the group
|
|
29
|
+
gets the lowest bottom and the highest top among its members. Call
|
|
30
|
+
it before ``plot()`` / ``to_plotly()``; calling it again measures
|
|
31
|
+
afresh, so it is safe after the data change.
|
|
32
|
+
|
|
33
|
+
Returns the shared ``(bottom, top)`` per group name.
|
|
34
|
+
"""
|
|
35
|
+
groups: dict[str, list[Any]] = {}
|
|
36
|
+
for chart in charts:
|
|
37
|
+
group: Any = getattr(chart, "y_group", None)
|
|
38
|
+
if group is not None:
|
|
39
|
+
groups.setdefault(str(group), []).append(chart)
|
|
40
|
+
|
|
41
|
+
shared: dict[str, tuple[float, float]] = {}
|
|
42
|
+
for name, members in groups.items():
|
|
43
|
+
bottoms: list[float] = []
|
|
44
|
+
tops: list[float] = []
|
|
45
|
+
for chart in members:
|
|
46
|
+
# measure the chart's own range, not one shared earlier
|
|
47
|
+
chart.y_limits = None
|
|
48
|
+
fig, ax = chart.build()
|
|
49
|
+
try:
|
|
50
|
+
bottom, top = ax.get_ylim()
|
|
51
|
+
finally:
|
|
52
|
+
plt.close(fig)
|
|
53
|
+
bottoms.append(float(bottom))
|
|
54
|
+
tops.append(float(top))
|
|
55
|
+
limits = (min(bottoms), max(tops))
|
|
56
|
+
for chart in members:
|
|
57
|
+
chart.y_limits = limits
|
|
58
|
+
shared[name] = limits
|
|
59
|
+
return shared
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
tesorotools/__init__.py,sha256=
|
|
1
|
+
tesorotools/__init__.py,sha256=BV31YV_qabD4vdXzoKb89MxXO386YE0xFjXqpDZSXaE,5334
|
|
2
2
|
tesorotools/_build_context.py,sha256=MH2AKjkwuubN__sBQoXbfTD1bh5RE6w9l5yUFxkJxy8,1982
|
|
3
3
|
tesorotools/_registry.py,sha256=ZJOr7jACxykvzoepebZDiJdTcR37Q3X-DQ07lm9ZYB0,7975
|
|
4
4
|
tesorotools/driver.py,sha256=qEVhLYanbG7VXe3WrCCD0RJV_0X4xzhalrfABPB1gSI,5016
|
|
@@ -7,16 +7,17 @@ tesorotools/orchestration.py,sha256=24C-LTM4uy7eckjXBjJbC2t92fg6yffl5H-E9Gm2W9M,
|
|
|
7
7
|
tesorotools/palette.py,sha256=fkf1_AxweRzviwKLb5zSZnWwP_Cr4zEYoqJ4jqIfBoE,1944
|
|
8
8
|
tesorotools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
tesorotools/scaffold.py,sha256=6mYNxLm_1CbEMjcYZ77m_2H1eTotbpatFJqhgyote0s,4578
|
|
10
|
-
tesorotools/artists/__init__.py,sha256
|
|
11
|
-
tesorotools/artists/_common.py,sha256=
|
|
10
|
+
tesorotools/artists/__init__.py,sha256=-w2IzVdT7iatJien_5JU9fHepx2kTcYcYgrjQXWZYRI,2492
|
|
11
|
+
tesorotools/artists/_common.py,sha256=k59YvyxIlTSEUPU6-koGksOdEKvDcwjhsdPV0LxRCLo,59272
|
|
12
12
|
tesorotools/artists/bar_line.py,sha256=4urcPuHEfohl4WI3pkb2I__1WZ2W73lDiGoqlwFA27w,14477
|
|
13
13
|
tesorotools/artists/barh_plot.py,sha256=n8DjIX1APCVS7HW4zzfh6ehggdgBqb6sezQHov1k7us,37520
|
|
14
14
|
tesorotools/artists/box_plot.py,sha256=KPQjbY2H5JWJWF_icApqpvE1f6sE4e56neIS_dRL6w8,20373
|
|
15
15
|
tesorotools/artists/compact.py,sha256=e_0tqkRSIgyiyMSwirJzj2TYSKojMxUHzug5tE_7toc,3003
|
|
16
16
|
tesorotools/artists/intraday_plot.py,sha256=TO-lKwwbIPxx4T-ObYHNuGGR3F_-GZ6XQGwrYU6tyL8,13259
|
|
17
|
-
tesorotools/artists/line_plot.py,sha256=
|
|
17
|
+
tesorotools/artists/line_plot.py,sha256=yqg7dHzzV_xfiwiWIlvNN6DVXQ9wjRBdOsiJYvG0iJY,31157
|
|
18
18
|
tesorotools/artists/matrix.py,sha256=cOpj9ZO5RR0-RlIP55UmD70i-738ck8wZIqRiDO22Hw,11785
|
|
19
|
-
tesorotools/artists/plotly_backend.py,sha256=
|
|
19
|
+
tesorotools/artists/plotly_backend.py,sha256=yaRnZayxgTbNVuiJW1p5l10mo5UFMTslA_QK-nBkZsM,37067
|
|
20
|
+
tesorotools/artists/sharing.py,sha256=Tg4AYtv0ileKQmzBSgmT1GzxA97tdU8ji_arE0oGEZ4,2236
|
|
20
21
|
tesorotools/artists/shock_plot.py,sha256=Vx9RRvRaJZIXh8G7oJSRRc1XtPgGkqA6xaVk4h4oJuE,13435
|
|
21
22
|
tesorotools/artists/stacked.py,sha256=MyItTb8NMOpR6jDclqjmofhAB-L9mD1vnzGQjXUTFBc,17439
|
|
22
23
|
tesorotools/artists/type_curve.py,sha256=0j1LjfS4Zdz4_W7qp-nY8VP0H-fENV5adUg4syAOvig,12570
|
|
@@ -129,7 +130,7 @@ tesorotools/web/templates/src/$package/main.py.tmpl,sha256=GXG3AA87WOjk5nTKA5Ywm
|
|
|
129
130
|
tesorotools/web/templates/src/$package/sections.py.tmpl,sha256=4MNZ85AW5ERG1SrkVAXIwdmAMeQEBARCYdAMqUr37mg,2084
|
|
130
131
|
tesorotools/web/templates/tests/__init__.py.tmpl,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
131
132
|
tesorotools/web/templates/tests/test_app.py.tmpl,sha256=ZGfV5GPN6WsS3a2gCNLPWejkHTX2H_N9XpurIADuUxI,615
|
|
132
|
-
tesorotools_python-0.1.
|
|
133
|
-
tesorotools_python-0.1.
|
|
134
|
-
tesorotools_python-0.1.
|
|
135
|
-
tesorotools_python-0.1.
|
|
133
|
+
tesorotools_python-0.1.2.dist-info/METADATA,sha256=ARJoWsUX5H4GXfcoU7EH_sVTr5aUusS-9OpkhfTVm-A,993
|
|
134
|
+
tesorotools_python-0.1.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
135
|
+
tesorotools_python-0.1.2.dist-info/entry_points.txt,sha256=PuNrObu4SVvajBHTASuWZPqwSsV7tPHwFJBzYk2nY5o,108
|
|
136
|
+
tesorotools_python-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|