tesorotools-python 0.1.3__tar.gz → 0.1.5__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 (152) hide show
  1. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/PKG-INFO +1 -1
  2. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/pyproject.toml +1 -0
  3. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/__init__.py +6 -0
  4. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/__init__.py +4 -0
  5. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/_common.py +66 -4
  6. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/bar_line.py +2 -1
  7. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/box_plot.py +3 -2
  8. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/intraday_plot.py +2 -1
  9. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/line_plot.py +4 -1
  10. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/plotly_backend.py +142 -1
  11. tesorotools_python-0.1.5/src/tesorotools/artists/sankey.py +436 -0
  12. tesorotools_python-0.1.5/src/tesorotools/artists/scatter.py +469 -0
  13. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/shock_plot.py +2 -1
  14. tesorotools_python-0.1.5/src/tesorotools/artists/tikz_backend.py +172 -0
  15. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/waterfall.py +2 -1
  16. tesorotools_python-0.1.5/src/tesorotools/assets/nota_template.dotx +0 -0
  17. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/plots.yaml +40 -0
  18. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/style/tesoro.sty +3 -0
  19. tesorotools_python-0.1.5/src/tesorotools/note/__init__.py +65 -0
  20. tesorotools_python-0.1.5/src/tesorotools/note/builder.py +1218 -0
  21. tesorotools_python-0.1.5/src/tesorotools/note/charts.py +134 -0
  22. tesorotools_python-0.1.5/src/tesorotools/note/cli.py +308 -0
  23. tesorotools_python-0.1.5/src/tesorotools/note/inline.py +358 -0
  24. tesorotools_python-0.1.5/src/tesorotools/note/issues.py +48 -0
  25. tesorotools_python-0.1.5/src/tesorotools/note/lint.py +1046 -0
  26. tesorotools_python-0.1.5/src/tesorotools/note/markdown.py +1513 -0
  27. tesorotools_python-0.1.5/src/tesorotools/note/pdf.py +123 -0
  28. tesorotools_python-0.1.5/src/tesorotools/note/skeleton.md +109 -0
  29. tesorotools_python-0.1.5/src/tesorotools/note/skill/SKILL.md +382 -0
  30. tesorotools_python-0.1.5/src/tesorotools/note/skill/reglas.md +142 -0
  31. tesorotools_python-0.1.5/src/tesorotools/note/template.py +120 -0
  32. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/globals.py +4 -0
  33. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/app.py +11 -2
  34. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/brand.py +10 -1
  35. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/components.py +32 -12
  36. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/theme.py +29 -6
  37. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/.gitignore +0 -0
  38. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/_build_context.py +0 -0
  39. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/_registry.py +0 -0
  40. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/barh_plot.py +0 -0
  41. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/compact.py +0 -0
  42. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/matrix.py +0 -0
  43. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/sharing.py +0 -0
  44. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/stacked.py +0 -0
  45. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/type_curve.py +0 -0
  46. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/artists/vector_plot.py +0 -0
  47. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/README.md +0 -0
  48. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  49. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  50. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  51. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  52. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  53. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  54. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  55. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  56. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/fonts/README.md +0 -0
  57. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/template.docx +0 -0
  58. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  59. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/data_sources/__init__.py +0 -0
  60. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/data_sources/debug.py +0 -0
  61. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/database/__init__.py +0 -0
  62. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/database/local.py +0 -0
  63. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/database/push.py +0 -0
  64. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/database/shared.py +0 -0
  65. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/dependencies/__init__.py +0 -0
  66. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/dependencies/node.py +0 -0
  67. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/dependencies/resolution.py +0 -0
  68. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/driver.py +0 -0
  69. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/__init__.py +0 -0
  70. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/cli.py +0 -0
  71. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/scaffold.py +0 -0
  72. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/article/main.tex.tmpl +0 -0
  73. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/article/sections/01-introduccion.tex +0 -0
  74. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/article/sections/02-analisis.tex +0 -0
  75. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/article/sections/03-conclusiones.tex +0 -0
  76. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/beamer/main.tex.tmpl +0 -0
  77. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/beamer/slides/01-contexto.tex +0 -0
  78. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/beamer/slides/02-analisis.tex +0 -0
  79. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/beamer/slides/03-conclusiones.tex +0 -0
  80. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/beamer/style/beamerthemeTesoro.sty +0 -0
  81. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/book/appendices/a-fuentes.tex +0 -0
  82. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/book/chapters/01-introduccion.tex +0 -0
  83. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/book/chapters/02-analisis.tex +0 -0
  84. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/book/frontmatter/resumen.tex +0 -0
  85. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/book/main.tex.tmpl +0 -0
  86. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/README.md.tmpl +0 -0
  87. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/dot-gitignore +0 -0
  88. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/dot-vscode/settings.json +0 -0
  89. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/figures/dot-gitkeep +0 -0
  90. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/fonts/README.md +0 -0
  91. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/latexmkrc +0 -0
  92. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/refs.bib +0 -0
  93. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/style/tesorobrand.sty.tmpl +0 -0
  94. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/latex/templates/common/style/tesorolocal.sty.tmpl +0 -0
  95. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/manifest.py +0 -0
  96. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/offsets/__init__.py +0 -0
  97. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/offsets/offsets.py +0 -0
  98. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/offsets/outliers.py +0 -0
  99. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/orchestration.py +0 -0
  100. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/palette.py +0 -0
  101. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/pipeline/__init__.py +0 -0
  102. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/pipeline/diagnose.py +0 -0
  103. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/pipeline/engine.py +0 -0
  104. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/pipeline/rules.py +0 -0
  105. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/providers/__init__.py +0 -0
  106. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/providers/base.py +0 -0
  107. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/providers/bde.py +0 -0
  108. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/providers/ecb.py +0 -0
  109. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/providers/fred.py +0 -0
  110. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/providers/imf_irfcl.py +0 -0
  111. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/providers/lseg.py +0 -0
  112. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/py.typed +0 -0
  113. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/__init__.py +0 -0
  114. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/__init__.py +0 -0
  115. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/content.py +0 -0
  116. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/images.py +0 -0
  117. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/section.py +0 -0
  118. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/subtitle.py +0 -0
  119. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/table.py +0 -0
  120. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/text.py +0 -0
  121. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/content/title.py +0 -0
  122. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/document.py +0 -0
  123. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/render/report.py +0 -0
  124. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/scaffold.py +0 -0
  125. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/testing/__init__.py +0 -0
  126. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/testing/compare.py +0 -0
  127. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/__init__.py +0 -0
  128. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/config.py +0 -0
  129. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/format.py +0 -0
  130. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/matplotlib.py +0 -0
  131. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/series.py +0 -0
  132. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/shortcuts.py +0 -0
  133. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/utils/template.py +0 -0
  134. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/__init__.py +0 -0
  135. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/cli.py +0 -0
  136. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/formatting.py +0 -0
  137. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/launcher.py +0 -0
  138. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/palette.py +0 -0
  139. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/plotting.py +0 -0
  140. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/scaffold.py +0 -0
  141. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/Dockerfile.tmpl +0 -0
  142. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/README.md.tmpl +0 -0
  143. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/dot-dockerignore.tmpl +0 -0
  144. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/dot-gitignore.tmpl +0 -0
  145. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/dot-streamlit/config.toml.tmpl +0 -0
  146. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/pyproject.toml.tmpl +0 -0
  147. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/src/$package/__init__.py.tmpl +0 -0
  148. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/src/$package/cli.py.tmpl +0 -0
  149. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/src/$package/main.py.tmpl +0 -0
  150. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/src/$package/sections.py.tmpl +0 -0
  151. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/src/tesorotools/web/templates/tests/__init__.py.tmpl +0 -0
  152. {tesorotools_python-0.1.3 → tesorotools_python-0.1.5}/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.3
3
+ Version: 0.1.5
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
@@ -611,7 +624,7 @@ def compact_legend_font_pt() -> float:
611
624
  )
612
625
 
613
626
 
614
- def compact_legend_kwargs() -> dict[str, Any]:
627
+ def compact_legend_kwargs(ax: Axes | None = None) -> dict[str, Any]:
615
628
  """Legend styling for compact charts, from the ``compact`` config.
616
629
 
617
630
  Returns ``fontsize`` / ``handlelength`` / ``handletextpad`` /
@@ -620,9 +633,17 @@ def compact_legend_kwargs() -> dict[str, Any]:
620
633
  the metrics used to *draw* the legend in :func:`legend_above` -- the
621
634
  invariant that prevents an over-counted ``ncol``. The smaller font
622
635
  and shorter handles let several wide labels share one legend row.
636
+
637
+ With *ax*, the legend is never larger than its tick labels: a chart
638
+ drawn with a small ``font_pt`` (the 7 pt of the notes) keeps the
639
+ legend at that size instead of ``legend_font_pt``.
623
640
  """
641
+ fontsize = compact_legend_font_pt()
642
+ ticks = (ax.get_yticklabels() or ax.get_xticklabels()) if ax else []
643
+ if ticks:
644
+ fontsize = min(fontsize, float(ticks[0].get_fontsize()))
624
645
  return {
625
- "fontsize": compact_legend_font_pt(),
646
+ "fontsize": fontsize,
626
647
  "handlelength": float(COMPACT_CONFIG.get("legend_handlelength", 1.2)),
627
648
  "handletextpad": float(COMPACT_CONFIG.get("legend_handletextpad", 0.4)),
628
649
  "columnspacing": float(COMPACT_CONFIG.get("legend_columnspacing", 0.8)),
@@ -980,6 +1001,40 @@ def apply_date_axis(
980
1001
  label.set_rotation_mode("anchor")
981
1002
 
982
1003
 
1004
+ class DecimalsLocator(MaxNLocator):
1005
+ """Matplotlib's default y ticks, kept printable at *decimals*.
1006
+
1007
+ The default locator steps by 1, 2, 2.5 or 5 of a decade, so with a
1008
+ tick format of two decimals it can put ticks every 0.005, and two
1009
+ neighbours print alike (``1,17`` twice). When the step is not a
1010
+ whole multiple of ``10 ** -decimals`` (the unit) the next of 1, 2 or
1011
+ 5 units takes its place: fewer ticks, all different. Otherwise the
1012
+ ticks are exactly the default ones.
1013
+ """
1014
+
1015
+ def __init__(self, decimals: int, nbins: int | Literal["auto"] = "auto"):
1016
+ super().__init__( # type: ignore[reportUnknownMemberType]
1017
+ nbins=nbins, steps=[1, 2, 2.5, 5, 10]
1018
+ )
1019
+ self.decimals = decimals
1020
+
1021
+ def tick_values(self, vmin: float, vmax: float) -> Any:
1022
+ ticks = super().tick_values(vmin, vmax)
1023
+ if len(ticks) < 2:
1024
+ return ticks
1025
+ unit = 10.0**-self.decimals
1026
+ step = float(ticks[1] - ticks[0])
1027
+ ratio = step / unit
1028
+ if round(ratio) >= 1 and math.isclose(ratio, round(ratio)):
1029
+ return ticks
1030
+ # The default steps are 1, 2, 2.5 or 5 times a power of ten, so a
1031
+ # step that is no whole multiple of the unit is below 5 units.
1032
+ new = next(f * unit for f in (1, 2, 5) if f * unit >= step)
1033
+ low = math.floor(min(vmin, vmax) / new) * new
1034
+ high = math.ceil(max(vmin, vmax) / new) * new
1035
+ return np.arange(low, high + new / 2, new)
1036
+
1037
+
983
1038
  def compact_y_axis(ax: Axes) -> None:
984
1039
  """Denser y-ticks for compact charts (more reference numbers).
985
1040
 
@@ -994,6 +1049,12 @@ def compact_y_axis(ax: Axes) -> None:
994
1049
  raw: Any = COMPACT_CONFIG.get("y_max_ticks")
995
1050
  if raw is None:
996
1051
  return
1052
+ current = ax.yaxis.get_major_locator()
1053
+ if isinstance(current, DecimalsLocator):
1054
+ ax.yaxis.set_major_locator(
1055
+ DecimalsLocator(current.decimals, nbins=int(raw))
1056
+ )
1057
+ return
997
1058
  ax.yaxis.set_major_locator(
998
1059
  MaxNLocator(nbins=int(raw), steps=[1, 2, 2.5, 5, 10])
999
1060
  )
@@ -1198,7 +1259,7 @@ def place_legend(
1198
1259
  side = resolve_legend_position(position, compact=compact)
1199
1260
  fig_w_px: float = fig.get_size_inches()[0] * fig.dpi
1200
1261
  if compact:
1201
- legend_kw = compact_legend_kwargs()
1262
+ legend_kw = compact_legend_kwargs(ax)
1202
1263
  if ncol is None:
1203
1264
  ncol = auto_ncol(
1204
1265
  ax, labels, available_width_px=fig_w_px, **legend_kw
@@ -1288,6 +1349,7 @@ def style_spines(
1288
1349
  return format_annotation(y, decimals, units)
1289
1350
 
1290
1351
  ax.yaxis.set_major_formatter(FuncFormatter(_fmt_tick))
1352
+ ax.yaxis.set_major_locator(DecimalsLocator(decimals))
1291
1353
  ax.set_xlabel( # type: ignore[reportUnknownMemberType]
1292
1354
  ""
1293
1355
  )
@@ -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__ = ["BarLinePlot"]
66
67
  BAR_LINE_CONFIG: dict[str, Any] = PLOT_CONFIG["bar_line"]
67
68
 
68
69
 
69
- class BarLinePlot:
70
+ class BarLinePlot(FigureLifecycle):
70
71
  """Combo chart: vertical bars and marked lines, optionally on two axes.
71
72
 
72
73
  Parameters mirror the other time-series artists where they overlap
@@ -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``,
@@ -498,7 +499,7 @@ class BoxPlot:
498
499
  self.legend.position, compact=self.compact
499
500
  )
500
501
  if self.compact:
501
- kw = compact_legend_kwargs()
502
+ kw = compact_legend_kwargs(ax)
502
503
  ncol = self.legend.ncol or auto_ncol(
503
504
  ax, labels, available_width_px=fig_w_px, **kw
504
505
  )
@@ -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"])
@@ -20,7 +20,7 @@ never depends on plotly.
20
20
 
21
21
  Coverage: every image artist except ``IntradayPlot`` -- line, type
22
22
  curve, stacked area, stacked bar, bar+line, box, shock, waterfall,
23
- horizontal bar, grouped bar, matrix and vector.
23
+ horizontal bar, grouped bar, matrix, vector, sankey and scatter.
24
24
 
25
25
  Known mimicry caveats (the styling that does NOT transfer for free between
26
26
  backends, per the dual-backend research):
@@ -64,6 +64,8 @@ if TYPE_CHECKING:
64
64
  )
65
65
  from tesorotools.artists.line_plot import LinePlot
66
66
  from tesorotools.artists.matrix import MatrixChart
67
+ from tesorotools.artists.sankey import SankeyPlot
68
+ from tesorotools.artists.scatter import ScatterPlot
67
69
  from tesorotools.artists.shock_plot import ShockChart
68
70
  from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
69
71
  from tesorotools.artists.type_curve import TypeCurve
@@ -78,6 +80,8 @@ __all__ = [
78
80
  "house_template",
79
81
  "line_plot_to_figure",
80
82
  "matrix_to_figure",
83
+ "sankey_to_figure",
84
+ "scatter_to_figure",
81
85
  "shock_chart_to_figure",
82
86
  "stacked_area_to_figure",
83
87
  "stacked_bar_to_figure",
@@ -1092,3 +1096,140 @@ def vector_plot_to_figure(chart: VectorPlot) -> go.Figure:
1092
1096
  ticksuffix=chart.y_fmt.units or "",
1093
1097
  )
1094
1098
  return fig
1099
+
1100
+
1101
+ def sankey_to_figure(chart: SankeyPlot) -> go.Figure:
1102
+ """Interactive ``Figure`` from a :class:`SankeyPlot` (``go.Sankey``).
1103
+
1104
+ Consumes the same layout as ``SankeyPlot.build``: one Plotly node per
1105
+ (stage, node) pair, pinned at its column and at the vertical centre
1106
+ the static chart gives it (``arrangement="fixed"``), and one link per
1107
+ flow coloured like its source node with ``sankey.flow_alpha``. Node
1108
+ labels carry the node value, as the end columns of the PNG do.
1109
+ """
1110
+ from tesorotools.artists.sankey import SANKEY_CONFIG
1111
+
1112
+ lay = chart._layout()
1113
+ n_col = len(lay.columns)
1114
+ alpha = float(SANKEY_CONFIG.get("flow_alpha", 0.45))
1115
+ index: dict[tuple[int, str], int] = {}
1116
+ labels: list[str] = []
1117
+ colors: list[str] = []
1118
+ xs: list[float] = []
1119
+ ys: list[float] = []
1120
+ for (c, n), top in lay.tops.items():
1121
+ index[(c, n)] = len(labels)
1122
+ height = lay.heights[(c, n)]
1123
+ labels.append(f"{chart.label(n)} {chart.value_text(height)}")
1124
+ colors.append(_color(chart.color(n)))
1125
+ # Plotly pins nodes strictly inside (0, 1); y grows downwards.
1126
+ xs.append(min(max(c / (n_col - 1), 0.001), 0.999))
1127
+ centre = 1 - (top - height / 2) / lay.total_height
1128
+ ys.append(min(max(centre, 0.001), 0.999))
1129
+ fig = _new_figure()
1130
+ fig.add_trace(
1131
+ go.Sankey(
1132
+ arrangement="fixed",
1133
+ valueformat=f".{chart.fmt.decimals}f",
1134
+ valuesuffix=chart.fmt.units or "",
1135
+ node={
1136
+ "label": labels,
1137
+ "color": colors,
1138
+ "x": xs,
1139
+ "y": ys,
1140
+ "pad": 10,
1141
+ "thickness": 12,
1142
+ "line": {"width": 0},
1143
+ },
1144
+ link={
1145
+ "source": [index[(f.column, f.source)] for f in lay.flows],
1146
+ "target": [index[(f.column + 1, f.target)] for f in lay.flows],
1147
+ "value": [f.value for f in lay.flows],
1148
+ "color": [
1149
+ _color(chart.color(f.source), alpha) for f in lay.flows
1150
+ ],
1151
+ },
1152
+ )
1153
+ )
1154
+ for c, text in enumerate(lay.stage_labels):
1155
+ fig.add_annotation(
1156
+ x=c / (n_col - 1),
1157
+ y=1.06,
1158
+ xref="paper",
1159
+ yref="paper",
1160
+ text=f"<b>{text}</b>",
1161
+ showarrow=False,
1162
+ )
1163
+ return fig
1164
+
1165
+
1166
+ #: ``ScatterPlot`` label side -> Plotly ``textposition``.
1167
+ _TEXT_POSITION = {
1168
+ "above right": "top right",
1169
+ "below right": "bottom right",
1170
+ "above left": "top left",
1171
+ "below left": "bottom left",
1172
+ "right": "middle right",
1173
+ "left": "middle left",
1174
+ "above": "top center",
1175
+ "below": "bottom center",
1176
+ }
1177
+
1178
+
1179
+ def scatter_to_figure(chart: ScatterPlot) -> go.Figure:
1180
+ """Interactive ``Figure`` from a :class:`ScatterPlot`.
1181
+
1182
+ One ``go.Scatter`` trace per group (so the legend lists the groups),
1183
+ each point labelled on the side the PNG chose
1184
+ (:meth:`ScatterPlot.placements`), with the same colours and limits.
1185
+ """
1186
+ from tesorotools.artists.scatter import SCATTER_CONFIG
1187
+
1188
+ rows = chart._rows()
1189
+ order = chart.group_order(rows)
1190
+ sides = chart.placements() if chart.labels else {}
1191
+ fig = _new_figure()
1192
+ for group in order:
1193
+ points = rows[rows["group"] == group]
1194
+ color = _color(chart.color(group, order))
1195
+ fig.add_trace(
1196
+ go.Scatter(
1197
+ x=points["x"],
1198
+ y=points["y"],
1199
+ mode="markers+text" if chart.labels else "markers",
1200
+ name=chart.group_label(group),
1201
+ showlegend=bool(chart.group) and chart.legend is not None,
1202
+ text=[chart.series[c] for c in points.index],
1203
+ textposition=[
1204
+ _TEXT_POSITION[sides.get(str(c), "above right")]
1205
+ for c in points.index
1206
+ ],
1207
+ textfont={"color": color},
1208
+ marker={
1209
+ "color": color,
1210
+ "size": float(SCATTER_CONFIG.get("marker_size", 60)) ** 0.5,
1211
+ "line": {
1212
+ "color": _color(
1213
+ SCATTER_CONFIG.get("edge_color", "#333333")
1214
+ ),
1215
+ "width": float(SCATTER_CONFIG.get("edge_width", 0.8)),
1216
+ },
1217
+ },
1218
+ hovertemplate="%{text}: %{x}, %{y}<extra></extra>",
1219
+ )
1220
+ )
1221
+ fig.update_layout(
1222
+ xaxis={
1223
+ "range": list(chart.limits(rows["x"], "x")),
1224
+ "title_text": chart.xlabel,
1225
+ "ticksuffix": chart.x_fmt.units,
1226
+ "showgrid": True,
1227
+ },
1228
+ yaxis={
1229
+ "range": list(chart.limits(rows["y"], "y")),
1230
+ "title_text": chart.ylabel,
1231
+ "ticksuffix": chart.y_fmt.units,
1232
+ },
1233
+ )
1234
+ _legend_side(fig, chart.legend)
1235
+ return fig