tesorotools-python 0.1.6__tar.gz → 0.1.8__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.6 → tesorotools_python-0.1.8}/PKG-INFO +2 -1
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/pyproject.toml +2 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/_common.py +322 -4
- tesorotools_python-0.1.8/src/tesorotools/artists/_dates.py +238 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/line_plot.py +54 -16
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/plotly_backend.py +17 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/stacked.py +22 -7
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/plots.yaml +27 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/lint.py +5 -1
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/palette.py +4 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/__init__.py +82 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/builder.py +1175 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/charts.py +61 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/cli.py +283 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/house.py +651 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/lint.py +183 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/markdown.py +950 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/pdf.py +68 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/skeleton.md +81 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/skill/SKILL.md +423 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/skill/reglas.md +159 -0
- tesorotools_python-0.1.8/src/tesorotools/presentation/template.py +78 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/providers/lseg.py +42 -14
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/.gitignore +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/bar_line.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/barh_plot.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/box_plot.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/compact.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/intraday_plot.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/matrix.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/sankey.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/scatter.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/sharing.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/shock_plot.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/tikz_backend.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/type_curve.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/vector_plot.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/artists/waterfall.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/nota_template.dotx +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/template.docx +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/cli.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/scaffold.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/article/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/article/sections/01-introduccion.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/article/sections/02-analisis.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/article/sections/03-conclusiones.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/beamer/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/beamer/slides/01-contexto.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/beamer/slides/02-analisis.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/beamer/slides/03-conclusiones.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/beamer/style/beamerthemeTesoro.sty +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/book/appendices/a-fuentes.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/book/chapters/01-introduccion.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/book/chapters/02-analisis.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/book/frontmatter/resumen.tex +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/book/main.tex.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/README.md.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/dot-gitignore +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/dot-vscode/settings.json +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/figures/dot-gitkeep +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/fonts/README.md +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/latexmkrc +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/refs.bib +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/style/tesoro.sty +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/style/tesorobrand.sty.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/latex/templates/common/style/tesorolocal.sty.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/builder.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/charts.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/cli.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/inline.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/issues.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/markdown.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/pdf.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/skeleton.md +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/skill/SKILL.md +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/skill/reglas.md +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/note/template.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/providers/fred.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/document.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/scaffold.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/utils/template.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/__init__.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/app.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/brand.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/cli.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/components.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/formatting.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/launcher.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/palette.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/plotting.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/scaffold.py +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/Dockerfile.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/README.md.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/dot-dockerignore.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/dot-gitignore.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/dot-streamlit/config.toml.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/pyproject.toml.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/src/$package/__init__.py.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/src/$package/cli.py.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/src/$package/main.py.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/src/$package/sections.py.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/tests/__init__.py.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/templates/tests/test_app.py.tmpl +0 -0
- {tesorotools_python-0.1.6 → tesorotools_python-0.1.8}/src/tesorotools/web/theme.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tesorotools-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Requires-Python: >=3.13
|
|
5
5
|
Requires-Dist: babel>=2.17
|
|
6
6
|
Requires-Dist: matplotlib>=3.10
|
|
@@ -9,6 +9,7 @@ Requires-Dist: pandas>=2.2
|
|
|
9
9
|
Requires-Dist: psycopg[binary]>=3.1
|
|
10
10
|
Requires-Dist: pyarrow>=18.0
|
|
11
11
|
Requires-Dist: python-docx>=1.1
|
|
12
|
+
Requires-Dist: python-pptx>=1.0
|
|
12
13
|
Requires-Dist: pywin32>=311; sys_platform == 'win32'
|
|
13
14
|
Requires-Dist: pyyaml>=6.0
|
|
14
15
|
Requires-Dist: sqlalchemy>=2.0
|
|
@@ -19,6 +19,7 @@ dependencies = [
|
|
|
19
19
|
# data visualization
|
|
20
20
|
"matplotlib>=3.10",
|
|
21
21
|
"python-docx>=1.1",
|
|
22
|
+
"python-pptx>=1.0",
|
|
22
23
|
|
|
23
24
|
# os dependencies
|
|
24
25
|
"pywin32>=311; sys_platform == 'win32'",
|
|
@@ -39,6 +40,7 @@ web = ["streamlit>=1.62", "plotly>=5.24"]
|
|
|
39
40
|
tesorotools-web = "tesorotools.web.cli:main"
|
|
40
41
|
tesorotools-latex = "tesorotools.latex.cli:main"
|
|
41
42
|
tesorotools-nota = "tesorotools.note.cli:main"
|
|
43
|
+
tesorotools-presentacion = "tesorotools.presentation.cli:main"
|
|
42
44
|
|
|
43
45
|
[dependency-groups]
|
|
44
46
|
dev = [
|
|
@@ -47,9 +47,21 @@ from matplotlib.dates import (
|
|
|
47
47
|
from matplotlib.figure import Figure
|
|
48
48
|
from matplotlib.font_manager import FontProperties
|
|
49
49
|
from matplotlib.legend import Legend as MplLegend
|
|
50
|
-
from matplotlib.
|
|
50
|
+
from matplotlib.text import Text
|
|
51
|
+
from matplotlib.ticker import (
|
|
52
|
+
FixedFormatter,
|
|
53
|
+
FixedLocator,
|
|
54
|
+
FuncFormatter,
|
|
55
|
+
MaxNLocator,
|
|
56
|
+
NullFormatter,
|
|
57
|
+
)
|
|
51
58
|
from yaml.nodes import MappingNode
|
|
52
59
|
|
|
60
|
+
from tesorotools.artists._dates import (
|
|
61
|
+
DateLayout,
|
|
62
|
+
date_layouts,
|
|
63
|
+
period_label,
|
|
64
|
+
)
|
|
53
65
|
from tesorotools.utils.matplotlib import (
|
|
54
66
|
PLOT_CONFIG,
|
|
55
67
|
apply_house_style,
|
|
@@ -110,6 +122,53 @@ def date_maxticks(density: str, *, compact: bool) -> int:
|
|
|
110
122
|
return int(table.get(density, fallback[density]))
|
|
111
123
|
|
|
112
124
|
|
|
125
|
+
# Most date labels an axis shows per date_density level, read from
|
|
126
|
+
# ``compact.date_max_labels`` / ``line.date_max_labels`` in ``plots.yaml``:
|
|
127
|
+
# the finest calendar step (every year, every month...) whose labels fit in
|
|
128
|
+
# the axis width AND stay within this count wins.
|
|
129
|
+
_DATE_MAX_LABELS_FALLBACK_COMPACT: dict[str, int] = {
|
|
130
|
+
"few": 4,
|
|
131
|
+
"default": 8,
|
|
132
|
+
"many": 14,
|
|
133
|
+
}
|
|
134
|
+
_DATE_MAX_LABELS_FALLBACK_LINE: dict[str, int] = {
|
|
135
|
+
"few": 6,
|
|
136
|
+
"default": 15,
|
|
137
|
+
"many": 30,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def date_max_labels(density: str, *, compact: bool) -> int:
|
|
142
|
+
"""Most date labels a ``date_density`` level allows on the x-axis.
|
|
143
|
+
|
|
144
|
+
Reads ``compact.date_max_labels`` / ``line.date_max_labels`` from
|
|
145
|
+
``plots.yaml``, falling back to the built-in table when the config
|
|
146
|
+
(or a level) is absent.
|
|
147
|
+
"""
|
|
148
|
+
cfg = COMPACT_CONFIG if compact else LINE_CONFIG
|
|
149
|
+
fallback = (
|
|
150
|
+
_DATE_MAX_LABELS_FALLBACK_COMPACT
|
|
151
|
+
if compact
|
|
152
|
+
else _DATE_MAX_LABELS_FALLBACK_LINE
|
|
153
|
+
)
|
|
154
|
+
table: dict[str, Any] = cfg.get("date_max_labels", fallback)
|
|
155
|
+
return int(table.get(density, fallback[density]))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _date_axis_config(*, compact: bool) -> tuple[int, float, float]:
|
|
159
|
+
"""Calendar date-axis knobs of a mode, from ``plots.yaml``.
|
|
160
|
+
|
|
161
|
+
``(date_monthly_max_months, date_centered_min_fraction,
|
|
162
|
+
date_label_gap_em)``.
|
|
163
|
+
"""
|
|
164
|
+
cfg = COMPACT_CONFIG if compact else LINE_CONFIG
|
|
165
|
+
return (
|
|
166
|
+
int(cfg.get("date_monthly_max_months", 24)),
|
|
167
|
+
float(cfg.get("date_centered_min_fraction", 0.5)),
|
|
168
|
+
float(cfg.get("date_label_gap_em", 0.3)),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
113
172
|
def dynamic_dpi(fig: Figure) -> int:
|
|
114
173
|
"""Save DPI adapted to the figure's physical size.
|
|
115
174
|
|
|
@@ -804,6 +863,188 @@ def _date_label_ha(
|
|
|
804
863
|
return "right" if rotation > 0 else "left"
|
|
805
864
|
|
|
806
865
|
|
|
866
|
+
def _style_date_labels(ax: Axes, rot: float, *, minor: bool) -> None:
|
|
867
|
+
"""Tilt the date labels *rot* degrees with the matching anchor.
|
|
868
|
+
|
|
869
|
+
Rotation goes in the axis tick kwargs; ``ha`` and the anchor rotation
|
|
870
|
+
mode are per-label Text properties, so they are reapplied after every
|
|
871
|
+
draw or locator swap.
|
|
872
|
+
"""
|
|
873
|
+
ha = _date_label_ha(None if rot == 0 else rot)
|
|
874
|
+
ax.tick_params( # type: ignore[reportUnknownMemberType]
|
|
875
|
+
axis="x", which="minor" if minor else "major", rotation=rot
|
|
876
|
+
)
|
|
877
|
+
for label in ax.get_xticklabels(minor=minor):
|
|
878
|
+
label.set_horizontalalignment(ha)
|
|
879
|
+
if rot != 0:
|
|
880
|
+
label.set_rotation_mode("anchor")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _layout_texts(layout: DateLayout, *, pin_start: bool) -> list[str]:
|
|
884
|
+
"""The label texts of *layout*, as the axis will print them."""
|
|
885
|
+
if layout.centered:
|
|
886
|
+
# centred: each label names its period
|
|
887
|
+
return [
|
|
888
|
+
period_label(p, layout.step, first=i == 0)
|
|
889
|
+
for i, p in enumerate(layout.periods)
|
|
890
|
+
]
|
|
891
|
+
values = [float(cast(Any, date2num(t))) for t in layout.ticks]
|
|
892
|
+
formatter = _tick_formatter(FixedLocator(values), values[0], pin_start)
|
|
893
|
+
return list(formatter.format_ticks(values))
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _tick_formatter(
|
|
897
|
+
locator: FixedLocator, start: float, pin_start: bool
|
|
898
|
+
) -> ConciseDateFormatter:
|
|
899
|
+
return (
|
|
900
|
+
_StartLabeledConcise(locator, start)
|
|
901
|
+
if pin_start
|
|
902
|
+
else ConciseDateFormatter(locator)
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def calendar_date_axis(
|
|
907
|
+
ax: Axes,
|
|
908
|
+
layouts: list[DateLayout],
|
|
909
|
+
*,
|
|
910
|
+
pin_start: bool,
|
|
911
|
+
rotation: float | None = None,
|
|
912
|
+
auto_rotate: bool = False,
|
|
913
|
+
gap_em: float = 0.3,
|
|
914
|
+
) -> DateLayout:
|
|
915
|
+
"""Label every year (or month) the axis can hold.
|
|
916
|
+
|
|
917
|
+
*layouts* are the candidate calendar steps, finest first (see
|
|
918
|
+
:func:`tesorotools.artists._dates.date_layouts`). Each one's labels
|
|
919
|
+
are measured at their real size and position, and the first whose
|
|
920
|
+
labels do not overlap wins: a decade shows every year, thirty years
|
|
921
|
+
fall back to one every two or five. When none fits, the coarsest is
|
|
922
|
+
used. Returns the layout drawn.
|
|
923
|
+
|
|
924
|
+
With the start pinned, the first calendar tick gives way to the
|
|
925
|
+
(wider) start label when they collide and it lies under half a step
|
|
926
|
+
from the start: a series from October 2015 shows ``oct. 2015``,
|
|
927
|
+
2017, 2018... Further away, dropping it would read as a skipped
|
|
928
|
+
year, so that step counts as not fitting.
|
|
929
|
+
|
|
930
|
+
Centred layouts (``date_labels: centered``) put the tick marks on the
|
|
931
|
+
period boundaries and the labels, as minor ticks without a mark, in
|
|
932
|
+
the middle of each period.
|
|
933
|
+
|
|
934
|
+
``rotation`` tilts the labels. ``auto_rotate`` (ignored when a
|
|
935
|
+
rotation is given) tries 0, 30, 45, 60 and 90 degrees and keeps the
|
|
936
|
+
flattest tilt that reaches the finest step any of them reaches.
|
|
937
|
+
|
|
938
|
+
Neighbouring labels must stay *gap_em* ems apart
|
|
939
|
+
(``date_label_gap_em`` in ``plots.yaml``).
|
|
940
|
+
"""
|
|
941
|
+
fig = ax.get_figure()
|
|
942
|
+
assert fig is not None
|
|
943
|
+
# settle the layout so dates map to their final pixels
|
|
944
|
+
fig.canvas.draw() # type: ignore[reportUnknownMemberType]
|
|
945
|
+
renderer = fig.canvas.get_renderer() # type: ignore[reportUnknownMemberType]
|
|
946
|
+
font = ax.xaxis.get_major_ticks()[0].label1.get_fontproperties()
|
|
947
|
+
# a fixed share of an em between neighbouring labels, whatever the dpi
|
|
948
|
+
pad_px = (
|
|
949
|
+
float(
|
|
950
|
+
renderer.points_to_pixels(font.get_size_in_points()) # type: ignore[reportUnknownMemberType]
|
|
951
|
+
)
|
|
952
|
+
* gap_em
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
def _boxes(layout: DateLayout, rot: float) -> list[Any]:
|
|
956
|
+
texts = _layout_texts(layout, pin_start=pin_start)
|
|
957
|
+
ha = _date_label_ha(None if rot == 0 else rot)
|
|
958
|
+
boxes: list[Any] = []
|
|
959
|
+
for when, text in zip(layout.labels, texts):
|
|
960
|
+
x_px = float(
|
|
961
|
+
ax.transData.transform((float(cast(Any, date2num(when))), 0))[0]
|
|
962
|
+
)
|
|
963
|
+
probe = Text(
|
|
964
|
+
x_px,
|
|
965
|
+
0.0,
|
|
966
|
+
text,
|
|
967
|
+
fontproperties=font,
|
|
968
|
+
rotation=rot,
|
|
969
|
+
horizontalalignment=ha,
|
|
970
|
+
rotation_mode="anchor" if rot != 0 else "default",
|
|
971
|
+
)
|
|
972
|
+
probe.set_figure(fig)
|
|
973
|
+
boxes.append(probe.get_window_extent(renderer)) # type: ignore[reportUnknownArgumentType]
|
|
974
|
+
return boxes
|
|
975
|
+
|
|
976
|
+
def _fit(layout: DateLayout, rot: float) -> list[int] | None:
|
|
977
|
+
boxes = _boxes(layout, rot)
|
|
978
|
+
kept = list(range(len(boxes)))
|
|
979
|
+
centered = layout.centered
|
|
980
|
+
if pin_start and not centered and len(kept) > 2:
|
|
981
|
+
# the first calendar tick may give way to the (wider) start
|
|
982
|
+
# label only when it sits under half a step from the start:
|
|
983
|
+
# further away, dropping it would open a gap that reads as a
|
|
984
|
+
# skipped year, so the step does not fit instead
|
|
985
|
+
when = [float(cast(Any, date2num(t))) for t in layout.ticks]
|
|
986
|
+
near = when[1] - when[0] < (when[2] - when[1]) / 2
|
|
987
|
+
if near and boxes[1].x0 <= boxes[0].x1 + pad_px:
|
|
988
|
+
kept.pop(1)
|
|
989
|
+
clear = all(
|
|
990
|
+
boxes[b].x0 > boxes[a].x1 + pad_px for a, b in zip(kept, kept[1:])
|
|
991
|
+
)
|
|
992
|
+
return kept if clear else None
|
|
993
|
+
|
|
994
|
+
tilts = (
|
|
995
|
+
(0.0, 30.0, 45.0, 60.0, 90.0)
|
|
996
|
+
if auto_rotate and rotation is None
|
|
997
|
+
else (0.0 if rotation is None else float(rotation),)
|
|
998
|
+
)
|
|
999
|
+
best: tuple[int, float, list[int]] | None = None
|
|
1000
|
+
for rot in tilts:
|
|
1001
|
+
for i, layout in enumerate(layouts):
|
|
1002
|
+
if best is not None and i >= best[0]:
|
|
1003
|
+
break
|
|
1004
|
+
kept = _fit(layout, rot)
|
|
1005
|
+
if kept is not None:
|
|
1006
|
+
best = (i, rot, kept)
|
|
1007
|
+
break
|
|
1008
|
+
if best is not None and best[0] == 0:
|
|
1009
|
+
break
|
|
1010
|
+
if best is None:
|
|
1011
|
+
coarsest = layouts[-1]
|
|
1012
|
+
best = (len(layouts) - 1, tilts[0], list(range(len(coarsest.labels))))
|
|
1013
|
+
index, rot, kept = best
|
|
1014
|
+
layout = layouts[index]
|
|
1015
|
+
centered = layout.centered
|
|
1016
|
+
|
|
1017
|
+
if centered:
|
|
1018
|
+
major = FixedLocator(
|
|
1019
|
+
[float(cast(Any, date2num(t))) for t in layout.ticks]
|
|
1020
|
+
)
|
|
1021
|
+
ax.xaxis.set_major_locator(major)
|
|
1022
|
+
ax.xaxis.set_major_formatter(NullFormatter())
|
|
1023
|
+
texts = _layout_texts(layout, pin_start=pin_start)
|
|
1024
|
+
ax.xaxis.set_minor_locator(
|
|
1025
|
+
FixedLocator(
|
|
1026
|
+
[float(cast(Any, date2num(layout.labels[k]))) for k in kept]
|
|
1027
|
+
)
|
|
1028
|
+
)
|
|
1029
|
+
ax.xaxis.set_minor_formatter(FixedFormatter([texts[k] for k in kept]))
|
|
1030
|
+
ax.tick_params( # type: ignore[reportUnknownMemberType]
|
|
1031
|
+
axis="x",
|
|
1032
|
+
which="minor",
|
|
1033
|
+
length=0,
|
|
1034
|
+
labelsize=ax.xaxis.get_major_ticks()[0].label1.get_fontsize(),
|
|
1035
|
+
)
|
|
1036
|
+
else:
|
|
1037
|
+
values = [float(cast(Any, date2num(layout.ticks[k]))) for k in kept]
|
|
1038
|
+
fixed = FixedLocator(values)
|
|
1039
|
+
ax.xaxis.set_major_locator(fixed)
|
|
1040
|
+
ax.xaxis.set_major_formatter(
|
|
1041
|
+
_tick_formatter(fixed, values[0], pin_start)
|
|
1042
|
+
)
|
|
1043
|
+
fig.canvas.draw() # type: ignore[reportUnknownMemberType]
|
|
1044
|
+
_style_date_labels(ax, rot, minor=centered)
|
|
1045
|
+
return layout
|
|
1046
|
+
|
|
1047
|
+
|
|
807
1048
|
def compact_date_axis(
|
|
808
1049
|
ax: Axes,
|
|
809
1050
|
data_min: Any,
|
|
@@ -813,8 +1054,22 @@ def compact_date_axis(
|
|
|
813
1054
|
rotation: float | None = None,
|
|
814
1055
|
pin_start: bool | None = None,
|
|
815
1056
|
auto_rotate: bool = False,
|
|
1057
|
+
max_labels: int | None = None,
|
|
1058
|
+
step: str = "auto",
|
|
1059
|
+
labels: str = "tick",
|
|
816
1060
|
) -> None:
|
|
817
|
-
"""Compact date x-axis:
|
|
1061
|
+
"""Compact date x-axis: as many calendar dates as fit the narrow figure.
|
|
1062
|
+
|
|
1063
|
+
The axis labels every year (or, over two years at most, every month)
|
|
1064
|
+
that fits in the width without overlapping and within *max_labels*
|
|
1065
|
+
(``None`` reads ``compact.date_max_labels`` for the default density),
|
|
1066
|
+
stepping to one every 2, 5, 10... years only when they do not fit;
|
|
1067
|
+
``step`` (``"1Y"``, ``"3M"``...) fixes the step instead, and
|
|
1068
|
+
``labels="centered"`` centres each year over its stretch. See
|
|
1069
|
+
:func:`calendar_date_axis`.
|
|
1070
|
+
|
|
1071
|
+
Spans too short for a calendar grid (a few weeks of daily data) keep
|
|
1072
|
+
the historical behaviour described below, with *maxticks* ticks.
|
|
818
1073
|
|
|
819
1074
|
Ticks are computed over the **data span** ``[data_min,
|
|
820
1075
|
data_max]`` and pinned with a ``FixedLocator``, so none
|
|
@@ -853,6 +1108,31 @@ def compact_date_axis(
|
|
|
853
1108
|
if pin_start is None
|
|
854
1109
|
else pin_start
|
|
855
1110
|
)
|
|
1111
|
+
monthly_max, min_fraction, gap_em = _date_axis_config(compact=True)
|
|
1112
|
+
layouts = date_layouts(
|
|
1113
|
+
data_min,
|
|
1114
|
+
data_max,
|
|
1115
|
+
step=step,
|
|
1116
|
+
max_labels=(
|
|
1117
|
+
date_max_labels("default", compact=True)
|
|
1118
|
+
if max_labels is None
|
|
1119
|
+
else max_labels
|
|
1120
|
+
),
|
|
1121
|
+
monthly_max_months=monthly_max,
|
|
1122
|
+
pin_start=show_start,
|
|
1123
|
+
centered=labels == "centered",
|
|
1124
|
+
min_fraction=min_fraction,
|
|
1125
|
+
)
|
|
1126
|
+
if layouts:
|
|
1127
|
+
calendar_date_axis(
|
|
1128
|
+
ax,
|
|
1129
|
+
layouts,
|
|
1130
|
+
pin_start=show_start,
|
|
1131
|
+
rotation=rotation,
|
|
1132
|
+
auto_rotate=auto_rotate,
|
|
1133
|
+
gap_em=gap_em,
|
|
1134
|
+
)
|
|
1135
|
+
return
|
|
856
1136
|
# same span-clipped ticks (and same pinned start) the non-compact axis
|
|
857
1137
|
# uses, so both modes answer "when does this start?" identically
|
|
858
1138
|
ticks: list[float] = date_span_ticks(
|
|
@@ -958,8 +1238,20 @@ def apply_date_axis(
|
|
|
958
1238
|
data_min: Any = None,
|
|
959
1239
|
data_max: Any = None,
|
|
960
1240
|
pin_start: bool = False,
|
|
1241
|
+
max_labels: int | None = None,
|
|
1242
|
+
step: str = "auto",
|
|
1243
|
+
labels: str = "tick",
|
|
961
1244
|
) -> None:
|
|
962
|
-
"""Native matplotlib date x-axis with
|
|
1245
|
+
"""Native matplotlib date x-axis with every year (or month) that fits.
|
|
1246
|
+
|
|
1247
|
+
Given the ``data_min`` / ``data_max`` span, the axis labels every year
|
|
1248
|
+
(or, over two years at most, every month) that fits in the width and
|
|
1249
|
+
within *max_labels* (``None`` reads ``line.date_max_labels`` for the
|
|
1250
|
+
default density), stepping to one every 2, 5, 10... years only when
|
|
1251
|
+
they do not; ``step`` fixes the step and ``labels="centered"`` centres
|
|
1252
|
+
each year over its stretch (see :func:`calendar_date_axis`). The rest
|
|
1253
|
+
of this docstring describes the fallback for spans too short for a
|
|
1254
|
+
calendar grid, or when no span is given.
|
|
963
1255
|
|
|
964
1256
|
The non-compact counterpart to :func:`compact_date_axis`: it takes over the
|
|
965
1257
|
x-axis (which must carry a **native** date axis, i.e. the series were
|
|
@@ -984,6 +1276,31 @@ def apply_date_axis(
|
|
|
984
1276
|
``rotation=None`` has to *actively* flatten them to keep the documented
|
|
985
1277
|
"horizontal unless asked" behaviour.
|
|
986
1278
|
"""
|
|
1279
|
+
if data_min is not None and data_max is not None:
|
|
1280
|
+
monthly_max, min_fraction, gap_em = _date_axis_config(compact=False)
|
|
1281
|
+
layouts = date_layouts(
|
|
1282
|
+
data_min,
|
|
1283
|
+
data_max,
|
|
1284
|
+
step=step,
|
|
1285
|
+
max_labels=(
|
|
1286
|
+
date_max_labels("default", compact=False)
|
|
1287
|
+
if max_labels is None
|
|
1288
|
+
else max_labels
|
|
1289
|
+
),
|
|
1290
|
+
monthly_max_months=monthly_max,
|
|
1291
|
+
pin_start=pin_start,
|
|
1292
|
+
centered=labels == "centered",
|
|
1293
|
+
min_fraction=min_fraction,
|
|
1294
|
+
)
|
|
1295
|
+
if layouts:
|
|
1296
|
+
calendar_date_axis(
|
|
1297
|
+
ax,
|
|
1298
|
+
layouts,
|
|
1299
|
+
pin_start=pin_start,
|
|
1300
|
+
rotation=rotation,
|
|
1301
|
+
gap_em=gap_em,
|
|
1302
|
+
)
|
|
1303
|
+
return
|
|
987
1304
|
if pin_start and data_min is not None and data_max is not None:
|
|
988
1305
|
ticks = date_span_ticks(
|
|
989
1306
|
data_min, data_max, maxticks=maxticks, pin_start=True
|
|
@@ -1226,8 +1543,9 @@ def style_compact_axes(
|
|
|
1226
1543
|
:func:`declutter_spines`.
|
|
1227
1544
|
"""
|
|
1228
1545
|
if font_pt is not None:
|
|
1546
|
+
# which="both": centred date labels live on the minor ticks
|
|
1229
1547
|
ax.tick_params( # type: ignore[reportUnknownMemberType]
|
|
1230
|
-
axis="both", labelsize=font_pt
|
|
1548
|
+
axis="both", which="both", labelsize=font_pt
|
|
1231
1549
|
)
|
|
1232
1550
|
if side == "right":
|
|
1233
1551
|
ax.yaxis.tick_right()
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Calendar grids for date x-axes: every year (or month) the axis can hold.
|
|
2
|
+
|
|
3
|
+
Pure date maths shared by the matplotlib date axes in ``_common`` and by
|
|
4
|
+
the Plotly backend: which calendar steps are worth trying over a span,
|
|
5
|
+
where their ticks fall and, for centred labels, where each period label
|
|
6
|
+
sits. Nothing here draws or measures text; picking the finest step whose
|
|
7
|
+
labels actually fit is the caller's job.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
#: Calendar steps, finest first. ``M`` counts months, ``Y`` years; the
|
|
18
|
+
#: month steps divide a year, so January is always on the grid.
|
|
19
|
+
DATE_STEPS: tuple[str, ...] = (
|
|
20
|
+
"1M",
|
|
21
|
+
"2M",
|
|
22
|
+
"3M",
|
|
23
|
+
"6M",
|
|
24
|
+
"1Y",
|
|
25
|
+
"2Y",
|
|
26
|
+
"5Y",
|
|
27
|
+
"10Y",
|
|
28
|
+
"20Y",
|
|
29
|
+
"50Y",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
#: How a date label relates to its tick: under it, or centred over the
|
|
33
|
+
#: period (year or month) that starts at the tick.
|
|
34
|
+
DATE_LABEL_MODES: tuple[str, ...] = ("tick", "centered")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class DateLayout:
|
|
39
|
+
"""Where one calendar step puts the tick marks and the labels.
|
|
40
|
+
|
|
41
|
+
``ticks`` are the tick marks. ``labels`` are the label positions;
|
|
42
|
+
with ``tick`` labels they are the ticks themselves, with ``centered``
|
|
43
|
+
labels they sit mid-period. ``periods`` holds, per label, the date
|
|
44
|
+
that names it (the tick itself, or the start of its period).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
step: str
|
|
48
|
+
ticks: list[pd.Timestamp]
|
|
49
|
+
labels: list[pd.Timestamp]
|
|
50
|
+
periods: list[pd.Timestamp]
|
|
51
|
+
centered: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_date_step(step: str) -> str:
|
|
55
|
+
"""Return *step* when it is ``auto`` or one of :data:`DATE_STEPS`."""
|
|
56
|
+
if step != "auto" and step not in DATE_STEPS:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"date_step must be 'auto' or one of {DATE_STEPS}: {step!r}"
|
|
59
|
+
)
|
|
60
|
+
return step
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_date_labels(mode: str) -> str:
|
|
64
|
+
"""Return *mode* when it is one of :data:`DATE_LABEL_MODES`."""
|
|
65
|
+
if mode not in DATE_LABEL_MODES:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"date_labels must be one of {DATE_LABEL_MODES}: {mode!r}"
|
|
68
|
+
)
|
|
69
|
+
return mode
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse(step: str) -> tuple[str, int]:
|
|
73
|
+
return step[-1], int(step[:-1])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _month_index(ts: pd.Timestamp) -> int:
|
|
77
|
+
return ts.year * 12 + ts.month - 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _month_start(index: int) -> pd.Timestamp:
|
|
81
|
+
return pd.Timestamp(year=index // 12, month=index % 12 + 1, day=1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _period_starts(
|
|
85
|
+
lo: pd.Timestamp, hi: pd.Timestamp, step: str, *, anchor_year: int
|
|
86
|
+
) -> list[pd.Timestamp]:
|
|
87
|
+
"""Calendar boundaries of *step* from the one at or before *lo* to *hi*.
|
|
88
|
+
|
|
89
|
+
Year steps run from ``anchor_year`` (so a two-year step over a series
|
|
90
|
+
starting in 2015 gives 2015, 2017, 2019 and not the calendar 2016,
|
|
91
|
+
2018); month steps stay on the calendar grid (quarters start in
|
|
92
|
+
January, April, July and October), which keeps January, and so the
|
|
93
|
+
year, on the axis.
|
|
94
|
+
"""
|
|
95
|
+
unit, n = _parse(step)
|
|
96
|
+
if unit == "Y":
|
|
97
|
+
return [
|
|
98
|
+
pd.Timestamp(year=y, month=1, day=1)
|
|
99
|
+
for y in range(anchor_year, hi.year + 1, n)
|
|
100
|
+
]
|
|
101
|
+
first = _month_index(lo) - _month_index(lo) % n
|
|
102
|
+
return [_month_start(i) for i in range(first, _month_index(hi) + 1, n)]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _tick_layout(
|
|
106
|
+
lo: pd.Timestamp, hi: pd.Timestamp, step: str, *, pin_start: bool
|
|
107
|
+
) -> DateLayout:
|
|
108
|
+
# pinned, the start label stands for its own year, so the grid is
|
|
109
|
+
# anchored there; unpinned, at the first year boundary inside the span
|
|
110
|
+
on_new_year = lo == pd.Timestamp(year=lo.year, month=1, day=1)
|
|
111
|
+
anchor = lo.year if pin_start or on_new_year else lo.year + 1
|
|
112
|
+
grid = [
|
|
113
|
+
t
|
|
114
|
+
for t in _period_starts(lo, hi, step, anchor_year=anchor)
|
|
115
|
+
if lo < t <= hi or (not pin_start and t == lo)
|
|
116
|
+
]
|
|
117
|
+
ticks = [lo, *grid] if pin_start else grid
|
|
118
|
+
return DateLayout(step, ticks, ticks, ticks)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _centered_layout(
|
|
122
|
+
lo: pd.Timestamp, hi: pd.Timestamp, step: str, *, min_fraction: float
|
|
123
|
+
) -> DateLayout:
|
|
124
|
+
unit, _n = _parse(step)
|
|
125
|
+
length = pd.DateOffset(years=1) if unit == "Y" else pd.DateOffset(months=1)
|
|
126
|
+
starts = _period_starts(lo, hi, step, anchor_year=lo.year)
|
|
127
|
+
labels: list[pd.Timestamp] = []
|
|
128
|
+
periods: list[pd.Timestamp] = []
|
|
129
|
+
for start in starts:
|
|
130
|
+
end = start + length
|
|
131
|
+
shown_lo = max(start, lo)
|
|
132
|
+
shown_hi = min(end, hi)
|
|
133
|
+
# a sliver of a year at either edge would push its label off the
|
|
134
|
+
# plot: label a period only when enough of it is drawn
|
|
135
|
+
if shown_hi - shown_lo >= (end - start) * min_fraction:
|
|
136
|
+
labels.append(shown_lo + (shown_hi - shown_lo) / 2)
|
|
137
|
+
periods.append(start)
|
|
138
|
+
ticks = [t for t in starts if lo <= t <= hi]
|
|
139
|
+
return DateLayout(step, ticks, labels, periods, centered=True)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def date_layout(
|
|
143
|
+
data_min: Any,
|
|
144
|
+
data_max: Any,
|
|
145
|
+
step: str,
|
|
146
|
+
*,
|
|
147
|
+
pin_start: bool,
|
|
148
|
+
centered: bool = False,
|
|
149
|
+
min_fraction: float = 0.5,
|
|
150
|
+
) -> DateLayout:
|
|
151
|
+
"""Ticks and labels that *step* puts over ``[data_min, data_max]``.
|
|
152
|
+
|
|
153
|
+
With ``tick`` labels (the default) every tick is labelled and, with
|
|
154
|
+
``pin_start``, the first datum leads as its own tick. With
|
|
155
|
+
``centered`` labels the ticks mark where each period starts and its
|
|
156
|
+
name sits in the middle of the part drawn; ``pin_start`` is ignored
|
|
157
|
+
there, since the first label already names the starting period.
|
|
158
|
+
"""
|
|
159
|
+
lo = pd.Timestamp(data_min)
|
|
160
|
+
hi = pd.Timestamp(data_max)
|
|
161
|
+
if centered:
|
|
162
|
+
return _centered_layout(lo, hi, step, min_fraction=min_fraction)
|
|
163
|
+
return _tick_layout(lo, hi, step, pin_start=pin_start)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def date_layouts(
|
|
167
|
+
data_min: Any,
|
|
168
|
+
data_max: Any,
|
|
169
|
+
*,
|
|
170
|
+
step: str,
|
|
171
|
+
max_labels: int,
|
|
172
|
+
monthly_max_months: int,
|
|
173
|
+
pin_start: bool,
|
|
174
|
+
centered: bool = False,
|
|
175
|
+
min_fraction: float = 0.5,
|
|
176
|
+
) -> list[DateLayout]:
|
|
177
|
+
"""Candidate layouts for the span, finest first.
|
|
178
|
+
|
|
179
|
+
An explicit *step* is the only candidate. With ``auto``, month steps
|
|
180
|
+
are tried only over spans up to *monthly_max_months* (beyond that the
|
|
181
|
+
axis reads in years), and a step is kept when it gives between two
|
|
182
|
+
and *max_labels* labels. An empty list means no calendar step suits
|
|
183
|
+
the span (a few weeks of daily data, say) and the caller falls back to
|
|
184
|
+
matplotlib's own date locator.
|
|
185
|
+
"""
|
|
186
|
+
lo = pd.Timestamp(data_min)
|
|
187
|
+
hi = pd.Timestamp(data_max)
|
|
188
|
+
if step != "auto":
|
|
189
|
+
pool: tuple[str, ...] = (step,)
|
|
190
|
+
limit = None
|
|
191
|
+
else:
|
|
192
|
+
months = _month_index(hi) - _month_index(lo)
|
|
193
|
+
pool = tuple(
|
|
194
|
+
s
|
|
195
|
+
for s in DATE_STEPS
|
|
196
|
+
if months <= monthly_max_months or s.endswith("Y")
|
|
197
|
+
)
|
|
198
|
+
limit = max_labels
|
|
199
|
+
layouts: list[DateLayout] = []
|
|
200
|
+
for candidate in pool:
|
|
201
|
+
layout = date_layout(
|
|
202
|
+
lo,
|
|
203
|
+
hi,
|
|
204
|
+
candidate,
|
|
205
|
+
pin_start=pin_start,
|
|
206
|
+
centered=centered,
|
|
207
|
+
min_fraction=min_fraction,
|
|
208
|
+
)
|
|
209
|
+
count = len(layout.labels)
|
|
210
|
+
if count >= 2 and (limit is None or count <= limit):
|
|
211
|
+
layouts.append(layout)
|
|
212
|
+
return layouts
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def period_label(
|
|
216
|
+
period: pd.Timestamp, step: str, *, first: bool = False
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Text of a centred label: the year, or the month (the year in January).
|
|
219
|
+
|
|
220
|
+
The *first* month label carries its year too (``may. 2024``) unless
|
|
221
|
+
it is January, so the reader knows which year the axis starts in.
|
|
222
|
+
"""
|
|
223
|
+
unit, _n = _parse(step)
|
|
224
|
+
if unit == "Y" or period.month == 1:
|
|
225
|
+
return str(period.year)
|
|
226
|
+
if first:
|
|
227
|
+
# locale's %b may already end in a dot ("may."): avoid "may.. 2024"
|
|
228
|
+
month = period.strftime("%b").lower().rstrip(".")
|
|
229
|
+
return f"{month}. {period.year}"
|
|
230
|
+
return period.strftime("%b")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def plotly_dtick(data_min: Any, step: str) -> tuple[str, str]:
|
|
234
|
+
"""Plotly ``(dtick, tick0)`` for *step*, anchored as the PNG axis is."""
|
|
235
|
+
unit, n = _parse(step)
|
|
236
|
+
if unit == "Y":
|
|
237
|
+
return f"M{12 * n}", f"{pd.Timestamp(data_min).year}-01-01"
|
|
238
|
+
return f"M{n}", "2000-01-01"
|