tesorotools-python 0.1.9__tar.gz → 0.1.10__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.
Files changed (170) hide show
  1. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/.gitignore +2 -1
  2. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/PKG-INFO +1 -1
  3. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/palette.py +2 -0
  4. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/charts.py +15 -7
  5. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/native.py +19 -2
  6. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/skill/SKILL.md +3 -2
  7. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/__init__.py +10 -0
  8. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/app.py +171 -1
  9. tesorotools_python-0.1.10/src/tesorotools/web/period.py +220 -0
  10. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/pyproject.toml +0 -0
  11. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/__init__.py +0 -0
  12. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/_build_context.py +0 -0
  13. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/_registry.py +0 -0
  14. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/__init__.py +0 -0
  15. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/_common.py +0 -0
  16. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/_dates.py +0 -0
  17. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/bar_line.py +0 -0
  18. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/barh_plot.py +0 -0
  19. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/box_plot.py +0 -0
  20. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/compact.py +0 -0
  21. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/intraday_plot.py +0 -0
  22. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/line_plot.py +0 -0
  23. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/matrix.py +0 -0
  24. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/plotly_backend.py +0 -0
  25. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/sankey.py +0 -0
  26. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/scatter.py +0 -0
  27. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/sharing.py +0 -0
  28. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/shock_plot.py +0 -0
  29. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/stacked.py +0 -0
  30. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/tikz_backend.py +0 -0
  31. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/type_curve.py +0 -0
  32. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/vector_plot.py +0 -0
  33. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/artists/waterfall.py +0 -0
  34. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/README.md +0 -0
  35. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  36. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  37. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  38. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  39. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  40. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  41. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  42. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  43. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/fonts/README.md +0 -0
  44. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/nota_template.dotx +0 -0
  45. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/plots.yaml +0 -0
  46. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/template.docx +0 -0
  47. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  48. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/data_sources/__init__.py +0 -0
  49. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/data_sources/debug.py +0 -0
  50. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/database/__init__.py +0 -0
  51. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/database/local.py +0 -0
  52. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/database/push.py +0 -0
  53. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/database/shared.py +0 -0
  54. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/dependencies/__init__.py +0 -0
  55. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/dependencies/node.py +0 -0
  56. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/dependencies/resolution.py +0 -0
  57. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/driver.py +0 -0
  58. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/__init__.py +0 -0
  59. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/cli.py +0 -0
  60. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/scaffold.py +0 -0
  61. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/article/main.tex.tmpl +0 -0
  62. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/article/sections/01-introduccion.tex +0 -0
  63. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/article/sections/02-analisis.tex +0 -0
  64. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/article/sections/03-conclusiones.tex +0 -0
  65. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/beamer/main.tex.tmpl +0 -0
  66. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/beamer/slides/01-contexto.tex +0 -0
  67. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/beamer/slides/02-analisis.tex +0 -0
  68. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/beamer/slides/03-conclusiones.tex +0 -0
  69. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/beamer/style/beamerthemeTesoro.sty +0 -0
  70. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/book/appendices/a-fuentes.tex +0 -0
  71. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/book/chapters/01-introduccion.tex +0 -0
  72. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/book/chapters/02-analisis.tex +0 -0
  73. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/book/frontmatter/resumen.tex +0 -0
  74. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/book/main.tex.tmpl +0 -0
  75. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/README.md.tmpl +0 -0
  76. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/dot-gitignore +0 -0
  77. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/dot-vscode/settings.json +0 -0
  78. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/figures/dot-gitkeep +0 -0
  79. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/fonts/README.md +0 -0
  80. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/latexmkrc +0 -0
  81. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/refs.bib +0 -0
  82. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/style/tesoro.sty +0 -0
  83. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/style/tesorobrand.sty.tmpl +0 -0
  84. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/latex/templates/common/style/tesorolocal.sty.tmpl +0 -0
  85. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/manifest.py +0 -0
  86. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/__init__.py +0 -0
  87. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/builder.py +0 -0
  88. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/charts.py +0 -0
  89. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/cli.py +0 -0
  90. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/inline.py +0 -0
  91. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/issues.py +0 -0
  92. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/lint.py +0 -0
  93. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/markdown.py +0 -0
  94. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/pdf.py +0 -0
  95. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/skeleton.md +0 -0
  96. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/skill/SKILL.md +0 -0
  97. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/skill/reglas.md +0 -0
  98. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/note/template.py +0 -0
  99. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/offsets/__init__.py +0 -0
  100. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/offsets/offsets.py +0 -0
  101. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/offsets/outliers.py +0 -0
  102. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/orchestration.py +0 -0
  103. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/pipeline/__init__.py +0 -0
  104. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/pipeline/diagnose.py +0 -0
  105. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/pipeline/engine.py +0 -0
  106. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/pipeline/rules.py +0 -0
  107. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/__init__.py +0 -0
  108. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/builder.py +0 -0
  109. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/cli.py +0 -0
  110. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/fit.py +0 -0
  111. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/glossary.py +0 -0
  112. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/house.py +0 -0
  113. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/lint.py +0 -0
  114. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/markdown.py +0 -0
  115. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/pdf.py +0 -0
  116. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/sections.py +0 -0
  117. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/skeleton.md +0 -0
  118. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/skill/reglas.md +0 -0
  119. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/presentation/template.py +0 -0
  120. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/providers/__init__.py +0 -0
  121. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/providers/base.py +0 -0
  122. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/providers/bde.py +0 -0
  123. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/providers/ecb.py +0 -0
  124. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/providers/fred.py +0 -0
  125. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/providers/imf_irfcl.py +0 -0
  126. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/providers/lseg.py +0 -0
  127. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/py.typed +0 -0
  128. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/__init__.py +0 -0
  129. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/__init__.py +0 -0
  130. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/content.py +0 -0
  131. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/images.py +0 -0
  132. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/section.py +0 -0
  133. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/subtitle.py +0 -0
  134. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/table.py +0 -0
  135. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/text.py +0 -0
  136. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/content/title.py +0 -0
  137. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/document.py +0 -0
  138. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/render/report.py +0 -0
  139. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/scaffold.py +0 -0
  140. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/testing/__init__.py +0 -0
  141. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/testing/compare.py +0 -0
  142. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/__init__.py +0 -0
  143. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/config.py +0 -0
  144. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/format.py +0 -0
  145. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/globals.py +0 -0
  146. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/matplotlib.py +0 -0
  147. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/series.py +0 -0
  148. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/shortcuts.py +0 -0
  149. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/utils/template.py +0 -0
  150. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/brand.py +0 -0
  151. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/cli.py +0 -0
  152. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/components.py +0 -0
  153. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/formatting.py +0 -0
  154. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/launcher.py +0 -0
  155. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/palette.py +0 -0
  156. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/plotting.py +0 -0
  157. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/scaffold.py +0 -0
  158. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/Dockerfile.tmpl +0 -0
  159. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/README.md.tmpl +0 -0
  160. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/dot-dockerignore.tmpl +0 -0
  161. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/dot-gitignore.tmpl +0 -0
  162. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/dot-streamlit/config.toml.tmpl +0 -0
  163. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/pyproject.toml.tmpl +0 -0
  164. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/src/$package/__init__.py.tmpl +0 -0
  165. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/src/$package/cli.py.tmpl +0 -0
  166. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/src/$package/main.py.tmpl +0 -0
  167. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/src/$package/sections.py.tmpl +0 -0
  168. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/tests/__init__.py.tmpl +0 -0
  169. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/templates/tests/test_app.py.tmpl +0 -0
  170. {tesorotools_python-0.1.9 → tesorotools_python-0.1.10}/src/tesorotools/web/theme.py +0 -0
@@ -32,8 +32,9 @@ test/
32
32
  # ruff
33
33
  .ruff_cache/
34
34
 
35
- # uv cache (CI restores it into the repo root)
35
+ # uv cache and verified-tree marks (CI restores them into the repo root)
36
36
  .uv-cache/
37
+ .ci-verified/
37
38
 
38
39
  demo_compacto/
39
40
  demo/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tesorotools-python
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  Requires-Python: >=3.13
5
5
  Requires-Dist: babel>=2.17
6
6
  Requires-Dist: matplotlib>=3.10
@@ -31,6 +31,8 @@ BLUE_PALE = "AEBAE7"
31
31
  BLUE_FAINT = "ECF0FD"
32
32
  #: Secondary text: captions, metadata, stale rows.
33
33
  GREY = "6D6D6D"
34
+ #: Plot area of the slide charts, under white grid lines.
35
+ GREY_PANEL = "EFEFEF"
34
36
  #: Tesoro yellow: highlights of the presentations (bands, frames, the
35
37
  #: section subtitles of the index) and its light shade for fills.
36
38
  YELLOW = "FFBD4C"
@@ -4,7 +4,8 @@ The same artists, palette and axis format as the notes
4
4
  (:func:`tesorotools.note.note_palette`, :func:`~tesorotools.note.axis_format`);
5
5
  only the size changes: each chart is drawn at the size of its frame in
6
6
  the template, with 12-point text that reads on a projected slide, the
7
- legend too, and on a white background, as the slide::
7
+ legend too, and the data on a grey panel with white grid lines, framed
8
+ by the white of the slide::
8
9
 
9
10
  with slide_style():
10
11
  LinePlot(
@@ -26,6 +27,7 @@ import matplotlib as mpl
26
27
 
27
28
  from tesorotools.artists._common import Legend
28
29
  from tesorotools.note.charts import note_palette
30
+ from tesorotools.palette import GREY_PANEL
29
31
 
30
32
  __all__ = [
31
33
  "SLIDE_CHART_CM",
@@ -49,13 +51,19 @@ SLIDE_CHART_CM: dict[str, tuple[float, float]] = {
49
51
  "pareja_estrecho": (10.4, 8.9),
50
52
  }
51
53
  _CM_TO_IN = 1 / 2.54
52
- #: What a slide chart changes of the house style: a white background,
53
- #: the one of the slide, whatever the style of the notes or the machine.
54
+ #: What a slide chart changes of the house style: the figure on white,
55
+ #: the one of the slide, whatever the style of the notes or the machine,
56
+ #: and the plot area grey (:data:`~tesorotools.palette.GREY_PANEL`) with
57
+ #: solid white grid lines.
54
58
  SLIDE_STYLE: dict[str, Any] = {
55
59
  "figure.facecolor": "white",
56
- "axes.facecolor": "white",
60
+ "axes.facecolor": f"#{GREY_PANEL}",
57
61
  "savefig.facecolor": "white",
58
62
  "savefig.transparent": False,
63
+ "grid.color": "white",
64
+ "grid.linestyle": "solid",
65
+ "grid.linewidth": 1.0,
66
+ "grid.alpha": 1.0,
59
67
  }
60
68
 
61
69
  _rc_context: Callable[[dict[str, Any]], Any] = mpl.rc_context # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
@@ -64,9 +72,9 @@ _rc_context: Callable[[dict[str, Any]], Any] = mpl.rc_context # pyright: ignore
64
72
  @contextmanager
65
73
  def slide_style() -> Generator[None]:
66
74
  """Draw the charts inside the block for a slide: the colours of the
67
- guide (:func:`~tesorotools.note.note_palette`) on a white background
68
- (:data:`SLIDE_STYLE`). Only while the block runs: the notes keep
69
- their own style."""
75
+ guide (:func:`~tesorotools.note.note_palette`) on the grey panel of
76
+ the slides (:data:`SLIDE_STYLE`). Only while the block runs: the
77
+ notes keep their own style."""
70
78
  with note_palette(), _rc_context(SLIDE_STYLE):
71
79
  yield
72
80
 
@@ -27,11 +27,13 @@ from pptx.enum.chart import (
27
27
  XL_MARKER_STYLE,
28
28
  )
29
29
  from pptx.enum.chart import XL_TICK_LABEL_POSITION as TICKS
30
- from pptx.oxml.ns import qn
30
+ from pptx.oxml import parse_xml
31
+ from pptx.oxml.ns import nsdecls, qn
31
32
  from pptx.util import Emu, Pt
32
33
 
33
34
  from tesorotools.note.builder import format_number
34
35
  from tesorotools.note.charts import NOTE_COLORS, axis_decimals
36
+ from tesorotools.palette import GREY_PANEL
35
37
 
36
38
  __all__ = [
37
39
  "NATIVE_KINDS",
@@ -173,9 +175,10 @@ def add_native_chart(
173
175
  else:
174
176
  series.format.fill.solid()
175
177
  series.format.fill.fore_color.rgb = color
178
+ _panel(chart)
176
179
  values: Any = chart.value_axis
177
180
  values.has_major_gridlines = True
178
- values.major_gridlines.format.line.color.rgb = _rgb(_GRID)
181
+ values.major_gridlines.format.line.color.rgb = _rgb("FFFFFF")
179
182
  values.format.line.fill.background()
180
183
  values.tick_labels.number_format = fmt
181
184
  values.tick_labels.number_format_is_linked = False
@@ -198,6 +201,20 @@ def add_native_chart(
198
201
  return chart
199
202
 
200
203
 
204
+ def _panel(chart: Any) -> None:
205
+ """The plot area on the grey of the slide charts (:data:`SLIDE_STYLE`),
206
+ under white grid lines. python-pptx has no call for it: its
207
+ ``c:spPr`` goes last, after the axes, where the schema wants it (the
208
+ chart python-pptx writes has no ``c:extLst`` to go before)."""
209
+ area: Any = chart._chartSpace.chart.plotArea # pyright: ignore[reportPrivateUsage]
210
+ area.append(
211
+ parse_xml(
212
+ f"<c:spPr {nsdecls('c', 'a')}><a:solidFill><a:srgbClr"
213
+ f' val="{GREY_PANEL}"/></a:solidFill></c:spPr>'
214
+ )
215
+ )
216
+
217
+
201
218
  def _date_ticks(axis: Any, months: int) -> None:
202
219
  """Marks every year, half year or quarter, as the span asks."""
203
220
  if months > 48:
@@ -403,7 +403,7 @@ from tesorotools.note import axis_format
403
403
  from tesorotools.presentation import slide_chart_size, slide_style
404
404
 
405
405
  series = {"ES10Y": "España", "DE10Y": "Alemania"} # columna: etiqueta
406
- with slide_style(): # colores de la guía, fondo blanco
406
+ with slide_style(): # colores de la guía, área de datos gris
407
407
  LinePlot(
408
408
  out_path=Path("graficos/tipos.png"),
409
409
  data=df, # DataFrame con DatetimeIndex
@@ -430,7 +430,8 @@ with slide_style(): # colores de la guía, fondo blanco
430
430
  `#2957FF`, Azul 3 `#5DA6FF`, Naranja `#E6821E` y Mostaza `#FBA737`) y el
431
431
  mismo formato del eje que en las notas: un gráfico de una nota y el de
432
432
  una presentación solo cambian de tamaño. `slide_style()` dibuja además
433
- sobre fondo blanco, el de la diapositiva.
433
+ el área de datos en gris claro (`#EFEFEF`) con la rejilla blanca, y el
434
+ resto del gráfico en blanco, el de la diapositiva.
434
435
  - `legend=True` pone la leyenda debajo del eje, a 12 puntos como el resto
435
436
  del gráfico, y solo si hay dos series o más (una sola serie no lleva
436
437
  leyenda: la nombra el título). Úsalo solo con los artists que admiten
@@ -38,15 +38,18 @@ from tesorotools.web.components import (
38
38
  )
39
39
  from tesorotools.web.formatting import format_number, long_date, short_date
40
40
  from tesorotools.web.launcher import launch
41
+ from tesorotools.web.period import Period, PeriodChoice, resolve_period
41
42
 
42
43
  if TYPE_CHECKING:
43
44
  from tesorotools.web.app import (
44
45
  Dashboard,
46
+ PeriodSelector,
45
47
  Section,
46
48
  block,
47
49
  chart,
48
50
  download_xlsx,
49
51
  html,
52
+ period_selector,
50
53
  xlsx_bytes,
51
54
  )
52
55
  from tesorotools.web.plotting import (
@@ -63,6 +66,9 @@ __all__ = [
63
66
  "FontFace",
64
67
  "Kpi",
65
68
  "NavLink",
69
+ "Period",
70
+ "PeriodChoice",
71
+ "PeriodSelector",
66
72
  "Section",
67
73
  "badge_html",
68
74
  "block",
@@ -80,6 +86,8 @@ __all__ = [
80
86
  "long_date",
81
87
  "nav_links_html",
82
88
  "notice_html",
89
+ "period_selector",
90
+ "resolve_period",
83
91
  "short_date",
84
92
  "sidebar_logo_html",
85
93
  "style_figure",
@@ -94,7 +102,9 @@ _APP = {
94
102
  "chart",
95
103
  "default_brand",
96
104
  "download_xlsx",
105
+ "PeriodSelector",
97
106
  "html",
107
+ "period_selector",
98
108
  "xlsx_bytes",
99
109
  }
100
110
  _PLOTTING = {"PLOTLY_CONFIG", "line_figure", "style_figure"}
@@ -30,10 +30,12 @@ Marks, fonts, signature and navigation come from a
30
30
 
31
31
  from __future__ import annotations
32
32
 
33
+ import datetime
33
34
  import io
34
35
  import logging
35
- from collections.abc import Callable, Sequence
36
+ from collections.abc import Callable, Mapping, Sequence
36
37
  from dataclasses import dataclass, field
38
+ from typing import Any
37
39
 
38
40
  import pandas as pd
39
41
  import plotly.graph_objects as go
@@ -52,6 +54,14 @@ from tesorotools.web.components import (
52
54
  section_index_markdown,
53
55
  sidebar_logo_html,
54
56
  )
57
+ from tesorotools.web.period import (
58
+ CUSTOM,
59
+ Period,
60
+ PeriodChoice,
61
+ Window,
62
+ as_day,
63
+ resolve_period,
64
+ )
55
65
  from tesorotools.web.plotting import PLOTLY_CONFIG
56
66
  from tesorotools.web.theme import DEFAULT_FAMILY, stylesheet
57
67
 
@@ -103,6 +113,166 @@ def chart(fig: go.Figure, *, key: str | None = None) -> None:
103
113
  )
104
114
 
105
115
 
116
+ #: Width of each date field, in pixels: fits "31/12/2026" and its icon.
117
+ _DATE_WIDTH = 140
118
+
119
+
120
+ def _start_key(key: str) -> str:
121
+ return f"{key}-desde"
122
+
123
+
124
+ def _end_key(key: str) -> str:
125
+ return f"{key}-hasta"
126
+
127
+
128
+ def _as_date(day: pd.Timestamp | None) -> datetime.date | None:
129
+ return None if day is None else day.date()
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class PeriodSelector:
134
+ """A period control, resolved but not yet drawn.
135
+
136
+ :attr:`period` is the window to draw with; :meth:`render` draws the
137
+ control. They are separate because the chart needs the window
138
+ **before** it is drawn and the control usually goes **under** it::
139
+
140
+ selector = period_selector("curva", WINDOWS, default="5 años",
141
+ dates=frame.index)
142
+ chart(figure(selector.period.clip(frame)))
143
+ selector.render()
144
+ """
145
+
146
+ key: str
147
+ choice: PeriodChoice
148
+ start_label: str = "Desde"
149
+ end_label: str = "Hasta"
150
+
151
+ @property
152
+ def period(self) -> Period:
153
+ return self.choice.period
154
+
155
+ def render(self) -> None:
156
+ """The row of buttons and, to its right, the two date fields.
157
+
158
+ The date fields get no ``value``: theirs is written in the session
159
+ before drawing them (the window of the selected button, or what
160
+ was typed, clamped to the data), because Streamlit warns when a
161
+ widget gets both, and a day outside ``min_value``/``max_value`` is
162
+ an exception and not a warning. On a narrow screen the row wraps
163
+ and the dates go under the buttons.
164
+ """
165
+ choice, state = self.choice, st.session_state
166
+ if choice.first is None or choice.last is None:
167
+ st.segmented_control(
168
+ "Periodo",
169
+ choice.options[:-1],
170
+ default=choice.default,
171
+ required=True,
172
+ key=self.key,
173
+ label_visibility="collapsed",
174
+ )
175
+ return
176
+ start, end = _start_key(self.key), _end_key(self.key)
177
+ if choice.period.warning is None:
178
+ state[start] = _as_date(choice.shown_start)
179
+ state[end] = _as_date(choice.shown_end)
180
+ else:
181
+ # What was typed stays, only clamped to the data: nobody gets
182
+ # their input erased for having it the wrong way round.
183
+ for date_key in (start, end):
184
+ day = as_day(state.get(date_key))
185
+ state[date_key] = _as_date(
186
+ None
187
+ if day is None
188
+ else min(max(day, choice.first), choice.last)
189
+ )
190
+ # The button also goes through the session and without
191
+ # ``default``: typing a date writes it there (see
192
+ # ``_to_custom``), and Streamlit warns about a default and a
193
+ # stored value at once.
194
+ if state.get(self.key) not in choice.options:
195
+ state[self.key] = choice.default
196
+ with st.container(
197
+ horizontal=True, vertical_alignment="bottom", gap="small"
198
+ ):
199
+ st.segmented_control(
200
+ "Periodo",
201
+ choice.options,
202
+ required=True,
203
+ key=self.key,
204
+ label_visibility="collapsed",
205
+ # Inside a row Streamlit keeps it on one line and scrolls
206
+ # it; on a phone the custom button fell off the screen.
207
+ width="content",
208
+ )
209
+ for label, date_key in (
210
+ (self.start_label, start),
211
+ (self.end_label, end),
212
+ ):
213
+ st.date_input(
214
+ label,
215
+ min_value=_as_date(choice.first),
216
+ max_value=_as_date(choice.last),
217
+ format="DD/MM/YYYY",
218
+ key=date_key,
219
+ on_change=_to_custom,
220
+ args=(self.key, choice.options[-1]),
221
+ width=_DATE_WIDTH,
222
+ )
223
+ if choice.period.warning:
224
+ # No ``icon=``: a Material icon written by name is one more
225
+ # place where the house font rule can fail.
226
+ st.warning(choice.period.warning)
227
+
228
+
229
+ def _to_custom(key: str, custom_label: str) -> None:
230
+ """``on_change`` of the date fields: typing selects the custom button.
231
+
232
+ It runs before the rerun, so the next pass already reads the new
233
+ window. The dates themselves are already in the session.
234
+ """
235
+ st.session_state[key] = custom_label
236
+
237
+
238
+ def period_selector(
239
+ key: str,
240
+ windows: Mapping[str, Window],
241
+ *,
242
+ default: str,
243
+ dates: Sequence[Any] | pd.Index | pd.Series,
244
+ first: pd.Timestamp | None = None,
245
+ custom_label: str = CUSTOM,
246
+ start_label: str = "Desde",
247
+ end_label: str = "Hasta",
248
+ ) -> PeriodSelector:
249
+ """A row of period buttons with two date fields, read from the session.
250
+
251
+ Nothing is drawn here: this reads what the three widgets left in
252
+ ``st.session_state`` (Streamlit writes it before rerunning the page,
253
+ so a click shows in that same pass) and resolves the window with
254
+ :func:`~tesorotools.web.period.resolve_period`. Draw the control
255
+ with :meth:`PeriodSelector.render`, wherever it goes.
256
+
257
+ Clip what is drawn **and** what is downloaded with the same
258
+ :attr:`PeriodSelector.period`, so the workbook is what is on screen.
259
+ To repaint only the chart on a click, put the chart and its control
260
+ inside an ``st.fragment``.
261
+ """
262
+ state = st.session_state
263
+ choice = resolve_period(
264
+ windows,
265
+ default=default,
266
+ dates=dates,
267
+ chosen=state.get(key),
268
+ typed_start=state.get(_start_key(key)),
269
+ typed_end=state.get(_end_key(key)),
270
+ first=first,
271
+ custom_label=custom_label,
272
+ )
273
+ return PeriodSelector(key, choice, start_label, end_label)
274
+
275
+
106
276
  def xlsx_bytes(frame: pd.DataFrame, sheet: str = "datos") -> bytes:
107
277
  """*frame* as an ``.xlsx`` workbook (real numbers, not text)."""
108
278
  buffer = io.BytesIO()
@@ -0,0 +1,220 @@
1
+ """The window of a time-series chart: preset buttons plus a typed range.
2
+
3
+ A row of preset buttons ("2 años", "5 años", "Todo"...) and, next to it,
4
+ two date fields ("Desde", "Hasta"). They are **one control**:
5
+
6
+ * a button sets the window and the two dates are rewritten with it, so
7
+ the dates always say what is being shown;
8
+ * typing a date selects the custom button and the window becomes the one
9
+ between the two dates, both days included;
10
+ * a date outside the data is clamped to it, and a start later than the
11
+ end is not drawn: the period falls back to the default button and
12
+ carries a warning saying so.
13
+
14
+ This module is the pure part: it decides the window from what the
15
+ widgets left in the session, and needs neither Streamlit nor Plotly. The
16
+ widgets live in :func:`tesorotools.web.app.period_selector`.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from collections.abc import Mapping, Sequence
22
+ from dataclasses import dataclass
23
+ from typing import Any
24
+
25
+ import numpy as np
26
+ import numpy.typing as npt
27
+ import pandas as pd
28
+
29
+ #: How far back a preset button looks: whole years before the last
30
+ #: datum, a fixed first day (for instance the last close of the previous
31
+ #: year, for a "year to date" button), or ``None`` for all the data.
32
+ Window = int | pd.Timestamp | None
33
+
34
+ #: Default label of the button that is selected when dates are typed.
35
+ CUSTOM = "Personalizado"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Period:
40
+ """The chosen window: from which day to which day, both included.
41
+
42
+ ``None`` on either side means "no cut on that side". With a preset
43
+ button ``end`` is always ``None``: the window ends at the last datum.
44
+ ``warning`` is what to say under the control when the typed dates
45
+ could not be used.
46
+ """
47
+
48
+ label: str
49
+ start: pd.Timestamp | None = None
50
+ end: pd.Timestamp | None = None
51
+ custom: bool = False
52
+ warning: str | None = None
53
+
54
+ def describe(self) -> str:
55
+ """How the window is named in a message: «"5 años"» or a range."""
56
+ if not self.custom:
57
+ return f"«{self.label}»"
58
+ start = "el principio" if self.start is None else _day(self.start)
59
+ end = "el último dato" if self.end is None else _day(self.end)
60
+ if self.start is None or self.end is None:
61
+ return f"el periodo desde {start} hasta {end}"
62
+ return f"el periodo del {start} al {end}"
63
+
64
+ def mask(
65
+ self, dates: pd.Index | pd.Series[Any] | Sequence[Any]
66
+ ) -> npt.NDArray[np.bool_]:
67
+ """Which of *dates* fall inside the window.
68
+
69
+ The end day is included whole: an intraday stamp of that day is
70
+ inside, not only its midnight.
71
+ """
72
+ stamps = pd.DatetimeIndex(list(dates))
73
+ inside = np.ones(len(stamps), dtype=bool)
74
+ if self.start is not None:
75
+ inside &= np.asarray(stamps >= self.start, dtype=bool)
76
+ if self.end is not None:
77
+ limit = self.end.normalize() + pd.Timedelta(days=1)
78
+ inside &= np.asarray(stamps < limit, dtype=bool)
79
+ return inside
80
+
81
+ def clip(self, frame: pd.DataFrame) -> pd.DataFrame:
82
+ """The rows of *frame* (indexed by date) inside the window."""
83
+ if frame.empty or (self.start is None and self.end is None):
84
+ return frame
85
+ return frame.loc[self.mask(frame.index)]
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class PeriodChoice:
90
+ """A resolved control: the period, and what the widgets must show.
91
+
92
+ ``first`` and ``last`` are the bounds of the date fields (``None``
93
+ when there is no data, and then only the buttons are drawn);
94
+ ``shown_start`` and ``shown_end`` are the dates the fields display.
95
+ """
96
+
97
+ period: Period
98
+ options: tuple[str, ...]
99
+ default: str
100
+ first: pd.Timestamp | None
101
+ last: pd.Timestamp | None
102
+ shown_start: pd.Timestamp | None
103
+ shown_end: pd.Timestamp | None
104
+
105
+
106
+ def _day(stamp: pd.Timestamp) -> str:
107
+ return f"{stamp:%d/%m/%Y}"
108
+
109
+
110
+ def as_day(value: object) -> pd.Timestamp | None:
111
+ """What a date widget left in the session, as a day; or ``None``."""
112
+ if value is None or value == "":
113
+ return None
114
+ try:
115
+ day = pd.Timestamp(value) # pyright: ignore[reportArgumentType]
116
+ except (TypeError, ValueError):
117
+ return None
118
+ return None if pd.isna(day) else day.normalize()
119
+
120
+
121
+ def _clamp(
122
+ day: pd.Timestamp, first: pd.Timestamp, last: pd.Timestamp
123
+ ) -> pd.Timestamp:
124
+ return min(max(day, first), last)
125
+
126
+
127
+ def resolve_period(
128
+ windows: Mapping[str, Window],
129
+ *,
130
+ default: str,
131
+ dates: Sequence[Any] | pd.Index | pd.Series,
132
+ chosen: object = None,
133
+ typed_start: object = None,
134
+ typed_end: object = None,
135
+ first: pd.Timestamp | None = None,
136
+ custom_label: str = CUSTOM,
137
+ ) -> PeriodChoice:
138
+ """Decide the window from what the widgets left in the session.
139
+
140
+ *windows* are the preset buttons, in order; *default* the one that
141
+ starts selected. *dates* are the dates of the data, and give the
142
+ bounds of the date fields: from the first one (or from *first*, if
143
+ given and later) to the last one. *chosen*, *typed_start* and
144
+ *typed_end* are the session values of the three widgets; anything
145
+ that is not one of the options, or not a date, counts as unset.
146
+ """
147
+ if default not in windows:
148
+ raise ValueError(f"default {default!r} is not one of the windows")
149
+ if custom_label in windows:
150
+ raise ValueError(f"{custom_label!r} is already a window label")
151
+ index = pd.DatetimeIndex(pd.to_datetime(pd.Series(list(dates)))).dropna()
152
+ options = (*windows, custom_label)
153
+ label = str(chosen) if chosen in options else default
154
+
155
+ if index.empty:
156
+ period = _preset(windows, default if label == custom_label else label)
157
+ return PeriodChoice(period, options, default, None, None, None, None)
158
+
159
+ newest = pd.Timestamp(index.max())
160
+ last = newest.normalize()
161
+ lower = pd.Timestamp(index.min()).normalize()
162
+ if first is not None:
163
+ lower = min(max(first.normalize(), lower), last)
164
+
165
+ if label != custom_label:
166
+ period = _preset(windows, label, newest)
167
+ else:
168
+ start = as_day(typed_start)
169
+ end = as_day(typed_end)
170
+ start = lower if start is None else _clamp(start, lower, last)
171
+ end = last if end is None else _clamp(end, lower, last)
172
+ if start > end:
173
+ fallback = _preset(windows, default, newest)
174
+ period = Period(
175
+ custom_label,
176
+ fallback.start,
177
+ fallback.end,
178
+ custom=True,
179
+ warning=(
180
+ f"«Desde» ({_day(start)}) es posterior a «Hasta» "
181
+ f"({_day(end)}): corrige una de las dos. Mientras, el "
182
+ f"gráfico enseña «{default}»."
183
+ ),
184
+ )
185
+ else:
186
+ period = Period(
187
+ custom_label,
188
+ None if start == lower else start,
189
+ None if end == last else end,
190
+ custom=True,
191
+ )
192
+
193
+ cut = lower if period.start is None else period.start.normalize()
194
+ return PeriodChoice(
195
+ period=period,
196
+ options=options,
197
+ default=default,
198
+ first=lower,
199
+ last=last,
200
+ shown_start=max(cut, lower),
201
+ shown_end=last if period.end is None else period.end,
202
+ )
203
+
204
+
205
+ def _preset(
206
+ windows: Mapping[str, Window],
207
+ label: str,
208
+ newest: pd.Timestamp | None = None,
209
+ ) -> Period:
210
+ """The period of a preset button, counted back from *newest*."""
211
+ window = windows[label]
212
+ if window is None:
213
+ return Period(label)
214
+ if isinstance(window, pd.Timestamp):
215
+ return Period(label, window)
216
+ if newest is None:
217
+ return Period(label)
218
+ # From the last datum **with its time**, so a button cuts exactly
219
+ # where a "last N years" filter on the raw data would.
220
+ return Period(label, newest - pd.DateOffset(years=window))