tesorotools-python 0.1.0__tar.gz → 0.1.2__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.0 → tesorotools_python-0.1.2}/PKG-INFO +1 -1
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/__init__.py +2 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/__init__.py +21 -4
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/_common.py +133 -6
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/bar_line.py +18 -1
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/barh_plot.py +46 -16
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/box_plot.py +16 -1
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/line_plot.py +55 -2
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/matrix.py +19 -9
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/plotly_backend.py +8 -3
- tesorotools_python-0.1.2/src/tesorotools/artists/sharing.py +59 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/shock_plot.py +13 -1
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/stacked.py +47 -16
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/type_curve.py +21 -9
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/vector_plot.py +20 -9
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/waterfall.py +13 -1
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/.gitignore +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/pyproject.toml +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/compact.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/artists/intraday_plot.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/plots.yaml +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/template.docx +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/cli.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/scaffold.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/article/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/article/sections/01-introduccion.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/article/sections/02-analisis.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/article/sections/03-conclusiones.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/beamer/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/beamer/slides/01-contexto.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/beamer/slides/02-analisis.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/beamer/slides/03-conclusiones.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/beamer/style/beamerthemeTesoro.sty +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/book/appendices/a-fuentes.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/book/chapters/01-introduccion.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/book/chapters/02-analisis.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/book/frontmatter/resumen.tex +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/book/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/README.md.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/dot-gitignore +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/dot-vscode/settings.json +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/figures/dot-gitkeep +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/fonts/README.md +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/latexmkrc +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/refs.bib +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/style/tesoro.sty +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/style/tesorobrand.sty.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/latex/templates/common/style/tesorolocal.sty.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/palette.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/providers/fred.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/providers/lseg.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/document.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/scaffold.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/utils/template.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/__init__.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/app.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/brand.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/cli.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/components.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/formatting.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/launcher.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/palette.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/plotting.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/scaffold.py +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/Dockerfile.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/README.md.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/dot-dockerignore.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/dot-gitignore.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/dot-streamlit/config.toml.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/pyproject.toml.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/src/$package/__init__.py.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/src/$package/cli.py.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/src/$package/main.py.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/src/$package/sections.py.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/tests/__init__.py.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/src/tesorotools/web/templates/tests/test_app.py.tmpl +0 -0
- {tesorotools_python-0.1.0 → tesorotools_python-0.1.2}/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
|
|
|
@@ -12,9 +12,16 @@ Each class follows the same shape:
|
|
|
12
12
|
``data_path`` (``.feather`` on disk), never both.
|
|
13
13
|
* ``from_yaml(loader, node)`` -- builds the same instance
|
|
14
14
|
from a ``!{name}`` YAML tag.
|
|
15
|
-
* ``
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
* ``build()`` -- renders the chart in memory and returns
|
|
16
|
+
``(Figure, Axes)``; ``save(fig, path=..., dpi=...)``
|
|
17
|
+
writes it; ``plot()`` does both and closes the figure.
|
|
18
|
+
Every artist exposes the three (see :class:`Artist`).
|
|
19
|
+
* ``to_plotly()`` -- the same chart as an interactive
|
|
20
|
+
Plotly figure (all but ``IntradayPlot``);
|
|
21
|
+
:func:`plotly_unsupported` lists the keys it drops.
|
|
22
|
+
|
|
23
|
+
:func:`share_y_axes` gives the charts that declare the same
|
|
24
|
+
``y_group`` one common y scale.
|
|
18
25
|
|
|
19
26
|
Shared layout, annotation and styling helpers (plus the
|
|
20
27
|
``Format`` and ``Legend`` config holders) live in
|
|
@@ -25,13 +32,20 @@ live under :mod:`tesorotools.render`; this package is
|
|
|
25
32
|
strictly image output.
|
|
26
33
|
"""
|
|
27
34
|
|
|
28
|
-
from tesorotools.artists._common import
|
|
35
|
+
from tesorotools.artists._common import (
|
|
36
|
+
Artist,
|
|
37
|
+
FigureLifecycle,
|
|
38
|
+
Format,
|
|
39
|
+
Legend,
|
|
40
|
+
plotly_unsupported,
|
|
41
|
+
)
|
|
29
42
|
from tesorotools.artists.bar_line import BarLinePlot
|
|
30
43
|
from tesorotools.artists.barh_plot import GroupedBarChart, HorizontalBarChart
|
|
31
44
|
from tesorotools.artists.box_plot import BoxPlot
|
|
32
45
|
from tesorotools.artists.intraday_plot import IntradayPlot
|
|
33
46
|
from tesorotools.artists.line_plot import LinePlot
|
|
34
47
|
from tesorotools.artists.matrix import MatrixChart
|
|
48
|
+
from tesorotools.artists.sharing import share_y_axes
|
|
35
49
|
from tesorotools.artists.shock_plot import ShockChart
|
|
36
50
|
from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
|
|
37
51
|
from tesorotools.artists.type_curve import TypeCurve
|
|
@@ -42,6 +56,7 @@ __all__ = [
|
|
|
42
56
|
"Artist",
|
|
43
57
|
"BarLinePlot",
|
|
44
58
|
"BoxPlot",
|
|
59
|
+
"FigureLifecycle",
|
|
45
60
|
"Format",
|
|
46
61
|
"GroupedBarChart",
|
|
47
62
|
"HorizontalBarChart",
|
|
@@ -55,4 +70,6 @@ __all__ = [
|
|
|
55
70
|
"TypeCurve",
|
|
56
71
|
"VectorPlot",
|
|
57
72
|
"Waterfall",
|
|
73
|
+
"plotly_unsupported",
|
|
74
|
+
"share_y_axes",
|
|
58
75
|
]
|
|
@@ -23,13 +23,18 @@ 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
|
|
|
29
30
|
import matplotlib.pyplot as plt
|
|
30
31
|
import pandas as pd
|
|
31
32
|
from matplotlib.axes import Axes
|
|
32
|
-
from matplotlib.dates import
|
|
33
|
+
from matplotlib.dates import (
|
|
34
|
+
AutoDateLocator,
|
|
35
|
+
ConciseDateFormatter,
|
|
36
|
+
YearLocator,
|
|
37
|
+
)
|
|
33
38
|
from matplotlib.dates import (
|
|
34
39
|
date2num, # type: ignore[reportUnknownVariableType]
|
|
35
40
|
)
|
|
@@ -126,6 +131,70 @@ def dynamic_dpi(fig: Figure) -> int:
|
|
|
126
131
|
return int(round(min(max_dpi, max(min_dpi, target_px / long_in))))
|
|
127
132
|
|
|
128
133
|
|
|
134
|
+
def plotly_unsupported(artist: object) -> frozenset[str]:
|
|
135
|
+
"""Configuration keys *artist* accepts but ``to_plotly()`` drops.
|
|
136
|
+
|
|
137
|
+
*artist* is an artist class or instance. The keys change the PNG
|
|
138
|
+
but not the interactive figure; nested ones are dotted
|
|
139
|
+
(``"legend.ncol"``, ``"fmt.signed"``). A UI built on top can tell
|
|
140
|
+
the user why moving such a control leaves the figure as it is.
|
|
141
|
+
Needs no ``plotly``: it reads the class attribute
|
|
142
|
+
``PLOTLY_UNSUPPORTED``, which the test suite checks against real
|
|
143
|
+
renders.
|
|
144
|
+
"""
|
|
145
|
+
cls = artist if isinstance(artist, type) else type(artist)
|
|
146
|
+
if not hasattr(cls, "to_plotly"):
|
|
147
|
+
raise TypeError(f"{cls.__name__} has no to_plotly()")
|
|
148
|
+
declared: frozenset[str] | None = getattr(cls, "PLOTLY_UNSUPPORTED", None)
|
|
149
|
+
if declared is None:
|
|
150
|
+
raise TypeError(f"{cls.__name__} does not declare PLOTLY_UNSUPPORTED")
|
|
151
|
+
return frozenset(declared)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class FigureLifecycle:
|
|
155
|
+
"""``save()`` and ``plot()`` on top of a subclass's ``build()``.
|
|
156
|
+
|
|
157
|
+
Gives an artist the same three-step life cycle as the rest:
|
|
158
|
+
``build()`` returns the figure in memory, ``save()`` writes it
|
|
159
|
+
and ``plot()`` does both and closes the figure. A subclass only
|
|
160
|
+
implements ``build()``.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
out_path: Path
|
|
164
|
+
|
|
165
|
+
def build(self) -> tuple[Figure, Axes]:
|
|
166
|
+
raise NotImplementedError
|
|
167
|
+
|
|
168
|
+
def save(
|
|
169
|
+
self,
|
|
170
|
+
fig: Figure,
|
|
171
|
+
*,
|
|
172
|
+
path: Path | None = None,
|
|
173
|
+
dpi: int | None = None,
|
|
174
|
+
) -> Path:
|
|
175
|
+
"""Persist *fig* as a PNG and return the path written.
|
|
176
|
+
|
|
177
|
+
Defaults to ``self.out_path``; pass ``path`` to redirect.
|
|
178
|
+
``dpi`` overrides the save resolution; when omitted it is
|
|
179
|
+
derived from the figure size via :func:`dynamic_dpi`.
|
|
180
|
+
"""
|
|
181
|
+
target: Path = path if path is not None else self.out_path
|
|
182
|
+
save_dpi: int = dpi if dpi is not None else dynamic_dpi(fig)
|
|
183
|
+
fig.savefig( # type: ignore[reportUnknownMemberType]
|
|
184
|
+
target, dpi=save_dpi
|
|
185
|
+
)
|
|
186
|
+
return target
|
|
187
|
+
|
|
188
|
+
def plot(self) -> Axes:
|
|
189
|
+
"""Build the chart, persist it to ``self.out_path`` and close it."""
|
|
190
|
+
fig, ax = self.build()
|
|
191
|
+
try:
|
|
192
|
+
self.save(fig)
|
|
193
|
+
finally:
|
|
194
|
+
plt.close(fig)
|
|
195
|
+
return ax
|
|
196
|
+
|
|
197
|
+
|
|
129
198
|
# ----------------------------------------------------------------------
|
|
130
199
|
# Structural contract for image artists
|
|
131
200
|
# ----------------------------------------------------------------------
|
|
@@ -137,13 +206,21 @@ class Artist(Protocol):
|
|
|
137
206
|
|
|
138
207
|
The built-in artists and any third-party artist
|
|
139
208
|
registered via :func:`tesorotools.register_artist`
|
|
140
|
-
expose the same
|
|
209
|
+
expose the same surface:
|
|
141
210
|
|
|
142
211
|
* ``out_path: Path`` -- the ``.png`` destination.
|
|
143
212
|
* ``from_yaml(loader, node) -> Self`` -- the YAML
|
|
144
213
|
constructor bound to the ``!{name}`` tag.
|
|
145
|
-
* ``
|
|
146
|
-
|
|
214
|
+
* ``build() -> (Figure, Axes)`` -- render the chart in
|
|
215
|
+
memory, without touching the disk.
|
|
216
|
+
* ``save(fig, *, path=None, dpi=None) -> Path`` -- write
|
|
217
|
+
a built figure (to ``out_path`` unless ``path`` says
|
|
218
|
+
otherwise).
|
|
219
|
+
* ``plot() -> Axes`` -- ``build()`` + ``save()``, close
|
|
220
|
+
the figure and return the axes for inspection.
|
|
221
|
+
|
|
222
|
+
:class:`FigureLifecycle` provides ``save()`` and ``plot()``
|
|
223
|
+
for an artist that only implements ``build()``.
|
|
147
224
|
|
|
148
225
|
The Protocol is ``runtime_checkable`` so consumers can
|
|
149
226
|
use ``isinstance(obj, Artist)`` to validate a candidate
|
|
@@ -156,6 +233,16 @@ class Artist(Protocol):
|
|
|
156
233
|
@classmethod
|
|
157
234
|
def from_yaml(cls, loader: Any, node: MappingNode) -> Self: ...
|
|
158
235
|
|
|
236
|
+
def build(self) -> tuple[Figure, Axes]: ...
|
|
237
|
+
|
|
238
|
+
def save(
|
|
239
|
+
self,
|
|
240
|
+
fig: Figure,
|
|
241
|
+
*,
|
|
242
|
+
path: Path | None = None,
|
|
243
|
+
dpi: int | None = None,
|
|
244
|
+
) -> Path: ...
|
|
245
|
+
|
|
159
246
|
def plot(self) -> Axes: ...
|
|
160
247
|
|
|
161
248
|
|
|
@@ -580,6 +667,12 @@ def date_span_ticks(
|
|
|
580
667
|
10 % of the span is dropped so the two labels do not collide. Without
|
|
581
668
|
it the plain in-span candidates are returned (falling back to the two
|
|
582
669
|
endpoints when the locator offers nothing inside).
|
|
670
|
+
|
|
671
|
+
When the locator steps by several years, the pinned grid is anchored to
|
|
672
|
+
the **start year** instead of to multiples of the step: a 2-year step
|
|
673
|
+
over 2023-2027 gives 2023, 2025, 2027 rather than the pinned 2023 plus
|
|
674
|
+
the calendar 2024, 2026, whose uneven gaps (and missing odd years) read
|
|
675
|
+
as if the axis skipped a year.
|
|
583
676
|
"""
|
|
584
677
|
lo: float = float(cast(Any, date2num(data_min)))
|
|
585
678
|
hi: float = float(cast(Any, date2num(data_max)))
|
|
@@ -589,6 +682,18 @@ def date_span_ticks(
|
|
|
589
682
|
)
|
|
590
683
|
if not pin_start:
|
|
591
684
|
return [t for t in candidates if lo <= t <= hi] or [lo, hi]
|
|
685
|
+
chosen: Any = locator.get_locator( # type: ignore[reportUnknownMemberType]
|
|
686
|
+
data_min, data_max
|
|
687
|
+
)
|
|
688
|
+
if isinstance(chosen, YearLocator):
|
|
689
|
+
step = int(cast(Any, chosen.base).step)
|
|
690
|
+
if step > 1:
|
|
691
|
+
first_year: int = num2date(lo).year
|
|
692
|
+
last_year: int = num2date(hi).year
|
|
693
|
+
candidates = [
|
|
694
|
+
float(cast(Any, date2num(pd.Timestamp(year=y, month=1, day=1))))
|
|
695
|
+
for y in range(first_year, last_year + 1, step)
|
|
696
|
+
]
|
|
592
697
|
min_gap: float = (hi - lo) * 0.10
|
|
593
698
|
interior: list[float] = [
|
|
594
699
|
t for t in candidates if lo < t <= hi and t - lo >= min_gap
|
|
@@ -1195,6 +1300,26 @@ def style_spines(
|
|
|
1195
1300
|
)
|
|
1196
1301
|
|
|
1197
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
|
+
|
|
1198
1323
|
def style_baseline(
|
|
1199
1324
|
ax: Axes,
|
|
1200
1325
|
reference: float = 0,
|
|
@@ -1235,6 +1360,7 @@ def annotate_last_values(
|
|
|
1235
1360
|
fontsize: float | None = None,
|
|
1236
1361
|
right_pad_px: float = 10.0,
|
|
1237
1362
|
headroom_pad_pt: float = 0.0,
|
|
1363
|
+
grow_top: bool = True,
|
|
1238
1364
|
) -> None:
|
|
1239
1365
|
"""Label the last non-NaN value of each column on the right.
|
|
1240
1366
|
|
|
@@ -1261,7 +1387,8 @@ def annotate_last_values(
|
|
|
1261
1387
|
y-limit, that limit grows enough to keep this many points
|
|
1262
1388
|
free and the labels are re-packed at the new scale, so a
|
|
1263
1389
|
tall stack never spills into a legend above the axes (nor
|
|
1264
|
-
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).
|
|
1265
1392
|
"""
|
|
1266
1393
|
fig = ax.get_figure()
|
|
1267
1394
|
if fig is None:
|
|
@@ -1333,7 +1460,7 @@ def annotate_last_values(
|
|
|
1333
1460
|
pad_px = headroom_pad_pt * fig.dpi / 72.0
|
|
1334
1461
|
top_axes_px: float = trans.transform((0, ymax))[1] # type: ignore[reportUnknownArgumentType]
|
|
1335
1462
|
needed_px = placements[-1] + text_height / 2 + pad_px
|
|
1336
|
-
if needed_px > top_axes_px:
|
|
1463
|
+
if grow_top and needed_px > top_axes_px:
|
|
1337
1464
|
new_top: float = trans.inverted().transform( # type: ignore[reportUnknownArgumentType]
|
|
1338
1465
|
(0, needed_px)
|
|
1339
1466
|
)[1]
|
|
@@ -28,7 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
|
|
29
29
|
import locale
|
|
30
30
|
from pathlib import Path
|
|
31
|
-
from typing import Any, Self
|
|
31
|
+
from typing import Any, ClassVar, Self
|
|
32
32
|
|
|
33
33
|
import matplotlib.pyplot as plt
|
|
34
34
|
import numpy as np
|
|
@@ -89,6 +89,23 @@ class BarLinePlot:
|
|
|
89
89
|
(``None`` labels every slot).
|
|
90
90
|
"""
|
|
91
91
|
|
|
92
|
+
#: Keys that change the PNG but never the ``to_plotly()`` figure
|
|
93
|
+
#: (see :func:`plotly_unsupported`); checked against real renders
|
|
94
|
+
#: by ``tests/test_plotly_support.py``.
|
|
95
|
+
PLOTLY_UNSUPPORTED: ClassVar[frozenset[str]] = frozenset(
|
|
96
|
+
{
|
|
97
|
+
"bar_width",
|
|
98
|
+
"compact",
|
|
99
|
+
"figsize",
|
|
100
|
+
"fmt.decimals",
|
|
101
|
+
"font_pt",
|
|
102
|
+
"legend.ncol",
|
|
103
|
+
"max_xticks",
|
|
104
|
+
"plot_size",
|
|
105
|
+
"x_rotation",
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
92
109
|
def __init__(
|
|
93
110
|
self,
|
|
94
111
|
out_path: Path,
|
|
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
from enum import Enum
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Any, Self
|
|
21
|
+
from typing import Any, ClassVar, Self
|
|
22
22
|
|
|
23
23
|
import matplotlib.patheffects as pe
|
|
24
24
|
import matplotlib.pyplot as plt
|
|
@@ -32,6 +32,7 @@ from matplotlib.ticker import FuncFormatter
|
|
|
32
32
|
from yaml.nodes import MappingNode
|
|
33
33
|
|
|
34
34
|
from tesorotools.artists._common import (
|
|
35
|
+
FigureLifecycle,
|
|
35
36
|
AX_CONFIG,
|
|
36
37
|
Format,
|
|
37
38
|
Legend,
|
|
@@ -39,7 +40,6 @@ from tesorotools.artists._common import (
|
|
|
39
40
|
compact_barh_figsize,
|
|
40
41
|
compact_figure_kwargs,
|
|
41
42
|
compact_font_pt,
|
|
42
|
-
dynamic_dpi,
|
|
43
43
|
legend_layout,
|
|
44
44
|
place_legend,
|
|
45
45
|
resolve_data,
|
|
@@ -447,7 +447,7 @@ def _place_in_or_out_labels(
|
|
|
447
447
|
return outside
|
|
448
448
|
|
|
449
449
|
|
|
450
|
-
class HorizontalBarChart:
|
|
450
|
+
class HorizontalBarChart(FigureLifecycle):
|
|
451
451
|
"""Horizontal bar chart artist.
|
|
452
452
|
|
|
453
453
|
Parameters
|
|
@@ -507,6 +507,19 @@ class HorizontalBarChart:
|
|
|
507
507
|
outside; pragmatic but mixes the two looks).
|
|
508
508
|
"""
|
|
509
509
|
|
|
510
|
+
#: Keys that change the PNG but never the ``to_plotly()`` figure
|
|
511
|
+
#: (see :func:`plotly_unsupported`); checked against real renders
|
|
512
|
+
#: by ``tests/test_plotly_support.py``.
|
|
513
|
+
PLOTLY_UNSUPPORTED: ClassVar[frozenset[str]] = frozenset(
|
|
514
|
+
{
|
|
515
|
+
"color_value_labels",
|
|
516
|
+
"compact",
|
|
517
|
+
"figsize",
|
|
518
|
+
"font_pt",
|
|
519
|
+
"value_label_position",
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
|
|
510
523
|
def __init__(
|
|
511
524
|
self,
|
|
512
525
|
out_path: Path,
|
|
@@ -641,7 +654,11 @@ class HorizontalBarChart:
|
|
|
641
654
|
frame = frame.sort_values(by=_Col.VALUE.value)
|
|
642
655
|
return frame
|
|
643
656
|
|
|
644
|
-
def
|
|
657
|
+
def build(self) -> tuple[Figure, Axes]:
|
|
658
|
+
"""Render the chart in memory; :meth:`save` writes it.
|
|
659
|
+
|
|
660
|
+
:meth:`plot` (from :class:`FigureLifecycle`) does both.
|
|
661
|
+
"""
|
|
645
662
|
"""Render the chart and persist it to ``self.out_path``."""
|
|
646
663
|
frame = self._build_frame()
|
|
647
664
|
|
|
@@ -689,14 +706,10 @@ class HorizontalBarChart:
|
|
|
689
706
|
axis="both", labelsize=font_pt
|
|
690
707
|
)
|
|
691
708
|
|
|
692
|
-
fig
|
|
693
|
-
self.out_path, dpi=dynamic_dpi(fig)
|
|
694
|
-
)
|
|
695
|
-
plt.close(fig)
|
|
696
|
-
return ax
|
|
709
|
+
return fig, ax
|
|
697
710
|
|
|
698
711
|
|
|
699
|
-
class GroupedBarChart:
|
|
712
|
+
class GroupedBarChart(FigureLifecycle):
|
|
700
713
|
"""Grouped (clustered) horizontal bar chart artist.
|
|
701
714
|
|
|
702
715
|
Compares two or more series side-by-side within each
|
|
@@ -764,6 +777,23 @@ class GroupedBarChart:
|
|
|
764
777
|
see :class:`HorizontalBarChart`.
|
|
765
778
|
"""
|
|
766
779
|
|
|
780
|
+
#: Keys that change the PNG but never the ``to_plotly()`` figure
|
|
781
|
+
#: (see :func:`plotly_unsupported`); checked against real renders
|
|
782
|
+
#: by ``tests/test_plotly_support.py``.
|
|
783
|
+
PLOTLY_UNSUPPORTED: ClassVar[frozenset[str]] = frozenset(
|
|
784
|
+
{
|
|
785
|
+
"annotate",
|
|
786
|
+
"color_value_labels",
|
|
787
|
+
"compact",
|
|
788
|
+
"figsize",
|
|
789
|
+
"font_pt",
|
|
790
|
+
"group_width",
|
|
791
|
+
"legend.ncol",
|
|
792
|
+
"plot_size",
|
|
793
|
+
"value_label_position",
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
|
|
767
797
|
def __init__(
|
|
768
798
|
self,
|
|
769
799
|
out_path: Path,
|
|
@@ -867,7 +897,11 @@ class GroupedBarChart:
|
|
|
867
897
|
|
|
868
898
|
return grouped_bar_to_figure(self)
|
|
869
899
|
|
|
870
|
-
def
|
|
900
|
+
def build(self) -> tuple[Figure, Axes]:
|
|
901
|
+
"""Render the chart in memory; :meth:`save` writes it.
|
|
902
|
+
|
|
903
|
+
:meth:`plot` (from :class:`FigureLifecycle`) does both.
|
|
904
|
+
"""
|
|
871
905
|
"""Render the chart and persist it to ``self.out_path``."""
|
|
872
906
|
cat_ids = list(self.categories.keys())
|
|
873
907
|
ser_ids = list(self.series.keys())
|
|
@@ -941,8 +975,4 @@ class GroupedBarChart:
|
|
|
941
975
|
if self.plot_size is not None and not self.compact:
|
|
942
976
|
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
|
943
977
|
|
|
944
|
-
fig
|
|
945
|
-
self.out_path, dpi=dynamic_dpi(fig)
|
|
946
|
-
)
|
|
947
|
-
plt.close(fig)
|
|
948
|
-
return ax
|
|
978
|
+
return fig, ax
|
|
@@ -32,7 +32,7 @@ The last-value marker is a circle by default and fully configurable
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
34
|
from pathlib import Path
|
|
35
|
-
from typing import Any, Self
|
|
35
|
+
from typing import Any, ClassVar, Self
|
|
36
36
|
|
|
37
37
|
import matplotlib.pyplot as plt
|
|
38
38
|
import numpy as np
|
|
@@ -129,6 +129,21 @@ class BoxPlot:
|
|
|
129
129
|
* ``legend_labels`` -- override the four proxy labels.
|
|
130
130
|
"""
|
|
131
131
|
|
|
132
|
+
#: Keys that change the PNG but never the ``to_plotly()`` figure
|
|
133
|
+
#: (see :func:`plotly_unsupported`); checked against real renders
|
|
134
|
+
#: by ``tests/test_plotly_support.py``.
|
|
135
|
+
PLOTLY_UNSUPPORTED: ClassVar[frozenset[str]] = frozenset(
|
|
136
|
+
{
|
|
137
|
+
"compact",
|
|
138
|
+
"figsize",
|
|
139
|
+
"fmt.decimals",
|
|
140
|
+
"font_pt",
|
|
141
|
+
"legend.ncol",
|
|
142
|
+
"plot_size",
|
|
143
|
+
"x_rotation",
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
132
147
|
def __init__(
|
|
133
148
|
self,
|
|
134
149
|
out_path: Path,
|
|
@@ -11,8 +11,9 @@ 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
|
-
from typing import Any, Self
|
|
16
|
+
from typing import Any, ClassVar, Self
|
|
16
17
|
|
|
17
18
|
import matplotlib.pyplot as plt
|
|
18
19
|
import pandas as pd
|
|
@@ -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,12 +162,43 @@ 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``;
|
|
166
177
|
``plot()`` is the thin convenience that does both.
|
|
167
178
|
"""
|
|
168
179
|
|
|
180
|
+
#: Keys that change the PNG but never the ``to_plotly()`` figure
|
|
181
|
+
#: (see :func:`plotly_unsupported`); checked against real renders
|
|
182
|
+
#: by ``tests/test_plotly_support.py``.
|
|
183
|
+
PLOTLY_UNSUPPORTED: ClassVar[frozenset[str]] = frozenset(
|
|
184
|
+
{
|
|
185
|
+
"adapt_height",
|
|
186
|
+
"axis_decimals",
|
|
187
|
+
"compact",
|
|
188
|
+
"date_density",
|
|
189
|
+
"date_rotation",
|
|
190
|
+
"date_show_start",
|
|
191
|
+
"figsize",
|
|
192
|
+
"font_pt",
|
|
193
|
+
"legend.ncol",
|
|
194
|
+
"means_window",
|
|
195
|
+
"plot_size",
|
|
196
|
+
"ref_lines",
|
|
197
|
+
"show_means",
|
|
198
|
+
"vlines",
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
169
202
|
def __init__(
|
|
170
203
|
self,
|
|
171
204
|
out_path: Path,
|
|
@@ -201,9 +234,16 @@ class LinePlot:
|
|
|
201
234
|
ref_lines: list[dict[str, Any]] | None = None,
|
|
202
235
|
show_means: bool = False,
|
|
203
236
|
means_window: tuple[Any, Any] | None = None,
|
|
237
|
+
y_limits: Sequence[float] | None = None,
|
|
238
|
+
y_group: str | None = None,
|
|
204
239
|
) -> None:
|
|
205
240
|
if out_path.suffix != ".png":
|
|
206
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
|
+
)
|
|
207
247
|
if base_100 and base_0:
|
|
208
248
|
raise ValueError(
|
|
209
249
|
"base_100 and base_0 are mutually exclusive: a series is "
|
|
@@ -247,6 +287,8 @@ class LinePlot:
|
|
|
247
287
|
self.ref_lines = ref_lines or []
|
|
248
288
|
self.show_means = show_means
|
|
249
289
|
self.means_window = means_window
|
|
290
|
+
self.y_limits = check_y_limits(y_limits)
|
|
291
|
+
self.y_group = y_group
|
|
250
292
|
|
|
251
293
|
@property
|
|
252
294
|
def _pin_start_date(self) -> bool:
|
|
@@ -302,7 +344,8 @@ class LinePlot:
|
|
|
302
344
|
``axis_decimals``, ``axis_units``, ``annotate_units``,
|
|
303
345
|
``date_density``, ``date_rotation``, ``date_show_start``,
|
|
304
346
|
``adapt_height``,
|
|
305
|
-
``ref_lines``, ``show_means``, ``means_window
|
|
347
|
+
``ref_lines``, ``show_means``, ``means_window``,
|
|
348
|
+
``y_limits``, ``y_group``.
|
|
306
349
|
|
|
307
350
|
Example
|
|
308
351
|
-------
|
|
@@ -440,6 +483,11 @@ class LinePlot:
|
|
|
440
483
|
# data point. annotate_last_values reopens room on the right.
|
|
441
484
|
ax.margins(x=0)
|
|
442
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
|
+
|
|
443
491
|
if self.vlines:
|
|
444
492
|
draw_vlines(ax, self.vlines)
|
|
445
493
|
|
|
@@ -472,6 +520,7 @@ class LinePlot:
|
|
|
472
520
|
headroom_pad_pt=(
|
|
473
521
|
compact_headroom_pad_pt() if self.compact else 0.0
|
|
474
522
|
),
|
|
523
|
+
grow_top=self.y_limits is None,
|
|
475
524
|
)
|
|
476
525
|
|
|
477
526
|
style_spines(
|
|
@@ -514,6 +563,10 @@ class LinePlot:
|
|
|
514
563
|
)
|
|
515
564
|
lo, hi = ax.get_ylim()
|
|
516
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)
|
|
517
570
|
|
|
518
571
|
# per-series historical mean as a dotted same-colour line (the old
|
|
519
572
|
# `medias=True`): the mean is taken over means_window (or the whole
|
|
@@ -27,7 +27,7 @@ Styling constants come from ``PLOT_CONFIG['matrix']``.
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import Any, Self
|
|
30
|
+
from typing import Any, ClassVar, Self
|
|
31
31
|
|
|
32
32
|
import matplotlib as mpl
|
|
33
33
|
import matplotlib.pyplot as plt
|
|
@@ -40,9 +40,9 @@ from matplotlib.patches import Rectangle
|
|
|
40
40
|
from yaml.nodes import MappingNode
|
|
41
41
|
|
|
42
42
|
from tesorotools.artists._common import (
|
|
43
|
+
FigureLifecycle,
|
|
43
44
|
FIG_CONFIG,
|
|
44
45
|
Format,
|
|
45
|
-
dynamic_dpi,
|
|
46
46
|
resolve_data,
|
|
47
47
|
)
|
|
48
48
|
from tesorotools.utils.matplotlib import PLOT_CONFIG, format_annotation
|
|
@@ -66,7 +66,7 @@ def _text_color(rgba: tuple[float, float, float, float]) -> str:
|
|
|
66
66
|
return MATRIX_CONFIG["dark_text"]
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
class MatrixChart:
|
|
69
|
+
class MatrixChart(FigureLifecycle):
|
|
70
70
|
"""Annotated heatmap-table artist (any ``n_rows x n_cols``).
|
|
71
71
|
|
|
72
72
|
Parameters
|
|
@@ -102,6 +102,16 @@ class MatrixChart:
|
|
|
102
102
|
cells read as tiles.
|
|
103
103
|
"""
|
|
104
104
|
|
|
105
|
+
#: Keys that change the PNG but never the ``to_plotly()`` figure
|
|
106
|
+
#: (see :func:`plotly_unsupported`); checked against real renders
|
|
107
|
+
#: by ``tests/test_plotly_support.py``.
|
|
108
|
+
PLOTLY_UNSUPPORTED: ClassVar[frozenset[str]] = frozenset(
|
|
109
|
+
{
|
|
110
|
+
"cmap",
|
|
111
|
+
"figsize",
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
105
115
|
def __init__(
|
|
106
116
|
self,
|
|
107
117
|
out_path: Path,
|
|
@@ -199,7 +209,11 @@ class MatrixChart:
|
|
|
199
209
|
|
|
200
210
|
return matrix_to_figure(self)
|
|
201
211
|
|
|
202
|
-
def
|
|
212
|
+
def build(self) -> tuple[Figure, Axes]:
|
|
213
|
+
"""Render the chart in memory; :meth:`save` writes it.
|
|
214
|
+
|
|
215
|
+
:meth:`plot` (from :class:`FigureLifecycle`) does both.
|
|
216
|
+
"""
|
|
203
217
|
"""Render the chart and persist it to ``self.out_path``."""
|
|
204
218
|
row_ids = list(self.rows)
|
|
205
219
|
col_ids = list(self.cols)
|
|
@@ -260,11 +274,7 @@ class MatrixChart:
|
|
|
260
274
|
)
|
|
261
275
|
|
|
262
276
|
self._style_axes(ax, row_ids, col_ids)
|
|
263
|
-
fig
|
|
264
|
-
self.out_path, dpi=dynamic_dpi(fig)
|
|
265
|
-
)
|
|
266
|
-
plt.close(fig)
|
|
267
|
-
return ax
|
|
277
|
+
return fig, ax
|
|
268
278
|
|
|
269
279
|
def _annotate_cell(
|
|
270
280
|
self,
|