tesorotools-python 0.1.2__tar.gz → 0.1.3__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 (135) hide show
  1. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/PKG-INFO +1 -1
  2. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/_common.py +7 -1
  3. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/bar_line.py +145 -13
  4. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/plotly_backend.py +29 -5
  5. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/.gitignore +0 -0
  6. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/pyproject.toml +0 -0
  7. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/__init__.py +0 -0
  8. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/_build_context.py +0 -0
  9. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/_registry.py +0 -0
  10. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/__init__.py +0 -0
  11. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/barh_plot.py +0 -0
  12. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/box_plot.py +0 -0
  13. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/compact.py +0 -0
  14. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/intraday_plot.py +0 -0
  15. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/line_plot.py +0 -0
  16. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/matrix.py +0 -0
  17. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/sharing.py +0 -0
  18. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/shock_plot.py +0 -0
  19. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/stacked.py +0 -0
  20. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/type_curve.py +0 -0
  21. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/vector_plot.py +0 -0
  22. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/artists/waterfall.py +0 -0
  23. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/README.md +0 -0
  24. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  25. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  26. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  27. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  28. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  29. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  30. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  31. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  32. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/fonts/README.md +0 -0
  33. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/plots.yaml +0 -0
  34. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/template.docx +0 -0
  35. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  36. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/data_sources/__init__.py +0 -0
  37. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/data_sources/debug.py +0 -0
  38. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/database/__init__.py +0 -0
  39. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/database/local.py +0 -0
  40. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/database/push.py +0 -0
  41. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/database/shared.py +0 -0
  42. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/dependencies/__init__.py +0 -0
  43. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/dependencies/node.py +0 -0
  44. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/dependencies/resolution.py +0 -0
  45. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/driver.py +0 -0
  46. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/__init__.py +0 -0
  47. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/cli.py +0 -0
  48. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/scaffold.py +0 -0
  49. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/main.tex.tmpl +0 -0
  50. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/sections/01-introduccion.tex +0 -0
  51. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/sections/02-analisis.tex +0 -0
  52. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/article/sections/03-conclusiones.tex +0 -0
  53. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/main.tex.tmpl +0 -0
  54. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/slides/01-contexto.tex +0 -0
  55. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/slides/02-analisis.tex +0 -0
  56. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/slides/03-conclusiones.tex +0 -0
  57. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/beamer/style/beamerthemeTesoro.sty +0 -0
  58. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/appendices/a-fuentes.tex +0 -0
  59. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/chapters/01-introduccion.tex +0 -0
  60. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/chapters/02-analisis.tex +0 -0
  61. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/frontmatter/resumen.tex +0 -0
  62. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/book/main.tex.tmpl +0 -0
  63. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/README.md.tmpl +0 -0
  64. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/dot-gitignore +0 -0
  65. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/dot-vscode/settings.json +0 -0
  66. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/figures/dot-gitkeep +0 -0
  67. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/fonts/README.md +0 -0
  68. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/latexmkrc +0 -0
  69. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/refs.bib +0 -0
  70. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/style/tesoro.sty +0 -0
  71. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/style/tesorobrand.sty.tmpl +0 -0
  72. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/latex/templates/common/style/tesorolocal.sty.tmpl +0 -0
  73. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/manifest.py +0 -0
  74. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/offsets/__init__.py +0 -0
  75. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/offsets/offsets.py +0 -0
  76. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/offsets/outliers.py +0 -0
  77. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/orchestration.py +0 -0
  78. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/palette.py +0 -0
  79. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/__init__.py +0 -0
  80. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/diagnose.py +0 -0
  81. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/engine.py +0 -0
  82. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/pipeline/rules.py +0 -0
  83. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/providers/__init__.py +0 -0
  84. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/providers/base.py +0 -0
  85. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/providers/bde.py +0 -0
  86. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/providers/ecb.py +0 -0
  87. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/providers/fred.py +0 -0
  88. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/providers/imf_irfcl.py +0 -0
  89. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/providers/lseg.py +0 -0
  90. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/py.typed +0 -0
  91. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/__init__.py +0 -0
  92. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/__init__.py +0 -0
  93. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/content.py +0 -0
  94. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/images.py +0 -0
  95. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/section.py +0 -0
  96. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/subtitle.py +0 -0
  97. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/table.py +0 -0
  98. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/text.py +0 -0
  99. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/content/title.py +0 -0
  100. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/document.py +0 -0
  101. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/render/report.py +0 -0
  102. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/scaffold.py +0 -0
  103. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/testing/__init__.py +0 -0
  104. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/testing/compare.py +0 -0
  105. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/__init__.py +0 -0
  106. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/config.py +0 -0
  107. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/format.py +0 -0
  108. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/globals.py +0 -0
  109. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/matplotlib.py +0 -0
  110. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/series.py +0 -0
  111. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/shortcuts.py +0 -0
  112. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/utils/template.py +0 -0
  113. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/__init__.py +0 -0
  114. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/app.py +0 -0
  115. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/brand.py +0 -0
  116. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/cli.py +0 -0
  117. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/components.py +0 -0
  118. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/formatting.py +0 -0
  119. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/launcher.py +0 -0
  120. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/palette.py +0 -0
  121. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/plotting.py +0 -0
  122. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/scaffold.py +0 -0
  123. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/Dockerfile.tmpl +0 -0
  124. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/README.md.tmpl +0 -0
  125. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/dot-dockerignore.tmpl +0 -0
  126. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/dot-gitignore.tmpl +0 -0
  127. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/dot-streamlit/config.toml.tmpl +0 -0
  128. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/pyproject.toml.tmpl +0 -0
  129. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/__init__.py.tmpl +0 -0
  130. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/cli.py.tmpl +0 -0
  131. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/main.py.tmpl +0 -0
  132. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/src/$package/sections.py.tmpl +0 -0
  133. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/tests/__init__.py.tmpl +0 -0
  134. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/templates/tests/test_app.py.tmpl +0 -0
  135. {tesorotools_python-0.1.2 → tesorotools_python-0.1.3}/src/tesorotools/web/theme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tesorotools-python
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Requires-Python: >=3.13
5
5
  Requires-Dist: babel>=2.17
6
6
  Requires-Dist: matplotlib>=3.10
@@ -1174,6 +1174,7 @@ def place_legend(
1174
1174
  compact: bool,
1175
1175
  ncol: int | None = None,
1176
1176
  position: str = "auto",
1177
+ entries: tuple[list[Any], list[str]] | None = None,
1177
1178
  ) -> MplLegend | None:
1178
1179
  """Place the figure legend, compact-aware, from the labelled artists.
1179
1180
 
@@ -1184,9 +1185,14 @@ def place_legend(
1184
1185
  ``rcParams`` metrics. ``position`` (``"above"`` / ``"below"`` /
1185
1186
  ``"auto"``) chooses the side through :func:`resolve_legend_position`, so
1186
1187
  a compact chart can carry its legend under the plot instead of over it.
1188
+ ``entries`` passes precomputed ``(handles, labels)`` instead of reading
1189
+ them off *ax* -- for charts whose series span several axes (a
1190
+ ``twinx`` secondary y-axis), in the order the legend should list them.
1187
1191
  Returns ``None`` (drawing nothing) when there are no labels.
1188
1192
  """
1189
- handles, labels = ax.get_legend_handles_labels()
1193
+ handles, labels = (
1194
+ entries if entries is not None else ax.get_legend_handles_labels()
1195
+ )
1190
1196
  if not labels:
1191
1197
  return None
1192
1198
  side = resolve_legend_position(position, compact=compact)
@@ -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
@@ -63,7 +67,7 @@ BAR_LINE_CONFIG: dict[str, Any] = PLOT_CONFIG["bar_line"]
63
67
 
64
68
 
65
69
  class BarLinePlot:
66
- """Combo chart: vertical bars and marked lines on one shared axis.
70
+ """Combo chart: vertical bars and marked lines, optionally on two axes.
67
71
 
68
72
  Parameters mirror the other time-series artists where they overlap
69
73
  (``out_path``, ``data`` / ``data_path``, ``scale``, ``start_date`` /
@@ -87,6 +91,21 @@ class BarLinePlot:
87
91
  ``date_format`` is applied via ``strftime`` (lower-cased, e.g.
88
92
  ``"dic-21"``) to a DatetimeIndex; ``max_xticks`` thins the labels
89
93
  (``None`` labels every slot).
94
+ * ``secondary`` -- ids (from ``bars`` and/or ``lines``) drawn against a
95
+ second y-axis on the right, with its own scale. Empty (the default)
96
+ keeps the single shared axis, ticked on the house side (right). With
97
+ a secondary axis the primary one moves to the left, its grid is the
98
+ only one drawn, and the legend still lists every series in declared
99
+ order (bars, then lines). It cannot hold every series: the primary
100
+ axis would be left empty.
101
+ * ``secondary_fmt`` -- tick format (``!format``) of the secondary axis;
102
+ defaults to ``fmt``.
103
+ * ``ylabel`` / ``secondary_ylabel`` -- y-axis titles (empty: none).
104
+ ``ylabel`` sits on the primary axis' tick side.
105
+
106
+ ``baseline`` draws the 0 line on every axis that holds bars (they grow
107
+ from it) or, with no bars at all, on the primary axis, so a lines-only
108
+ axis is never stretched down to 0.
90
109
  """
91
110
 
92
111
  #: Keys that change the PNG but never the ``to_plotly()`` figure
@@ -102,6 +121,7 @@ class BarLinePlot:
102
121
  "legend.ncol",
103
122
  "max_xticks",
104
123
  "plot_size",
124
+ "secondary_fmt.decimals",
105
125
  "x_rotation",
106
126
  }
107
127
  )
@@ -130,6 +150,10 @@ class BarLinePlot:
130
150
  max_xticks: int | None = None,
131
151
  compact: bool = False,
132
152
  font_pt: float | None = None,
153
+ secondary: list[str] | None = None,
154
+ secondary_fmt: Format | None = None,
155
+ ylabel: str = "",
156
+ secondary_ylabel: str = "",
133
157
  ) -> None:
134
158
  if out_path.suffix != ".png":
135
159
  raise ValueError(f"out_path must be .png: {out_path}")
@@ -139,6 +163,17 @@ class BarLinePlot:
139
163
  raise ValueError(
140
164
  "BarLinePlot needs at least one series in `bars` or `lines`"
141
165
  )
166
+ self.secondary = list(secondary or [])
167
+ unknown = [c for c in self.secondary if c not in self.bars | self.lines]
168
+ if unknown:
169
+ raise ValueError(
170
+ f"`secondary` ids must be in `bars` or `lines`: {unknown}"
171
+ )
172
+ if set(self.secondary) >= {*self.bars, *self.lines}:
173
+ raise ValueError(
174
+ "`secondary` cannot hold every series: the primary axis "
175
+ "would be empty"
176
+ )
142
177
  self.out_path = out_path
143
178
  self.data = resolve_data(data, data_path)
144
179
  self.scale = scale
@@ -157,6 +192,9 @@ class BarLinePlot:
157
192
  self.max_xticks = max_xticks
158
193
  self.compact = compact
159
194
  self.font_pt = font_pt
195
+ self.secondary_fmt = secondary_fmt or self.fmt
196
+ self.ylabel = ylabel
197
+ self.secondary_ylabel = secondary_ylabel
160
198
 
161
199
  @classmethod
162
200
  def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
@@ -179,7 +217,8 @@ class BarLinePlot:
179
217
  ``fmt`` (``!format``), ``legend`` (``!legend``), ``figsize``,
180
218
  ``plot_size``, ``bar_styles`` / ``line_styles``, ``bar_width``,
181
219
  ``date_format``, ``x_rotation``, ``max_xticks``, ``compact``,
182
- ``font_pt``.
220
+ ``font_pt``, ``secondary``, ``secondary_fmt`` (``!format``),
221
+ ``ylabel`` / ``secondary_ylabel``.
183
222
 
184
223
  Example
185
224
  -------
@@ -194,6 +233,26 @@ class BarLinePlot:
194
233
  surprise: "Sorpresa (pp)"
195
234
  fmt: !format {decimals: 0}
196
235
  legend: !legend {}
236
+
237
+ With scales too far apart for one axis, send some series to the
238
+ right-hand axis:
239
+
240
+ .. code-block:: yaml
241
+
242
+ eurusd_fair_value: !bar_line
243
+ out_path: out/eurusd_fair_value.png
244
+ data_path: data/eurusd.feather
245
+ bars:
246
+ r2: "R^2"
247
+ lines:
248
+ eurusd: "EURUSD"
249
+ fair_value: "Modelo valor justo (60 días)"
250
+ secondary: [r2]
251
+ fmt: !format {decimals: 2}
252
+ secondary_fmt: !format {decimals: 1}
253
+ ylabel: "EURUSD"
254
+ secondary_ylabel: "Coeficiente de determinación (R^2)"
255
+ legend: !legend {}
197
256
  """
198
257
  cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
199
258
  node, deep=True
@@ -287,9 +346,13 @@ class BarLinePlot:
287
346
  **compact_figure_kwargs(self.compact, self.figsize)
288
347
  )
289
348
  ax: Axes = fig.add_subplot()
349
+ ax2: Axes | None = ax.twinx() if self.secondary else None
350
+
351
+ def target(col: str) -> Axes:
352
+ return ax2 if ax2 is not None and col in self.secondary else ax
290
353
 
291
354
  # Bars first (behind the lines). Several bar series share each slot,
292
- # dodged side by side; a single series is centred.
355
+ # dodged side by side (across both axes); a single one is centred.
293
356
  n_bars = len(self.bars)
294
357
  each = self.bar_width / n_bars if n_bars else self.bar_width
295
358
  default_alpha: float = float(BAR_LINE_CONFIG["bar"].get("alpha", 0.9))
@@ -299,7 +362,7 @@ class BarLinePlot:
299
362
  style.setdefault("alpha", default_alpha)
300
363
  offset = (j - (n_bars - 1) / 2) * each
301
364
  values = plot_data[col].fillna(0).to_numpy(dtype=np.float64)
302
- ax.bar( # type: ignore[reportUnknownMemberType]
365
+ target(col).bar( # type: ignore[reportUnknownMemberType]
303
366
  x + offset,
304
367
  values,
305
368
  width=each,
@@ -317,7 +380,7 @@ class BarLinePlot:
317
380
  style.setdefault("marker", line_cfg.get("marker", "o"))
318
381
  style.setdefault("linewidth", line_cfg.get("linewidth", 2))
319
382
  style.setdefault("markersize", line_cfg.get("markersize", 5))
320
- ax.plot( # type: ignore[reportUnknownMemberType]
383
+ target(col).plot( # type: ignore[reportUnknownMemberType]
321
384
  x,
322
385
  plot_data[col].to_numpy(dtype=np.float64),
323
386
  label=label,
@@ -336,14 +399,31 @@ class BarLinePlot:
336
399
  # style_spines enables the y grid only (axis="y"), which is what these
337
400
  # charts read against; the categorical x-axis stays gridless.
338
401
  if self.baseline:
339
- style_baseline(ax, 0, **AX_CONFIG["baseline"])
340
-
402
+ on_primary, on_secondary = self._baseline_on()
403
+ for axis, on in ((ax, on_primary), (ax2, on_secondary)):
404
+ if on and axis is not None:
405
+ style_baseline(axis, 0, **AX_CONFIG["baseline"])
406
+
407
+ side = "right"
408
+ if ax2 is not None:
409
+ side = "left"
410
+ ax.yaxis.tick_left()
411
+ self._style_secondary(ax, ax2, font_pt)
341
412
  if self.compact:
342
- style_compact_axes(ax, font_pt, side="right")
413
+ style_compact_axes(ax, font_pt, side=side)
414
+ if self.ylabel:
415
+ ax.yaxis.set_label_position(side) # type: ignore[reportArgumentType]
416
+ ax.set_ylabel( # type: ignore[reportUnknownMemberType]
417
+ self.ylabel, fontsize=font_pt
418
+ )
343
419
 
344
420
  if self.legend is not None:
345
421
  place_legend(
346
- fig, ax, compact=self.compact, **legend_layout(self.legend)
422
+ fig,
423
+ ax,
424
+ compact=self.compact,
425
+ entries=self._legend_entries(ax, ax2),
426
+ **legend_layout(self.legend),
347
427
  )
348
428
 
349
429
  if self.plot_size is not None and not self.compact:
@@ -351,6 +431,58 @@ class BarLinePlot:
351
431
 
352
432
  return fig, ax
353
433
 
434
+ def _baseline_on(self) -> tuple[bool, bool]:
435
+ """``(primary, secondary)``: the axes that get the 0 line.
436
+
437
+ Those holding bars (they grow from 0) or, with no bars at all, the
438
+ primary one. Shared by :meth:`build` and :meth:`to_plotly`.
439
+ """
440
+ on_secondary = [c in self.secondary for c in self.bars]
441
+ secondary = any(on_secondary)
442
+ primary = not all(on_secondary) or not secondary
443
+ return primary, secondary
444
+
445
+ def _style_secondary(
446
+ self, ax: Axes, ax2: Axes, font_pt: float | None
447
+ ) -> None:
448
+ """Style the right-hand axis: own format, no grid, title, z-order.
449
+
450
+ The twin is drawn above the primary by default, which would hide
451
+ the primary series behind secondary bars; when the secondary holds
452
+ bars the primary is brought forward with a transparent background.
453
+ """
454
+ style_spines(
455
+ ax2,
456
+ decimals=self.secondary_fmt.decimals,
457
+ units=self.secondary_fmt.units,
458
+ **AX_CONFIG["spines"],
459
+ )
460
+ ax2.grid(False) # type: ignore[reportUnknownMemberType]
461
+ if self.compact:
462
+ style_compact_axes(ax2, font_pt, side="right")
463
+ if self.secondary_ylabel:
464
+ ax2.set_ylabel( # type: ignore[reportUnknownMemberType]
465
+ self.secondary_ylabel, fontsize=font_pt
466
+ )
467
+ if any(c in self.secondary for c in self.bars):
468
+ ax.set_zorder(ax2.get_zorder() + 1)
469
+ ax.patch.set_visible(False)
470
+
471
+ def _legend_entries(
472
+ self, ax: Axes, ax2: Axes | None
473
+ ) -> tuple[list[Any], list[str]]:
474
+ """Handles and labels of both axes, in declared order."""
475
+ handles, labels = ax.get_legend_handles_labels()
476
+ if ax2 is not None:
477
+ more_handles, more_labels = ax2.get_legend_handles_labels()
478
+ handles, labels = handles + more_handles, labels + more_labels
479
+ order = [*self.bars.values(), *self.lines.values()]
480
+ pairs = sorted(
481
+ zip(handles, labels, strict=True),
482
+ key=lambda pair: order.index(pair[1]),
483
+ )
484
+ return [h for h, _ in pairs], [label for _, label in pairs]
485
+
354
486
  def save(
355
487
  self,
356
488
  fig: Figure,
@@ -197,10 +197,11 @@ def _legend_side(fig: go.Figure, legend: Legend | None) -> None:
197
197
  )
198
198
 
199
199
 
200
- def _baseline(fig: go.Figure, reference: float) -> None:
201
- """Draw the house baseline (axhline) at *reference*."""
200
+ def _baseline(fig: go.Figure, reference: float, *, yref: str = "y") -> None:
201
+ """Draw the house baseline (axhline) at *reference* on axis *yref*."""
202
202
  fig.add_hline(
203
203
  y=reference,
204
+ yref=yref,
204
205
  line_color=AX_CONFIG["baseline"]["color"],
205
206
  line_width=AX_CONFIG["baseline"]["linewidth"],
206
207
  )
@@ -503,7 +504,8 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
503
504
  """Interactive ``Figure`` from a :class:`BarLinePlot`.
504
505
 
505
506
  Bars (grouped) and marked lines share one y-axis over a categorical
506
- x-axis of the same date-formatted labels the matplotlib artist uses.
507
+ x-axis of the same date-formatted labels the matplotlib artist uses;
508
+ the ``secondary`` series go to an overlaid right-hand ``yaxis2``.
507
509
  """
508
510
  plot_data = chart._prepare_data()
509
511
  palette = _house_palette()
@@ -515,6 +517,9 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
515
517
  default_alpha = float(BAR_LINE_CONFIG["bar"].get("alpha", 0.9))
516
518
  line_cfg = BAR_LINE_CONFIG["line"]
517
519
 
520
+ def yaxis(col: str) -> str:
521
+ return "y2" if col in chart.secondary else "y"
522
+
518
523
  for j, (col, label) in enumerate(chart.bars.items()):
519
524
  style = chart.bar_styles.get(col, {})
520
525
  color = (
@@ -525,6 +530,7 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
525
530
  x=x,
526
531
  y=plot_data[col].fillna(0),
527
532
  name=label,
533
+ yaxis=yaxis(col),
528
534
  marker_color=_color(color, style.get("alpha", default_alpha)),
529
535
  )
530
536
  )
@@ -541,6 +547,7 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
541
547
  y=plot_data[col],
542
548
  mode="lines+markers",
543
549
  name=label,
550
+ yaxis=yaxis(col),
544
551
  line={
545
552
  "color": _color(color),
546
553
  "width": style.get(
@@ -552,10 +559,27 @@ def bar_line_to_figure(chart: BarLinePlot) -> go.Figure:
552
559
  )
553
560
 
554
561
  fig.update_layout(barmode="group")
562
+ if chart.ylabel:
563
+ fig.update_layout(yaxis_title_text=chart.ylabel)
564
+ if chart.secondary:
565
+ fig.update_layout(
566
+ yaxis_side="left",
567
+ yaxis2={
568
+ "overlaying": "y",
569
+ "side": "right",
570
+ "showgrid": False,
571
+ "ticksuffix": chart.secondary_fmt.units,
572
+ "title_text": chart.secondary_ylabel,
573
+ },
574
+ )
555
575
  if chart.baseline:
556
- _baseline(fig, 0)
576
+ on_primary, on_secondary = chart._baseline_on()
577
+ if on_primary:
578
+ _baseline(fig, 0)
579
+ if on_secondary:
580
+ _baseline(fig, 0, yref="y2")
557
581
  if chart.fmt.units:
558
- fig.update_yaxes(ticksuffix=chart.fmt.units)
582
+ fig.update_layout(yaxis_ticksuffix=chart.fmt.units)
559
583
  _legend_side(fig, chart.legend)
560
584
  return fig
561
585