tesorotools-python 0.1.2__tar.gz → 0.1.4__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 (149) hide show
  1. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/PKG-INFO +1 -1
  2. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/pyproject.toml +1 -0
  3. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/__init__.py +6 -0
  4. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/__init__.py +4 -0
  5. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/_common.py +62 -2
  6. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/bar_line.py +147 -14
  7. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/box_plot.py +2 -1
  8. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/intraday_plot.py +2 -1
  9. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/line_plot.py +4 -1
  10. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/plotly_backend.py +171 -6
  11. tesorotools_python-0.1.4/src/tesorotools/artists/sankey.py +436 -0
  12. tesorotools_python-0.1.4/src/tesorotools/artists/scatter.py +469 -0
  13. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/shock_plot.py +2 -1
  14. tesorotools_python-0.1.4/src/tesorotools/artists/tikz_backend.py +172 -0
  15. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/waterfall.py +2 -1
  16. tesorotools_python-0.1.4/src/tesorotools/assets/nota_template.dotx +0 -0
  17. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/plots.yaml +40 -0
  18. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/style/tesoro.sty +3 -0
  19. tesorotools_python-0.1.4/src/tesorotools/note/__init__.py +40 -0
  20. tesorotools_python-0.1.4/src/tesorotools/note/builder.py +749 -0
  21. tesorotools_python-0.1.4/src/tesorotools/note/cli.py +168 -0
  22. tesorotools_python-0.1.4/src/tesorotools/note/inline.py +129 -0
  23. tesorotools_python-0.1.4/src/tesorotools/note/lint.py +291 -0
  24. tesorotools_python-0.1.4/src/tesorotools/note/markdown.py +613 -0
  25. tesorotools_python-0.1.4/src/tesorotools/note/skeleton.md +75 -0
  26. tesorotools_python-0.1.4/src/tesorotools/note/skill/SKILL.md +171 -0
  27. tesorotools_python-0.1.4/src/tesorotools/note/skill/reglas.md +120 -0
  28. tesorotools_python-0.1.4/src/tesorotools/note/template.py +120 -0
  29. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/globals.py +4 -0
  30. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/app.py +11 -2
  31. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/brand.py +10 -1
  32. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/components.py +32 -12
  33. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/theme.py +29 -6
  34. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/.gitignore +0 -0
  35. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/_build_context.py +0 -0
  36. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/_registry.py +0 -0
  37. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/barh_plot.py +0 -0
  38. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/compact.py +0 -0
  39. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/matrix.py +0 -0
  40. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/sharing.py +0 -0
  41. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/stacked.py +0 -0
  42. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/type_curve.py +0 -0
  43. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/artists/vector_plot.py +0 -0
  44. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/README.md +0 -0
  45. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  46. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  47. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  48. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  49. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  50. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  51. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  52. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  53. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/fonts/README.md +0 -0
  54. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/template.docx +0 -0
  55. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  56. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/data_sources/__init__.py +0 -0
  57. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/data_sources/debug.py +0 -0
  58. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/database/__init__.py +0 -0
  59. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/database/local.py +0 -0
  60. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/database/push.py +0 -0
  61. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/database/shared.py +0 -0
  62. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/dependencies/__init__.py +0 -0
  63. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/dependencies/node.py +0 -0
  64. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/dependencies/resolution.py +0 -0
  65. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/driver.py +0 -0
  66. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/__init__.py +0 -0
  67. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/cli.py +0 -0
  68. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/scaffold.py +0 -0
  69. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/article/main.tex.tmpl +0 -0
  70. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/article/sections/01-introduccion.tex +0 -0
  71. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/article/sections/02-analisis.tex +0 -0
  72. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/article/sections/03-conclusiones.tex +0 -0
  73. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/beamer/main.tex.tmpl +0 -0
  74. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/beamer/slides/01-contexto.tex +0 -0
  75. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/beamer/slides/02-analisis.tex +0 -0
  76. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/beamer/slides/03-conclusiones.tex +0 -0
  77. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/beamer/style/beamerthemeTesoro.sty +0 -0
  78. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/book/appendices/a-fuentes.tex +0 -0
  79. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/book/chapters/01-introduccion.tex +0 -0
  80. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/book/chapters/02-analisis.tex +0 -0
  81. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/book/frontmatter/resumen.tex +0 -0
  82. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/book/main.tex.tmpl +0 -0
  83. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/README.md.tmpl +0 -0
  84. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/dot-gitignore +0 -0
  85. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/dot-vscode/settings.json +0 -0
  86. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/figures/dot-gitkeep +0 -0
  87. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/fonts/README.md +0 -0
  88. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/latexmkrc +0 -0
  89. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/refs.bib +0 -0
  90. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/style/tesorobrand.sty.tmpl +0 -0
  91. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/latex/templates/common/style/tesorolocal.sty.tmpl +0 -0
  92. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/manifest.py +0 -0
  93. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/offsets/__init__.py +0 -0
  94. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/offsets/offsets.py +0 -0
  95. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/offsets/outliers.py +0 -0
  96. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/orchestration.py +0 -0
  97. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/palette.py +0 -0
  98. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/pipeline/__init__.py +0 -0
  99. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/pipeline/diagnose.py +0 -0
  100. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/pipeline/engine.py +0 -0
  101. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/pipeline/rules.py +0 -0
  102. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/providers/__init__.py +0 -0
  103. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/providers/base.py +0 -0
  104. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/providers/bde.py +0 -0
  105. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/providers/ecb.py +0 -0
  106. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/providers/fred.py +0 -0
  107. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/providers/imf_irfcl.py +0 -0
  108. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/providers/lseg.py +0 -0
  109. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/py.typed +0 -0
  110. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/__init__.py +0 -0
  111. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/__init__.py +0 -0
  112. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/content.py +0 -0
  113. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/images.py +0 -0
  114. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/section.py +0 -0
  115. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/subtitle.py +0 -0
  116. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/table.py +0 -0
  117. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/text.py +0 -0
  118. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/content/title.py +0 -0
  119. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/document.py +0 -0
  120. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/render/report.py +0 -0
  121. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/scaffold.py +0 -0
  122. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/testing/__init__.py +0 -0
  123. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/testing/compare.py +0 -0
  124. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/__init__.py +0 -0
  125. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/config.py +0 -0
  126. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/format.py +0 -0
  127. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/matplotlib.py +0 -0
  128. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/series.py +0 -0
  129. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/shortcuts.py +0 -0
  130. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/utils/template.py +0 -0
  131. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/__init__.py +0 -0
  132. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/cli.py +0 -0
  133. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/formatting.py +0 -0
  134. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/launcher.py +0 -0
  135. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/palette.py +0 -0
  136. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/plotting.py +0 -0
  137. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/scaffold.py +0 -0
  138. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/Dockerfile.tmpl +0 -0
  139. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/README.md.tmpl +0 -0
  140. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/dot-dockerignore.tmpl +0 -0
  141. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/dot-gitignore.tmpl +0 -0
  142. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/dot-streamlit/config.toml.tmpl +0 -0
  143. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/pyproject.toml.tmpl +0 -0
  144. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/src/$package/__init__.py.tmpl +0 -0
  145. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/src/$package/cli.py.tmpl +0 -0
  146. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/src/$package/main.py.tmpl +0 -0
  147. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/src/$package/sections.py.tmpl +0 -0
  148. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/tests/__init__.py.tmpl +0 -0
  149. {tesorotools_python-0.1.2 → tesorotools_python-0.1.4}/src/tesorotools/web/templates/tests/test_app.py.tmpl +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tesorotools-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Requires-Python: >=3.13
5
5
  Requires-Dist: babel>=2.17
6
6
  Requires-Dist: matplotlib>=3.10
@@ -38,6 +38,7 @@ web = ["streamlit>=1.62", "plotly>=5.24"]
38
38
  [project.scripts]
39
39
  tesorotools-web = "tesorotools.web.cli:main"
40
40
  tesorotools-latex = "tesorotools.latex.cli:main"
41
+ tesorotools-nota = "tesorotools.note.cli:main"
41
42
 
42
43
  [dependency-groups]
43
44
  dev = [
@@ -66,6 +66,8 @@ from tesorotools.artists import (
66
66
  Legend,
67
67
  LinePlot,
68
68
  MatrixChart,
69
+ SankeyPlot,
70
+ ScatterPlot,
69
71
  ShockChart,
70
72
  StackedAreaPlot,
71
73
  StackedBarPlot,
@@ -108,6 +110,8 @@ def _register_builtins() -> None:
108
110
  register_artist("shock_plot", ShockChart)
109
111
  register_artist("intraday", IntradayPlot)
110
112
  register_artist("waterfall", Waterfall)
113
+ register_artist("sankey", SankeyPlot)
114
+ register_artist("scatter", ScatterPlot)
111
115
 
112
116
  register_tag("format", Format)
113
117
  register_tag("legend", Legend)
@@ -149,6 +153,8 @@ __all__ = [
149
153
  "MatrixChart",
150
154
  "RegistryProtocol",
151
155
  "Report",
156
+ "SankeyPlot",
157
+ "ScatterPlot",
152
158
  "Section",
153
159
  "ShockChart",
154
160
  "StackedAreaPlot",
@@ -45,6 +45,8 @@ from tesorotools.artists.box_plot import BoxPlot
45
45
  from tesorotools.artists.intraday_plot import IntradayPlot
46
46
  from tesorotools.artists.line_plot import LinePlot
47
47
  from tesorotools.artists.matrix import MatrixChart
48
+ from tesorotools.artists.sankey import SankeyPlot
49
+ from tesorotools.artists.scatter import ScatterPlot
48
50
  from tesorotools.artists.sharing import share_y_axes
49
51
  from tesorotools.artists.shock_plot import ShockChart
50
52
  from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
@@ -64,6 +66,8 @@ __all__ = [
64
66
  "Legend",
65
67
  "LinePlot",
66
68
  "MatrixChart",
69
+ "SankeyPlot",
70
+ "ScatterPlot",
67
71
  "ShockChart",
68
72
  "StackedAreaPlot",
69
73
  "StackedBarPlot",
@@ -23,11 +23,13 @@ overrides only need to update ``plots.yaml``.
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ import math
26
27
  from collections.abc import Sequence
27
28
  from pathlib import Path
28
29
  from typing import Any, Literal, Protocol, Self, cast, runtime_checkable
29
30
 
30
31
  import matplotlib.pyplot as plt
32
+ import numpy as np
31
33
  import pandas as pd
32
34
  from matplotlib.axes import Axes
33
35
  from matplotlib.dates import (
@@ -152,7 +154,7 @@ def plotly_unsupported(artist: object) -> frozenset[str]:
152
154
 
153
155
 
154
156
  class FigureLifecycle:
155
- """``save()`` and ``plot()`` on top of a subclass's ``build()``.
157
+ """``save()``, ``plot()`` and ``to_tikz()`` on top of ``build()``.
156
158
 
157
159
  Gives an artist the same three-step life cycle as the rest:
158
160
  ``build()`` returns the figure in memory, ``save()`` writes it
@@ -194,6 +196,17 @@ class FigureLifecycle:
194
196
  plt.close(fig)
195
197
  return ax
196
198
 
199
+ def to_tikz(self, path: Path | None = None) -> Path:
200
+ """Write the chart as PGF code for LaTeX and return the path.
201
+
202
+ By default next to the PNG, with a ``.pgf`` suffix; the document
203
+ includes it with ``\\input``. Needs a TeX installation (see
204
+ :mod:`tesorotools.artists.tikz_backend`).
205
+ """
206
+ from tesorotools.artists.tikz_backend import render_tikz
207
+
208
+ return render_tikz(self, path)
209
+
197
210
 
198
211
  # ----------------------------------------------------------------------
199
212
  # Structural contract for image artists
@@ -980,6 +993,40 @@ def apply_date_axis(
980
993
  label.set_rotation_mode("anchor")
981
994
 
982
995
 
996
+ class DecimalsLocator(MaxNLocator):
997
+ """Matplotlib's default y ticks, kept printable at *decimals*.
998
+
999
+ The default locator steps by 1, 2, 2.5 or 5 of a decade, so with a
1000
+ tick format of two decimals it can put ticks every 0.005, and two
1001
+ neighbours print alike (``1,17`` twice). When the step is not a
1002
+ whole multiple of ``10 ** -decimals`` (the unit) the next of 1, 2 or
1003
+ 5 units takes its place: fewer ticks, all different. Otherwise the
1004
+ ticks are exactly the default ones.
1005
+ """
1006
+
1007
+ def __init__(self, decimals: int, nbins: int | Literal["auto"] = "auto"):
1008
+ super().__init__( # type: ignore[reportUnknownMemberType]
1009
+ nbins=nbins, steps=[1, 2, 2.5, 5, 10]
1010
+ )
1011
+ self.decimals = decimals
1012
+
1013
+ def tick_values(self, vmin: float, vmax: float) -> Any:
1014
+ ticks = super().tick_values(vmin, vmax)
1015
+ if len(ticks) < 2:
1016
+ return ticks
1017
+ unit = 10.0**-self.decimals
1018
+ step = float(ticks[1] - ticks[0])
1019
+ ratio = step / unit
1020
+ if round(ratio) >= 1 and math.isclose(ratio, round(ratio)):
1021
+ return ticks
1022
+ # The default steps are 1, 2, 2.5 or 5 times a power of ten, so a
1023
+ # step that is no whole multiple of the unit is below 5 units.
1024
+ new = next(f * unit for f in (1, 2, 5) if f * unit >= step)
1025
+ low = math.floor(min(vmin, vmax) / new) * new
1026
+ high = math.ceil(max(vmin, vmax) / new) * new
1027
+ return np.arange(low, high + new / 2, new)
1028
+
1029
+
983
1030
  def compact_y_axis(ax: Axes) -> None:
984
1031
  """Denser y-ticks for compact charts (more reference numbers).
985
1032
 
@@ -994,6 +1041,12 @@ def compact_y_axis(ax: Axes) -> None:
994
1041
  raw: Any = COMPACT_CONFIG.get("y_max_ticks")
995
1042
  if raw is None:
996
1043
  return
1044
+ current = ax.yaxis.get_major_locator()
1045
+ if isinstance(current, DecimalsLocator):
1046
+ ax.yaxis.set_major_locator(
1047
+ DecimalsLocator(current.decimals, nbins=int(raw))
1048
+ )
1049
+ return
997
1050
  ax.yaxis.set_major_locator(
998
1051
  MaxNLocator(nbins=int(raw), steps=[1, 2, 2.5, 5, 10])
999
1052
  )
@@ -1174,6 +1227,7 @@ def place_legend(
1174
1227
  compact: bool,
1175
1228
  ncol: int | None = None,
1176
1229
  position: str = "auto",
1230
+ entries: tuple[list[Any], list[str]] | None = None,
1177
1231
  ) -> MplLegend | None:
1178
1232
  """Place the figure legend, compact-aware, from the labelled artists.
1179
1233
 
@@ -1184,9 +1238,14 @@ def place_legend(
1184
1238
  ``rcParams`` metrics. ``position`` (``"above"`` / ``"below"`` /
1185
1239
  ``"auto"``) chooses the side through :func:`resolve_legend_position`, so
1186
1240
  a compact chart can carry its legend under the plot instead of over it.
1241
+ ``entries`` passes precomputed ``(handles, labels)`` instead of reading
1242
+ them off *ax* -- for charts whose series span several axes (a
1243
+ ``twinx`` secondary y-axis), in the order the legend should list them.
1187
1244
  Returns ``None`` (drawing nothing) when there are no labels.
1188
1245
  """
1189
- handles, labels = ax.get_legend_handles_labels()
1246
+ handles, labels = (
1247
+ entries if entries is not None else ax.get_legend_handles_labels()
1248
+ )
1190
1249
  if not labels:
1191
1250
  return None
1192
1251
  side = resolve_legend_position(position, compact=compact)
@@ -1282,6 +1341,7 @@ def style_spines(
1282
1341
  return format_annotation(y, decimals, units)
1283
1342
 
1284
1343
  ax.yaxis.set_major_formatter(FuncFormatter(_fmt_tick))
1344
+ ax.yaxis.set_major_locator(DecimalsLocator(decimals))
1285
1345
  ax.set_xlabel( # type: ignore[reportUnknownMemberType]
1286
1346
  ""
1287
1347
  )
@@ -1,10 +1,14 @@
1
1
  """Bar + line combo chart artist, registered as ``!bar_line``.
2
2
 
3
3
  Defines :class:`BarLinePlot`: one time-series chart that draws some
4
- series as **vertical bars** and others as **marked lines** on a single
5
- shared y-axis. The canonical use is a secondary magnitude in bars (an
6
- earnings *surprise* in p.p.) under a primary series as the highlighted
7
- line (earnings *growth* in %), both read against the same 0 baseline.
4
+ series as **vertical bars** and others as **marked lines**, by default
5
+ on a single shared y-axis. The canonical use is a secondary magnitude in
6
+ bars (an earnings *surprise* in p.p.) under a primary series as the
7
+ highlighted line (earnings *growth* in %), both read against the same 0
8
+ baseline. When the scales are too far apart to share an axis (an
9
+ exchange rate around 1.2 against an R^2 between 0 and 1), ``secondary``
10
+ moves any subset of the series, bars or lines, to a right-hand y-axis
11
+ with its own scale, format and title.
8
12
 
9
13
  Unlike :class:`LinePlot`, the x-axis is **categorical**: one slot per
10
14
  observation (``np.arange``) with date-formatted tick labels, so the bars
@@ -38,6 +42,7 @@ from matplotlib.figure import Figure
38
42
  from yaml.nodes import MappingNode
39
43
 
40
44
  from tesorotools.artists._common import (
45
+ FigureLifecycle,
41
46
  AX_CONFIG,
42
47
  Format,
43
48
  Legend,
@@ -62,8 +67,8 @@ __all__ = ["BarLinePlot"]
62
67
  BAR_LINE_CONFIG: dict[str, Any] = PLOT_CONFIG["bar_line"]
63
68
 
64
69
 
65
- class BarLinePlot:
66
- """Combo chart: vertical bars and marked lines on one shared axis.
70
+ class BarLinePlot(FigureLifecycle):
71
+ """Combo chart: vertical bars and marked lines, optionally on two axes.
67
72
 
68
73
  Parameters mirror the other time-series artists where they overlap
69
74
  (``out_path``, ``data`` / ``data_path``, ``scale``, ``start_date`` /
@@ -87,6 +92,21 @@ class BarLinePlot:
87
92
  ``date_format`` is applied via ``strftime`` (lower-cased, e.g.
88
93
  ``"dic-21"``) to a DatetimeIndex; ``max_xticks`` thins the labels
89
94
  (``None`` labels every slot).
95
+ * ``secondary`` -- ids (from ``bars`` and/or ``lines``) drawn against a
96
+ second y-axis on the right, with its own scale. Empty (the default)
97
+ keeps the single shared axis, ticked on the house side (right). With
98
+ a secondary axis the primary one moves to the left, its grid is the
99
+ only one drawn, and the legend still lists every series in declared
100
+ order (bars, then lines). It cannot hold every series: the primary
101
+ axis would be left empty.
102
+ * ``secondary_fmt`` -- tick format (``!format``) of the secondary axis;
103
+ defaults to ``fmt``.
104
+ * ``ylabel`` / ``secondary_ylabel`` -- y-axis titles (empty: none).
105
+ ``ylabel`` sits on the primary axis' tick side.
106
+
107
+ ``baseline`` draws the 0 line on every axis that holds bars (they grow
108
+ from it) or, with no bars at all, on the primary axis, so a lines-only
109
+ axis is never stretched down to 0.
90
110
  """
91
111
 
92
112
  #: Keys that change the PNG but never the ``to_plotly()`` figure
@@ -102,6 +122,7 @@ class BarLinePlot:
102
122
  "legend.ncol",
103
123
  "max_xticks",
104
124
  "plot_size",
125
+ "secondary_fmt.decimals",
105
126
  "x_rotation",
106
127
  }
107
128
  )
@@ -130,6 +151,10 @@ class BarLinePlot:
130
151
  max_xticks: int | None = None,
131
152
  compact: bool = False,
132
153
  font_pt: float | None = None,
154
+ secondary: list[str] | None = None,
155
+ secondary_fmt: Format | None = None,
156
+ ylabel: str = "",
157
+ secondary_ylabel: str = "",
133
158
  ) -> None:
134
159
  if out_path.suffix != ".png":
135
160
  raise ValueError(f"out_path must be .png: {out_path}")
@@ -139,6 +164,17 @@ class BarLinePlot:
139
164
  raise ValueError(
140
165
  "BarLinePlot needs at least one series in `bars` or `lines`"
141
166
  )
167
+ self.secondary = list(secondary or [])
168
+ unknown = [c for c in self.secondary if c not in self.bars | self.lines]
169
+ if unknown:
170
+ raise ValueError(
171
+ f"`secondary` ids must be in `bars` or `lines`: {unknown}"
172
+ )
173
+ if set(self.secondary) >= {*self.bars, *self.lines}:
174
+ raise ValueError(
175
+ "`secondary` cannot hold every series: the primary axis "
176
+ "would be empty"
177
+ )
142
178
  self.out_path = out_path
143
179
  self.data = resolve_data(data, data_path)
144
180
  self.scale = scale
@@ -157,6 +193,9 @@ class BarLinePlot:
157
193
  self.max_xticks = max_xticks
158
194
  self.compact = compact
159
195
  self.font_pt = font_pt
196
+ self.secondary_fmt = secondary_fmt or self.fmt
197
+ self.ylabel = ylabel
198
+ self.secondary_ylabel = secondary_ylabel
160
199
 
161
200
  @classmethod
162
201
  def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
@@ -179,7 +218,8 @@ class BarLinePlot:
179
218
  ``fmt`` (``!format``), ``legend`` (``!legend``), ``figsize``,
180
219
  ``plot_size``, ``bar_styles`` / ``line_styles``, ``bar_width``,
181
220
  ``date_format``, ``x_rotation``, ``max_xticks``, ``compact``,
182
- ``font_pt``.
221
+ ``font_pt``, ``secondary``, ``secondary_fmt`` (``!format``),
222
+ ``ylabel`` / ``secondary_ylabel``.
183
223
 
184
224
  Example
185
225
  -------
@@ -194,6 +234,26 @@ class BarLinePlot:
194
234
  surprise: "Sorpresa (pp)"
195
235
  fmt: !format {decimals: 0}
196
236
  legend: !legend {}
237
+
238
+ With scales too far apart for one axis, send some series to the
239
+ right-hand axis:
240
+
241
+ .. code-block:: yaml
242
+
243
+ eurusd_fair_value: !bar_line
244
+ out_path: out/eurusd_fair_value.png
245
+ data_path: data/eurusd.feather
246
+ bars:
247
+ r2: "R^2"
248
+ lines:
249
+ eurusd: "EURUSD"
250
+ fair_value: "Modelo valor justo (60 días)"
251
+ secondary: [r2]
252
+ fmt: !format {decimals: 2}
253
+ secondary_fmt: !format {decimals: 1}
254
+ ylabel: "EURUSD"
255
+ secondary_ylabel: "Coeficiente de determinación (R^2)"
256
+ legend: !legend {}
197
257
  """
198
258
  cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
199
259
  node, deep=True
@@ -287,9 +347,13 @@ class BarLinePlot:
287
347
  **compact_figure_kwargs(self.compact, self.figsize)
288
348
  )
289
349
  ax: Axes = fig.add_subplot()
350
+ ax2: Axes | None = ax.twinx() if self.secondary else None
351
+
352
+ def target(col: str) -> Axes:
353
+ return ax2 if ax2 is not None and col in self.secondary else ax
290
354
 
291
355
  # Bars first (behind the lines). Several bar series share each slot,
292
- # dodged side by side; a single series is centred.
356
+ # dodged side by side (across both axes); a single one is centred.
293
357
  n_bars = len(self.bars)
294
358
  each = self.bar_width / n_bars if n_bars else self.bar_width
295
359
  default_alpha: float = float(BAR_LINE_CONFIG["bar"].get("alpha", 0.9))
@@ -299,7 +363,7 @@ class BarLinePlot:
299
363
  style.setdefault("alpha", default_alpha)
300
364
  offset = (j - (n_bars - 1) / 2) * each
301
365
  values = plot_data[col].fillna(0).to_numpy(dtype=np.float64)
302
- ax.bar( # type: ignore[reportUnknownMemberType]
366
+ target(col).bar( # type: ignore[reportUnknownMemberType]
303
367
  x + offset,
304
368
  values,
305
369
  width=each,
@@ -317,7 +381,7 @@ class BarLinePlot:
317
381
  style.setdefault("marker", line_cfg.get("marker", "o"))
318
382
  style.setdefault("linewidth", line_cfg.get("linewidth", 2))
319
383
  style.setdefault("markersize", line_cfg.get("markersize", 5))
320
- ax.plot( # type: ignore[reportUnknownMemberType]
384
+ target(col).plot( # type: ignore[reportUnknownMemberType]
321
385
  x,
322
386
  plot_data[col].to_numpy(dtype=np.float64),
323
387
  label=label,
@@ -336,14 +400,31 @@ class BarLinePlot:
336
400
  # style_spines enables the y grid only (axis="y"), which is what these
337
401
  # charts read against; the categorical x-axis stays gridless.
338
402
  if self.baseline:
339
- style_baseline(ax, 0, **AX_CONFIG["baseline"])
340
-
403
+ on_primary, on_secondary = self._baseline_on()
404
+ for axis, on in ((ax, on_primary), (ax2, on_secondary)):
405
+ if on and axis is not None:
406
+ style_baseline(axis, 0, **AX_CONFIG["baseline"])
407
+
408
+ side = "right"
409
+ if ax2 is not None:
410
+ side = "left"
411
+ ax.yaxis.tick_left()
412
+ self._style_secondary(ax, ax2, font_pt)
341
413
  if self.compact:
342
- style_compact_axes(ax, font_pt, side="right")
414
+ style_compact_axes(ax, font_pt, side=side)
415
+ if self.ylabel:
416
+ ax.yaxis.set_label_position(side) # type: ignore[reportArgumentType]
417
+ ax.set_ylabel( # type: ignore[reportUnknownMemberType]
418
+ self.ylabel, fontsize=font_pt
419
+ )
343
420
 
344
421
  if self.legend is not None:
345
422
  place_legend(
346
- fig, ax, compact=self.compact, **legend_layout(self.legend)
423
+ fig,
424
+ ax,
425
+ compact=self.compact,
426
+ entries=self._legend_entries(ax, ax2),
427
+ **legend_layout(self.legend),
347
428
  )
348
429
 
349
430
  if self.plot_size is not None and not self.compact:
@@ -351,6 +432,58 @@ class BarLinePlot:
351
432
 
352
433
  return fig, ax
353
434
 
435
+ def _baseline_on(self) -> tuple[bool, bool]:
436
+ """``(primary, secondary)``: the axes that get the 0 line.
437
+
438
+ Those holding bars (they grow from 0) or, with no bars at all, the
439
+ primary one. Shared by :meth:`build` and :meth:`to_plotly`.
440
+ """
441
+ on_secondary = [c in self.secondary for c in self.bars]
442
+ secondary = any(on_secondary)
443
+ primary = not all(on_secondary) or not secondary
444
+ return primary, secondary
445
+
446
+ def _style_secondary(
447
+ self, ax: Axes, ax2: Axes, font_pt: float | None
448
+ ) -> None:
449
+ """Style the right-hand axis: own format, no grid, title, z-order.
450
+
451
+ The twin is drawn above the primary by default, which would hide
452
+ the primary series behind secondary bars; when the secondary holds
453
+ bars the primary is brought forward with a transparent background.
454
+ """
455
+ style_spines(
456
+ ax2,
457
+ decimals=self.secondary_fmt.decimals,
458
+ units=self.secondary_fmt.units,
459
+ **AX_CONFIG["spines"],
460
+ )
461
+ ax2.grid(False) # type: ignore[reportUnknownMemberType]
462
+ if self.compact:
463
+ style_compact_axes(ax2, font_pt, side="right")
464
+ if self.secondary_ylabel:
465
+ ax2.set_ylabel( # type: ignore[reportUnknownMemberType]
466
+ self.secondary_ylabel, fontsize=font_pt
467
+ )
468
+ if any(c in self.secondary for c in self.bars):
469
+ ax.set_zorder(ax2.get_zorder() + 1)
470
+ ax.patch.set_visible(False)
471
+
472
+ def _legend_entries(
473
+ self, ax: Axes, ax2: Axes | None
474
+ ) -> tuple[list[Any], list[str]]:
475
+ """Handles and labels of both axes, in declared order."""
476
+ handles, labels = ax.get_legend_handles_labels()
477
+ if ax2 is not None:
478
+ more_handles, more_labels = ax2.get_legend_handles_labels()
479
+ handles, labels = handles + more_handles, labels + more_labels
480
+ order = [*self.bars.values(), *self.lines.values()]
481
+ pairs = sorted(
482
+ zip(handles, labels, strict=True),
483
+ key=lambda pair: order.index(pair[1]),
484
+ )
485
+ return [h for h, _ in pairs], [label for _, label in pairs]
486
+
354
487
  def save(
355
488
  self,
356
489
  fig: Figure,
@@ -44,6 +44,7 @@ from matplotlib.patches import Patch
44
44
  from yaml.nodes import MappingNode
45
45
 
46
46
  from tesorotools.artists._common import (
47
+ FigureLifecycle,
47
48
  AX_CONFIG,
48
49
  Format,
49
50
  Legend,
@@ -105,7 +106,7 @@ def _weighted_quantile(
105
106
  return float(np.interp(q, cum, v))
106
107
 
107
108
 
108
- class BoxPlot:
109
+ class BoxPlot(FigureLifecycle):
109
110
  """Box-and-whisker chart, one box per category.
110
111
 
111
112
  Parameters mirror the other artists where they overlap (``out_path``,
@@ -42,6 +42,7 @@ from matplotlib.figure import Figure
42
42
  from yaml.nodes import MappingNode
43
43
 
44
44
  from tesorotools.artists._common import (
45
+ FigureLifecycle,
45
46
  AX_CONFIG,
46
47
  Format,
47
48
  Legend,
@@ -66,7 +67,7 @@ __all__ = ["IntradayPlot"]
66
67
  _MAX_XTICKS = 10
67
68
 
68
69
 
69
- class IntradayPlot:
70
+ class IntradayPlot(FigureLifecycle):
70
71
  """Single-instrument intraday line chart on a gap-free ordinal axis.
71
72
 
72
73
  Parameters mirror the other image artists where they overlap
@@ -23,6 +23,8 @@ from matplotlib.ticker import FuncFormatter
23
23
  from yaml.nodes import MappingNode
24
24
 
25
25
  from tesorotools.artists._common import (
26
+ DecimalsLocator,
27
+ FigureLifecycle,
26
28
  AX_CONFIG,
27
29
  DATE_DENSITY_LEVELS,
28
30
  Format,
@@ -56,7 +58,7 @@ locale.setlocale(locale.LC_ALL, "")
56
58
  __all__ = ["LinePlot"]
57
59
 
58
60
 
59
- class LinePlot:
61
+ class LinePlot(FigureLifecycle):
60
62
  """Time-series line chart with the tesorotools visual style.
61
63
 
62
64
  Parameters mirror the other image artists where they
@@ -542,6 +544,7 @@ class LinePlot:
542
544
  return format_annotation(y, axis_decimals, units)
543
545
 
544
546
  ax.yaxis.set_major_formatter(FuncFormatter(_fmt_axis_tick))
547
+ ax.yaxis.set_major_locator(DecimalsLocator(axis_decimals))
545
548
  if self.baseline:
546
549
  reference = 100 if self.base_100 else 0
547
550
  style_baseline(ax, reference, **AX_CONFIG["baseline"])