tesorotools-python 0.1.1__tar.gz → 0.1.3__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.1.1 → tesorotools_python-0.1.3}/PKG-INFO +1 -1
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/__init__.py +2 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/__init__.py +5 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/_common.py +32 -3
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/bar_line.py +145 -13
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/line_plot.py +32 -1
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/plotly_backend.py +32 -5
- tesorotools_python-0.1.3/src/tesorotools/artists/sharing.py +59 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/.gitignore +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/pyproject.toml +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/barh_plot.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/box_plot.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/compact.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/intraday_plot.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/matrix.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/shock_plot.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/stacked.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/type_curve.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/vector_plot.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/waterfall.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/plots.yaml +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/template.docx +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/cli.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/scaffold.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/sections/01-introduccion.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/sections/02-analisis.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/sections/03-conclusiones.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/slides/01-contexto.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/slides/02-analisis.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/slides/03-conclusiones.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/style/beamerthemeTesoro.sty +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/appendices/a-fuentes.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/chapters/01-introduccion.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/chapters/02-analisis.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/frontmatter/resumen.tex +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/README.md.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/dot-gitignore +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/dot-vscode/settings.json +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/figures/dot-gitkeep +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/fonts/README.md +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/latexmkrc +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/refs.bib +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/style/tesoro.sty +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/style/tesorobrand.sty.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/style/tesorolocal.sty.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/palette.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/fred.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/lseg.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/document.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/scaffold.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/utils/template.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/__init__.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/app.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/brand.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/cli.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/components.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/formatting.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/launcher.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/palette.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/plotting.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/scaffold.py +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/Dockerfile.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/README.md.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/dot-dockerignore.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/dot-gitignore.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/dot-streamlit/config.toml.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/pyproject.toml.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/__init__.py.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/cli.py.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/main.py.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/sections.py.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/tests/__init__.py.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/tests/test_app.py.tmpl +0 -0
- {tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/theme.py +0 -0
|
@@ -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
|
|
|
@@ -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
|
]
|
|
@@ -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
|
|
|
@@ -1173,6 +1174,7 @@ def place_legend(
|
|
|
1173
1174
|
compact: bool,
|
|
1174
1175
|
ncol: int | None = None,
|
|
1175
1176
|
position: str = "auto",
|
|
1177
|
+
entries: tuple[list[Any], list[str]] | None = None,
|
|
1176
1178
|
) -> MplLegend | None:
|
|
1177
1179
|
"""Place the figure legend, compact-aware, from the labelled artists.
|
|
1178
1180
|
|
|
@@ -1183,9 +1185,14 @@ def place_legend(
|
|
|
1183
1185
|
``rcParams`` metrics. ``position`` (``"above"`` / ``"below"`` /
|
|
1184
1186
|
``"auto"``) chooses the side through :func:`resolve_legend_position`, so
|
|
1185
1187
|
a compact chart can carry its legend under the plot instead of over it.
|
|
1188
|
+
``entries`` passes precomputed ``(handles, labels)`` instead of reading
|
|
1189
|
+
them off *ax* -- for charts whose series span several axes (a
|
|
1190
|
+
``twinx`` secondary y-axis), in the order the legend should list them.
|
|
1186
1191
|
Returns ``None`` (drawing nothing) when there are no labels.
|
|
1187
1192
|
"""
|
|
1188
|
-
handles, labels =
|
|
1193
|
+
handles, labels = (
|
|
1194
|
+
entries if entries is not None else ax.get_legend_handles_labels()
|
|
1195
|
+
)
|
|
1189
1196
|
if not labels:
|
|
1190
1197
|
return None
|
|
1191
1198
|
side = resolve_legend_position(position, compact=compact)
|
|
@@ -1299,6 +1306,26 @@ def style_spines(
|
|
|
1299
1306
|
)
|
|
1300
1307
|
|
|
1301
1308
|
|
|
1309
|
+
def check_y_limits(
|
|
1310
|
+
limits: Sequence[float] | None,
|
|
1311
|
+
) -> tuple[float, float] | None:
|
|
1312
|
+
"""Validate a ``y_limits`` pair and return it as ``(bottom, top)``.
|
|
1313
|
+
|
|
1314
|
+
YAML hands the pair over as a list, so any two-item sequence is
|
|
1315
|
+
accepted; ``None`` (no fixed range) passes through. Raises
|
|
1316
|
+
``ValueError`` unless there are exactly two numbers with
|
|
1317
|
+
``bottom < top``.
|
|
1318
|
+
"""
|
|
1319
|
+
if limits is None:
|
|
1320
|
+
return None
|
|
1321
|
+
if len(limits) != 2:
|
|
1322
|
+
raise ValueError(f"y_limits must be (bottom, top): {limits!r}")
|
|
1323
|
+
bottom, top = float(limits[0]), float(limits[1])
|
|
1324
|
+
if not bottom < top:
|
|
1325
|
+
raise ValueError(f"y_limits bottom must be below top: {limits!r}")
|
|
1326
|
+
return bottom, top
|
|
1327
|
+
|
|
1328
|
+
|
|
1302
1329
|
def style_baseline(
|
|
1303
1330
|
ax: Axes,
|
|
1304
1331
|
reference: float = 0,
|
|
@@ -1339,6 +1366,7 @@ def annotate_last_values(
|
|
|
1339
1366
|
fontsize: float | None = None,
|
|
1340
1367
|
right_pad_px: float = 10.0,
|
|
1341
1368
|
headroom_pad_pt: float = 0.0,
|
|
1369
|
+
grow_top: bool = True,
|
|
1342
1370
|
) -> None:
|
|
1343
1371
|
"""Label the last non-NaN value of each column on the right.
|
|
1344
1372
|
|
|
@@ -1365,7 +1393,8 @@ def annotate_last_values(
|
|
|
1365
1393
|
y-limit, that limit grows enough to keep this many points
|
|
1366
1394
|
free and the labels are re-packed at the new scale, so a
|
|
1367
1395
|
tall stack never spills into a legend above the axes (nor
|
|
1368
|
-
is clipped at the top).
|
|
1396
|
+
is clipped at the top). ``grow_top=False`` keeps the top
|
|
1397
|
+
y-limit as it is (a fixed or shared y range must not move).
|
|
1369
1398
|
"""
|
|
1370
1399
|
fig = ax.get_figure()
|
|
1371
1400
|
if fig is None:
|
|
@@ -1437,7 +1466,7 @@ def annotate_last_values(
|
|
|
1437
1466
|
pad_px = headroom_pad_pt * fig.dpi / 72.0
|
|
1438
1467
|
top_axes_px: float = trans.transform((0, ymax))[1] # type: ignore[reportUnknownArgumentType]
|
|
1439
1468
|
needed_px = placements[-1] + text_height / 2 + pad_px
|
|
1440
|
-
if needed_px > top_axes_px:
|
|
1469
|
+
if grow_top and needed_px > top_axes_px:
|
|
1441
1470
|
new_top: float = trans.inverted().transform( # type: ignore[reportUnknownArgumentType]
|
|
1442
1471
|
(0, needed_px)
|
|
1443
1472
|
)[1]
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""Bar + line combo chart artist, registered as ``!bar_line``.
|
|
2
2
|
|
|
3
3
|
Defines :class:`BarLinePlot`: one time-series chart that draws some
|
|
4
|
-
series as **vertical bars** and others as **marked lines
|
|
5
|
-
shared y-axis. The canonical use is a secondary magnitude in
|
|
6
|
-
earnings *surprise* in p.p.) under a primary series as the
|
|
7
|
-
line (earnings *growth* in %), both read against the same 0
|
|
4
|
+
series as **vertical bars** and others as **marked lines**, by default
|
|
5
|
+
on a single shared y-axis. The canonical use is a secondary magnitude in
|
|
6
|
+
bars (an earnings *surprise* in p.p.) under a primary series as the
|
|
7
|
+
highlighted line (earnings *growth* in %), both read against the same 0
|
|
8
|
+
baseline. When the scales are too far apart to share an axis (an
|
|
9
|
+
exchange rate around 1.2 against an R^2 between 0 and 1), ``secondary``
|
|
10
|
+
moves any subset of the series, bars or lines, to a right-hand y-axis
|
|
11
|
+
with its own scale, format and title.
|
|
8
12
|
|
|
9
13
|
Unlike :class:`LinePlot`, the x-axis is **categorical**: one slot per
|
|
10
14
|
observation (``np.arange``) with date-formatted tick labels, so the bars
|
|
@@ -63,7 +67,7 @@ BAR_LINE_CONFIG: dict[str, Any] = PLOT_CONFIG["bar_line"]
|
|
|
63
67
|
|
|
64
68
|
|
|
65
69
|
class BarLinePlot:
|
|
66
|
-
"""Combo chart: vertical bars and marked lines on
|
|
70
|
+
"""Combo chart: vertical bars and marked lines, optionally on two axes.
|
|
67
71
|
|
|
68
72
|
Parameters mirror the other time-series artists where they overlap
|
|
69
73
|
(``out_path``, ``data`` / ``data_path``, ``scale``, ``start_date`` /
|
|
@@ -87,6 +91,21 @@ class BarLinePlot:
|
|
|
87
91
|
``date_format`` is applied via ``strftime`` (lower-cased, e.g.
|
|
88
92
|
``"dic-21"``) to a DatetimeIndex; ``max_xticks`` thins the labels
|
|
89
93
|
(``None`` labels every slot).
|
|
94
|
+
* ``secondary`` -- ids (from ``bars`` and/or ``lines``) drawn against a
|
|
95
|
+
second y-axis on the right, with its own scale. Empty (the default)
|
|
96
|
+
keeps the single shared axis, ticked on the house side (right). With
|
|
97
|
+
a secondary axis the primary one moves to the left, its grid is the
|
|
98
|
+
only one drawn, and the legend still lists every series in declared
|
|
99
|
+
order (bars, then lines). It cannot hold every series: the primary
|
|
100
|
+
axis would be left empty.
|
|
101
|
+
* ``secondary_fmt`` -- tick format (``!format``) of the secondary axis;
|
|
102
|
+
defaults to ``fmt``.
|
|
103
|
+
* ``ylabel`` / ``secondary_ylabel`` -- y-axis titles (empty: none).
|
|
104
|
+
``ylabel`` sits on the primary axis' tick side.
|
|
105
|
+
|
|
106
|
+
``baseline`` draws the 0 line on every axis that holds bars (they grow
|
|
107
|
+
from it) or, with no bars at all, on the primary axis, so a lines-only
|
|
108
|
+
axis is never stretched down to 0.
|
|
90
109
|
"""
|
|
91
110
|
|
|
92
111
|
#: Keys that change the PNG but never the ``to_plotly()`` figure
|
|
@@ -102,6 +121,7 @@ class BarLinePlot:
|
|
|
102
121
|
"legend.ncol",
|
|
103
122
|
"max_xticks",
|
|
104
123
|
"plot_size",
|
|
124
|
+
"secondary_fmt.decimals",
|
|
105
125
|
"x_rotation",
|
|
106
126
|
}
|
|
107
127
|
)
|
|
@@ -130,6 +150,10 @@ class BarLinePlot:
|
|
|
130
150
|
max_xticks: int | None = None,
|
|
131
151
|
compact: bool = False,
|
|
132
152
|
font_pt: float | None = None,
|
|
153
|
+
secondary: list[str] | None = None,
|
|
154
|
+
secondary_fmt: Format | None = None,
|
|
155
|
+
ylabel: str = "",
|
|
156
|
+
secondary_ylabel: str = "",
|
|
133
157
|
) -> None:
|
|
134
158
|
if out_path.suffix != ".png":
|
|
135
159
|
raise ValueError(f"out_path must be .png: {out_path}")
|
|
@@ -139,6 +163,17 @@ class BarLinePlot:
|
|
|
139
163
|
raise ValueError(
|
|
140
164
|
"BarLinePlot needs at least one series in `bars` or `lines`"
|
|
141
165
|
)
|
|
166
|
+
self.secondary = list(secondary or [])
|
|
167
|
+
unknown = [c for c in self.secondary if c not in self.bars | self.lines]
|
|
168
|
+
if unknown:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"`secondary` ids must be in `bars` or `lines`: {unknown}"
|
|
171
|
+
)
|
|
172
|
+
if set(self.secondary) >= {*self.bars, *self.lines}:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
"`secondary` cannot hold every series: the primary axis "
|
|
175
|
+
"would be empty"
|
|
176
|
+
)
|
|
142
177
|
self.out_path = out_path
|
|
143
178
|
self.data = resolve_data(data, data_path)
|
|
144
179
|
self.scale = scale
|
|
@@ -157,6 +192,9 @@ class BarLinePlot:
|
|
|
157
192
|
self.max_xticks = max_xticks
|
|
158
193
|
self.compact = compact
|
|
159
194
|
self.font_pt = font_pt
|
|
195
|
+
self.secondary_fmt = secondary_fmt or self.fmt
|
|
196
|
+
self.ylabel = ylabel
|
|
197
|
+
self.secondary_ylabel = secondary_ylabel
|
|
160
198
|
|
|
161
199
|
@classmethod
|
|
162
200
|
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
@@ -179,7 +217,8 @@ class BarLinePlot:
|
|
|
179
217
|
``fmt`` (``!format``), ``legend`` (``!legend``), ``figsize``,
|
|
180
218
|
``plot_size``, ``bar_styles`` / ``line_styles``, ``bar_width``,
|
|
181
219
|
``date_format``, ``x_rotation``, ``max_xticks``, ``compact``,
|
|
182
|
-
``font_pt
|
|
220
|
+
``font_pt``, ``secondary``, ``secondary_fmt`` (``!format``),
|
|
221
|
+
``ylabel`` / ``secondary_ylabel``.
|
|
183
222
|
|
|
184
223
|
Example
|
|
185
224
|
-------
|
|
@@ -194,6 +233,26 @@ class BarLinePlot:
|
|
|
194
233
|
surprise: "Sorpresa (pp)"
|
|
195
234
|
fmt: !format {decimals: 0}
|
|
196
235
|
legend: !legend {}
|
|
236
|
+
|
|
237
|
+
With scales too far apart for one axis, send some series to the
|
|
238
|
+
right-hand axis:
|
|
239
|
+
|
|
240
|
+
.. code-block:: yaml
|
|
241
|
+
|
|
242
|
+
eurusd_fair_value: !bar_line
|
|
243
|
+
out_path: out/eurusd_fair_value.png
|
|
244
|
+
data_path: data/eurusd.feather
|
|
245
|
+
bars:
|
|
246
|
+
r2: "R^2"
|
|
247
|
+
lines:
|
|
248
|
+
eurusd: "EURUSD"
|
|
249
|
+
fair_value: "Modelo valor justo (60 días)"
|
|
250
|
+
secondary: [r2]
|
|
251
|
+
fmt: !format {decimals: 2}
|
|
252
|
+
secondary_fmt: !format {decimals: 1}
|
|
253
|
+
ylabel: "EURUSD"
|
|
254
|
+
secondary_ylabel: "Coeficiente de determinación (R^2)"
|
|
255
|
+
legend: !legend {}
|
|
197
256
|
"""
|
|
198
257
|
cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
|
|
199
258
|
node, deep=True
|
|
@@ -287,9 +346,13 @@ class BarLinePlot:
|
|
|
287
346
|
**compact_figure_kwargs(self.compact, self.figsize)
|
|
288
347
|
)
|
|
289
348
|
ax: Axes = fig.add_subplot()
|
|
349
|
+
ax2: Axes | None = ax.twinx() if self.secondary else None
|
|
350
|
+
|
|
351
|
+
def target(col: str) -> Axes:
|
|
352
|
+
return ax2 if ax2 is not None and col in self.secondary else ax
|
|
290
353
|
|
|
291
354
|
# Bars first (behind the lines). Several bar series share each slot,
|
|
292
|
-
# dodged side by side; a single
|
|
355
|
+
# dodged side by side (across both axes); a single one is centred.
|
|
293
356
|
n_bars = len(self.bars)
|
|
294
357
|
each = self.bar_width / n_bars if n_bars else self.bar_width
|
|
295
358
|
default_alpha: float = float(BAR_LINE_CONFIG["bar"].get("alpha", 0.9))
|
|
@@ -299,7 +362,7 @@ class BarLinePlot:
|
|
|
299
362
|
style.setdefault("alpha", default_alpha)
|
|
300
363
|
offset = (j - (n_bars - 1) / 2) * each
|
|
301
364
|
values = plot_data[col].fillna(0).to_numpy(dtype=np.float64)
|
|
302
|
-
|
|
365
|
+
target(col).bar( # type: ignore[reportUnknownMemberType]
|
|
303
366
|
x + offset,
|
|
304
367
|
values,
|
|
305
368
|
width=each,
|
|
@@ -317,7 +380,7 @@ class BarLinePlot:
|
|
|
317
380
|
style.setdefault("marker", line_cfg.get("marker", "o"))
|
|
318
381
|
style.setdefault("linewidth", line_cfg.get("linewidth", 2))
|
|
319
382
|
style.setdefault("markersize", line_cfg.get("markersize", 5))
|
|
320
|
-
|
|
383
|
+
target(col).plot( # type: ignore[reportUnknownMemberType]
|
|
321
384
|
x,
|
|
322
385
|
plot_data[col].to_numpy(dtype=np.float64),
|
|
323
386
|
label=label,
|
|
@@ -336,14 +399,31 @@ class BarLinePlot:
|
|
|
336
399
|
# style_spines enables the y grid only (axis="y"), which is what these
|
|
337
400
|
# charts read against; the categorical x-axis stays gridless.
|
|
338
401
|
if self.baseline:
|
|
339
|
-
|
|
340
|
-
|
|
402
|
+
on_primary, on_secondary = self._baseline_on()
|
|
403
|
+
for axis, on in ((ax, on_primary), (ax2, on_secondary)):
|
|
404
|
+
if on and axis is not None:
|
|
405
|
+
style_baseline(axis, 0, **AX_CONFIG["baseline"])
|
|
406
|
+
|
|
407
|
+
side = "right"
|
|
408
|
+
if ax2 is not None:
|
|
409
|
+
side = "left"
|
|
410
|
+
ax.yaxis.tick_left()
|
|
411
|
+
self._style_secondary(ax, ax2, font_pt)
|
|
341
412
|
if self.compact:
|
|
342
|
-
style_compact_axes(ax, font_pt, side=
|
|
413
|
+
style_compact_axes(ax, font_pt, side=side)
|
|
414
|
+
if self.ylabel:
|
|
415
|
+
ax.yaxis.set_label_position(side) # type: ignore[reportArgumentType]
|
|
416
|
+
ax.set_ylabel( # type: ignore[reportUnknownMemberType]
|
|
417
|
+
self.ylabel, fontsize=font_pt
|
|
418
|
+
)
|
|
343
419
|
|
|
344
420
|
if self.legend is not None:
|
|
345
421
|
place_legend(
|
|
346
|
-
fig,
|
|
422
|
+
fig,
|
|
423
|
+
ax,
|
|
424
|
+
compact=self.compact,
|
|
425
|
+
entries=self._legend_entries(ax, ax2),
|
|
426
|
+
**legend_layout(self.legend),
|
|
347
427
|
)
|
|
348
428
|
|
|
349
429
|
if self.plot_size is not None and not self.compact:
|
|
@@ -351,6 +431,58 @@ class BarLinePlot:
|
|
|
351
431
|
|
|
352
432
|
return fig, ax
|
|
353
433
|
|
|
434
|
+
def _baseline_on(self) -> tuple[bool, bool]:
|
|
435
|
+
"""``(primary, secondary)``: the axes that get the 0 line.
|
|
436
|
+
|
|
437
|
+
Those holding bars (they grow from 0) or, with no bars at all, the
|
|
438
|
+
primary one. Shared by :meth:`build` and :meth:`to_plotly`.
|
|
439
|
+
"""
|
|
440
|
+
on_secondary = [c in self.secondary for c in self.bars]
|
|
441
|
+
secondary = any(on_secondary)
|
|
442
|
+
primary = not all(on_secondary) or not secondary
|
|
443
|
+
return primary, secondary
|
|
444
|
+
|
|
445
|
+
def _style_secondary(
|
|
446
|
+
self, ax: Axes, ax2: Axes, font_pt: float | None
|
|
447
|
+
) -> None:
|
|
448
|
+
"""Style the right-hand axis: own format, no grid, title, z-order.
|
|
449
|
+
|
|
450
|
+
The twin is drawn above the primary by default, which would hide
|
|
451
|
+
the primary series behind secondary bars; when the secondary holds
|
|
452
|
+
bars the primary is brought forward with a transparent background.
|
|
453
|
+
"""
|
|
454
|
+
style_spines(
|
|
455
|
+
ax2,
|
|
456
|
+
decimals=self.secondary_fmt.decimals,
|
|
457
|
+
units=self.secondary_fmt.units,
|
|
458
|
+
**AX_CONFIG["spines"],
|
|
459
|
+
)
|
|
460
|
+
ax2.grid(False) # type: ignore[reportUnknownMemberType]
|
|
461
|
+
if self.compact:
|
|
462
|
+
style_compact_axes(ax2, font_pt, side="right")
|
|
463
|
+
if self.secondary_ylabel:
|
|
464
|
+
ax2.set_ylabel( # type: ignore[reportUnknownMemberType]
|
|
465
|
+
self.secondary_ylabel, fontsize=font_pt
|
|
466
|
+
)
|
|
467
|
+
if any(c in self.secondary for c in self.bars):
|
|
468
|
+
ax.set_zorder(ax2.get_zorder() + 1)
|
|
469
|
+
ax.patch.set_visible(False)
|
|
470
|
+
|
|
471
|
+
def _legend_entries(
|
|
472
|
+
self, ax: Axes, ax2: Axes | None
|
|
473
|
+
) -> tuple[list[Any], list[str]]:
|
|
474
|
+
"""Handles and labels of both axes, in declared order."""
|
|
475
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
476
|
+
if ax2 is not None:
|
|
477
|
+
more_handles, more_labels = ax2.get_legend_handles_labels()
|
|
478
|
+
handles, labels = handles + more_handles, labels + more_labels
|
|
479
|
+
order = [*self.bars.values(), *self.lines.values()]
|
|
480
|
+
pairs = sorted(
|
|
481
|
+
zip(handles, labels, strict=True),
|
|
482
|
+
key=lambda pair: order.index(pair[1]),
|
|
483
|
+
)
|
|
484
|
+
return [h for h, _ in pairs], [label for _, label in pairs]
|
|
485
|
+
|
|
354
486
|
def save(
|
|
355
487
|
self,
|
|
356
488
|
fig: Figure,
|
|
@@ -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
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/plotly_backend.py
RENAMED
|
@@ -197,10 +197,11 @@ def _legend_side(fig: go.Figure, legend: Legend | None) -> None:
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
|
|
200
|
-
def _baseline(fig: go.Figure, reference: float) -> None:
|
|
201
|
-
"""Draw the house baseline (axhline) at *reference*."""
|
|
200
|
+
def _baseline(fig: go.Figure, reference: float, *, yref: str = "y") -> None:
|
|
201
|
+
"""Draw the house baseline (axhline) at *reference* on axis *yref*."""
|
|
202
202
|
fig.add_hline(
|
|
203
203
|
y=reference,
|
|
204
|
+
yref=yref,
|
|
204
205
|
line_color=AX_CONFIG["baseline"]["color"],
|
|
205
206
|
line_width=AX_CONFIG["baseline"]["linewidth"],
|
|
206
207
|
)
|
|
@@ -302,6 +303,9 @@ def line_plot_to_figure(chart: LinePlot) -> go.Figure:
|
|
|
302
303
|
# toggles (mirrors the matplotlib artist).
|
|
303
304
|
if chart.fmt.units and chart.axis_units:
|
|
304
305
|
fig.update_yaxes(ticksuffix=chart.fmt.units)
|
|
306
|
+
# fixed or group-shared y range (see share_y_axes)
|
|
307
|
+
if chart.y_limits is not None:
|
|
308
|
+
fig.update_yaxes(range=list(chart.y_limits))
|
|
305
309
|
_legend_side(fig, chart.legend)
|
|
306
310
|
return fig
|
|
307
311
|
|
|
@@ -500,7 +504,8 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
|
|
|
500
504
|
"""Interactive ``Figure`` from a :class:`BarLinePlot`.
|
|
501
505
|
|
|
502
506
|
Bars (grouped) and marked lines share one y-axis over a categorical
|
|
503
|
-
x-axis of the same date-formatted labels the matplotlib artist uses
|
|
507
|
+
x-axis of the same date-formatted labels the matplotlib artist uses;
|
|
508
|
+
the ``secondary`` series go to an overlaid right-hand ``yaxis2``.
|
|
504
509
|
"""
|
|
505
510
|
plot_data = chart._prepare_data()
|
|
506
511
|
palette = _house_palette()
|
|
@@ -512,6 +517,9 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
|
|
|
512
517
|
default_alpha = float(BAR_LINE_CONFIG["bar"].get("alpha", 0.9))
|
|
513
518
|
line_cfg = BAR_LINE_CONFIG["line"]
|
|
514
519
|
|
|
520
|
+
def yaxis(col: str) -> str:
|
|
521
|
+
return "y2" if col in chart.secondary else "y"
|
|
522
|
+
|
|
515
523
|
for j, (col, label) in enumerate(chart.bars.items()):
|
|
516
524
|
style = chart.bar_styles.get(col, {})
|
|
517
525
|
color = (
|
|
@@ -522,6 +530,7 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
|
|
|
522
530
|
x=x,
|
|
523
531
|
y=plot_data[col].fillna(0),
|
|
524
532
|
name=label,
|
|
533
|
+
yaxis=yaxis(col),
|
|
525
534
|
marker_color=_color(color, style.get("alpha", default_alpha)),
|
|
526
535
|
)
|
|
527
536
|
)
|
|
@@ -538,6 +547,7 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
|
|
|
538
547
|
y=plot_data[col],
|
|
539
548
|
mode="lines+markers",
|
|
540
549
|
name=label,
|
|
550
|
+
yaxis=yaxis(col),
|
|
541
551
|
line={
|
|
542
552
|
"color": _color(color),
|
|
543
553
|
"width": style.get(
|
|
@@ -549,10 +559,27 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
|
|
|
549
559
|
)
|
|
550
560
|
|
|
551
561
|
fig.update_layout(barmode="group")
|
|
562
|
+
if chart.ylabel:
|
|
563
|
+
fig.update_layout(yaxis_title_text=chart.ylabel)
|
|
564
|
+
if chart.secondary:
|
|
565
|
+
fig.update_layout(
|
|
566
|
+
yaxis_side="left",
|
|
567
|
+
yaxis2={
|
|
568
|
+
"overlaying": "y",
|
|
569
|
+
"side": "right",
|
|
570
|
+
"showgrid": False,
|
|
571
|
+
"ticksuffix": chart.secondary_fmt.units,
|
|
572
|
+
"title_text": chart.secondary_ylabel,
|
|
573
|
+
},
|
|
574
|
+
)
|
|
552
575
|
if chart.baseline:
|
|
553
|
-
|
|
576
|
+
on_primary, on_secondary = chart._baseline_on()
|
|
577
|
+
if on_primary:
|
|
578
|
+
_baseline(fig, 0)
|
|
579
|
+
if on_secondary:
|
|
580
|
+
_baseline(fig, 0, yref="y2")
|
|
554
581
|
if chart.fmt.units:
|
|
555
|
-
fig.
|
|
582
|
+
fig.update_layout(yaxis_ticksuffix=chart.fmt.units)
|
|
556
583
|
_legend_side(fig, chart.legend)
|
|
557
584
|
return fig
|
|
558
585
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/intraday_plot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/artists/vector_plot.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.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/assets/tesoro.mplstyle
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/data_sources/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/dependencies/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/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
|
|
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
|
|
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
|
|
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.1.1 → tesorotools_python-0.1.3}/src/tesorotools/providers/imf_irfcl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/content.py
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/images.py
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/section.py
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/subtitle.py
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/table.py
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/render/content/text.py
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/Dockerfile.tmpl
RENAMED
|
File without changes
|
{tesorotools_python-0.1.1 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/README.md.tmpl
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
|
|
File without changes
|