tesorotools-python 0.0.53__tar.gz → 0.0.55__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/.gitignore +3 -1
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/PKG-INFO +4 -2
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/pyproject.toml +1 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/__init__.py +17 -3
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/_common.py +242 -66
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/bar_line.py +4 -2
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/barh_plot.py +7 -5
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/box_plot.py +15 -5
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/compact.py +18 -2
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/intraday_plot.py +24 -3
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/line_plot.py +64 -21
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/plotly_backend.py +26 -1
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/shock_plot.py +4 -1
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/stacked.py +44 -8
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/type_curve.py +12 -4
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/plots.yaml +75 -3
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/template.docx +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/providers/__init__.py +11 -5
- tesorotools_python-0.0.55/src/tesorotools/providers/fred.py +375 -0
- tesorotools_python-0.0.55/src/tesorotools/render/content/table.py +1061 -0
- tesorotools_python-0.0.53/src/tesorotools/render/content/table.py +0 -505
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/matrix.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/vector_plot.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/waterfall.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/providers/lseg.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/document.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/utils/template.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: tesorotools-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.55
|
|
4
4
|
Requires-Python: >=3.13
|
|
5
5
|
Requires-Dist: babel>=2.17
|
|
6
6
|
Requires-Dist: matplotlib>=3.10
|
|
@@ -16,6 +16,8 @@ Provides-Extra: bde
|
|
|
16
16
|
Requires-Dist: requests>=2.31; extra == 'bde'
|
|
17
17
|
Provides-Extra: ecb
|
|
18
18
|
Requires-Dist: requests>=2.31; extra == 'ecb'
|
|
19
|
+
Provides-Extra: fred
|
|
20
|
+
Requires-Dist: requests>=2.31; extra == 'fred'
|
|
19
21
|
Provides-Extra: imf
|
|
20
22
|
Requires-Dist: requests>=2.31; extra == 'imf'
|
|
21
23
|
Requires-Dist: truststore>=0.10; extra == 'imf'
|
|
@@ -7,9 +7,11 @@ effects) and registers their YAML tags via
|
|
|
7
7
|
|
|
8
8
|
Provider subclasses gated by optional extras
|
|
9
9
|
(``BdeProvider`` requires ``[bde]``, ``EcbProvider``
|
|
10
|
-
requires ``[ecb]``, ``
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
requires ``[ecb]``, ``FredProvider`` requires ``[fred]``,
|
|
11
|
+
``ImfIrfclProvider`` requires ``[imf]``, ``LSEGProvider``
|
|
12
|
+
requires ``[lseg]``) are exposed lazily through
|
|
13
|
+
``__getattr__``; importing this module does not require the
|
|
14
|
+
extras to be installed.
|
|
13
15
|
|
|
14
16
|
Third parties extend the package via ``register_artist``,
|
|
15
17
|
``register_tag``, and ``register_provider`` (and their
|
|
@@ -31,6 +33,8 @@ except PackageNotFoundError: # pragma: no cover - source-only checkout
|
|
|
31
33
|
if TYPE_CHECKING:
|
|
32
34
|
from tesorotools.providers.bde import BdeProvider
|
|
33
35
|
from tesorotools.providers.ecb import EcbProvider
|
|
36
|
+
from tesorotools.providers.fred import FredProvider
|
|
37
|
+
from tesorotools.providers.imf_irfcl import ImfIrfclProvider
|
|
34
38
|
from tesorotools.providers.lseg import LSEGProvider
|
|
35
39
|
|
|
36
40
|
from tesorotools._build_context import BuildContext
|
|
@@ -131,10 +135,12 @@ __all__ = [
|
|
|
131
135
|
"DataProvider",
|
|
132
136
|
"EcbProvider",
|
|
133
137
|
"Format",
|
|
138
|
+
"FredProvider",
|
|
134
139
|
"GroupedBarChart",
|
|
135
140
|
"HorizontalBarChart",
|
|
136
141
|
"Image",
|
|
137
142
|
"Images",
|
|
143
|
+
"ImfIrfclProvider",
|
|
138
144
|
"IntradayPlot",
|
|
139
145
|
"Legend",
|
|
140
146
|
"LinePlot",
|
|
@@ -183,6 +189,14 @@ def __getattr__(name: str) -> Any:
|
|
|
183
189
|
from tesorotools.providers.ecb import EcbProvider
|
|
184
190
|
|
|
185
191
|
return EcbProvider
|
|
192
|
+
if name == "FredProvider":
|
|
193
|
+
from tesorotools.providers.fred import FredProvider
|
|
194
|
+
|
|
195
|
+
return FredProvider
|
|
196
|
+
if name == "ImfIrfclProvider":
|
|
197
|
+
from tesorotools.providers.imf_irfcl import ImfIrfclProvider
|
|
198
|
+
|
|
199
|
+
return ImfIrfclProvider
|
|
186
200
|
if name == "LSEGProvider":
|
|
187
201
|
from tesorotools.providers.lseg import LSEGProvider
|
|
188
202
|
|
|
@@ -68,6 +68,11 @@ CM_TO_IN: float = 1.0 / 2.54
|
|
|
68
68
|
# is missing a level; "default" reproduces the historical tick count of each mode
|
|
69
69
|
# (4 for compact, 6 for non-compact).
|
|
70
70
|
DATE_DENSITY_LEVELS: tuple[str, ...] = ("few", "default", "many")
|
|
71
|
+
|
|
72
|
+
# Legend bands a chart may ask for. ``auto`` defers to the mode default:
|
|
73
|
+
# ``compact.legend_position`` in ``plots.yaml`` for a compact chart (shipped
|
|
74
|
+
# as "above"), ``below`` for a full-size one.
|
|
75
|
+
LEGEND_POSITIONS: tuple[str, ...] = ("auto", "above", "below")
|
|
71
76
|
_DATE_MAXTICKS_FALLBACK_COMPACT: dict[str, int] = {
|
|
72
77
|
"few": 3,
|
|
73
78
|
"default": 4,
|
|
@@ -229,28 +234,45 @@ class Format:
|
|
|
229
234
|
class Legend:
|
|
230
235
|
"""Legend layout knobs carried in YAML.
|
|
231
236
|
|
|
232
|
-
|
|
233
|
-
|
|
237
|
+
``ncol`` absent (None) means "let the artist auto-fit
|
|
238
|
+
using ``auto_ncol``". ``position`` picks the band the
|
|
239
|
+
legend lives in: ``"above"`` the plot or ``"below"`` it;
|
|
240
|
+
``"auto"`` (the default) keeps each mode's house default,
|
|
241
|
+
which for compact charts is read from
|
|
242
|
+
``compact.legend_position`` in ``plots.yaml``. Both
|
|
243
|
+
values work in compact and in full-size charts, so a
|
|
244
|
+
compact chart in a Word table can carry its labels under
|
|
245
|
+
the plot when the text above it already reads as a title.
|
|
234
246
|
"""
|
|
235
247
|
|
|
236
|
-
def __init__(self, ncol: int | None = None) -> None:
|
|
248
|
+
def __init__(self, ncol: int | None = None, position: str = "auto") -> None:
|
|
249
|
+
if position not in LEGEND_POSITIONS:
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"legend position must be one of {LEGEND_POSITIONS}: "
|
|
252
|
+
f"{position!r}"
|
|
253
|
+
)
|
|
237
254
|
self.ncol = ncol
|
|
255
|
+
self.position = position
|
|
238
256
|
|
|
239
257
|
@classmethod
|
|
240
258
|
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
241
259
|
"""Build a :class:`Legend` from a ``!legend`` YAML tag.
|
|
242
260
|
|
|
243
|
-
|
|
244
|
-
``null``) lets the artist auto-fit using
|
|
245
|
-
:func:`auto_ncol
|
|
246
|
-
|
|
261
|
+
Both keys are optional. Omitting ``ncol`` (or
|
|
262
|
+
passing ``null``) lets the artist auto-fit using
|
|
263
|
+
:func:`auto_ncol`; omitting ``position`` keeps the
|
|
264
|
+
mode default (``compact.legend_position`` for a
|
|
265
|
+
compact chart, below the plot for a full-size one).
|
|
266
|
+
An empty ``!legend {}`` enables the legend with
|
|
267
|
+
auto-fitted columns in its default band.
|
|
247
268
|
|
|
248
269
|
Example
|
|
249
270
|
-------
|
|
250
271
|
.. code-block:: yaml
|
|
251
272
|
|
|
252
|
-
legend: !legend {ncol: 3}
|
|
253
|
-
legend: !legend {}
|
|
273
|
+
legend: !legend {ncol: 3} # explicit
|
|
274
|
+
legend: !legend {position: below} # under the plot
|
|
275
|
+
legend: !legend {} # auto-fit
|
|
254
276
|
"""
|
|
255
277
|
legend_cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
|
|
256
278
|
node, deep=True
|
|
@@ -526,6 +548,65 @@ def compact_legend_max_ncol() -> int | None:
|
|
|
526
548
|
return None if raw is None else int(raw)
|
|
527
549
|
|
|
528
550
|
|
|
551
|
+
def date_show_start_default(*, compact: bool) -> bool:
|
|
552
|
+
"""House default for pinning the first datum as the leading x tick.
|
|
553
|
+
|
|
554
|
+
Reads ``compact.date_show_start`` / ``line.date_show_start`` from
|
|
555
|
+
``plots.yaml`` (both shipped ``true``), so "does the chart say when the
|
|
556
|
+
series starts?" stays a data decision. A per-chart
|
|
557
|
+
``date_show_start: false`` overrides it.
|
|
558
|
+
"""
|
|
559
|
+
cfg = COMPACT_CONFIG if compact else LINE_CONFIG
|
|
560
|
+
return bool(cfg.get("date_show_start", True))
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def date_span_ticks(
|
|
564
|
+
data_min: Any,
|
|
565
|
+
data_max: Any,
|
|
566
|
+
*,
|
|
567
|
+
maxticks: int,
|
|
568
|
+
pin_start: bool,
|
|
569
|
+
) -> list[float]:
|
|
570
|
+
"""Date ticks (as date numbers) computed over the **data span**.
|
|
571
|
+
|
|
572
|
+
``AutoDateLocator`` candidates clipped to ``[data_min, data_max]``, so no
|
|
573
|
+
tick floats outside the plotted data: off-span ticks are what leave the
|
|
574
|
+
blank labels at both ends of pandas' own date axis, and in compact (where
|
|
575
|
+
``margins(x=0)``) they get clipped by the figure edge.
|
|
576
|
+
|
|
577
|
+
With ``pin_start`` the first datum becomes the leading tick -- the only
|
|
578
|
+
way the reader can tell *when the series starts*, since the natural
|
|
579
|
+
calendar tick usually falls before it -- and any candidate closer than
|
|
580
|
+
10 % of the span is dropped so the two labels do not collide. Without
|
|
581
|
+
it the plain in-span candidates are returned (falling back to the two
|
|
582
|
+
endpoints when the locator offers nothing inside).
|
|
583
|
+
"""
|
|
584
|
+
lo: float = float(cast(Any, date2num(data_min)))
|
|
585
|
+
hi: float = float(cast(Any, date2num(data_max)))
|
|
586
|
+
locator = AutoDateLocator(minticks=2, maxticks=maxticks)
|
|
587
|
+
candidates: list[float] = cast(
|
|
588
|
+
"list[float]", locator.tick_values(data_min, data_max)
|
|
589
|
+
)
|
|
590
|
+
if not pin_start:
|
|
591
|
+
return [t for t in candidates if lo <= t <= hi] or [lo, hi]
|
|
592
|
+
min_gap: float = (hi - lo) * 0.10
|
|
593
|
+
interior: list[float] = [
|
|
594
|
+
t for t in candidates if lo < t <= hi and t - lo >= min_gap
|
|
595
|
+
]
|
|
596
|
+
return [lo, *interior]
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def compact_legend_position() -> str:
|
|
600
|
+
"""Default legend band for compact charts (``compact.legend_position``).
|
|
601
|
+
|
|
602
|
+
``"above"`` unless ``plots.yaml`` says otherwise: on a 7.4 cm figure the
|
|
603
|
+
bottom edge already carries the date ticks, so the top band is the
|
|
604
|
+
cheaper place to spend vertical space. A per-chart
|
|
605
|
+
``legend: !legend {position: below}`` always wins over this default.
|
|
606
|
+
"""
|
|
607
|
+
return str(COMPACT_CONFIG.get("legend_position", "above"))
|
|
608
|
+
|
|
609
|
+
|
|
529
610
|
def compact_headroom_pad_pt() -> float:
|
|
530
611
|
"""Points of clearance to leave above the top value label (0 if unset)."""
|
|
531
612
|
return float(COMPACT_CONFIG.get("annotate_headroom_pad_pt", 0))
|
|
@@ -633,27 +714,16 @@ def compact_date_axis(
|
|
|
633
714
|
given (an explicit angle wins).
|
|
634
715
|
"""
|
|
635
716
|
lo: float = float(cast(Any, date2num(data_min)))
|
|
636
|
-
hi: float = float(cast(Any, date2num(data_max)))
|
|
637
|
-
locator = AutoDateLocator(minticks=2, maxticks=maxticks)
|
|
638
|
-
candidates: list[float] = cast(
|
|
639
|
-
"list[float]", locator.tick_values(data_min, data_max)
|
|
640
|
-
)
|
|
641
717
|
show_start: bool = (
|
|
642
|
-
|
|
718
|
+
date_show_start_default(compact=True)
|
|
643
719
|
if pin_start is None
|
|
644
720
|
else pin_start
|
|
645
721
|
)
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
# Drop any auto tick that would crowd the start label.
|
|
652
|
-
min_gap: float = (hi - lo) * 0.10
|
|
653
|
-
interior = [t for t in interior if t - lo >= min_gap]
|
|
654
|
-
ticks: list[float] = [lo, *interior]
|
|
655
|
-
else:
|
|
656
|
-
ticks = [t for t in candidates if lo <= t <= hi] or [lo, hi]
|
|
722
|
+
# same span-clipped ticks (and same pinned start) the non-compact axis
|
|
723
|
+
# uses, so both modes answer "when does this start?" identically
|
|
724
|
+
ticks: list[float] = date_span_ticks(
|
|
725
|
+
data_min, data_max, maxticks=maxticks, pin_start=show_start
|
|
726
|
+
)
|
|
657
727
|
|
|
658
728
|
def _set_ticks(values: list[float]) -> None:
|
|
659
729
|
loc = FixedLocator(values)
|
|
@@ -751,6 +821,9 @@ def apply_date_axis(
|
|
|
751
821
|
*,
|
|
752
822
|
maxticks: int,
|
|
753
823
|
rotation: float | None = None,
|
|
824
|
+
data_min: Any = None,
|
|
825
|
+
data_max: Any = None,
|
|
826
|
+
pin_start: bool = False,
|
|
754
827
|
) -> None:
|
|
755
828
|
"""Native matplotlib date x-axis with a soft tick count and optional slant.
|
|
756
829
|
|
|
@@ -761,17 +834,35 @@ def apply_date_axis(
|
|
|
761
834
|
instance (the concise context logic misfires with a mismatched one).
|
|
762
835
|
``rotation`` (degrees) tilts the labels; ``None`` keeps them horizontal.
|
|
763
836
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
837
|
+
``pin_start`` (with the ``data_min`` / ``data_max`` span) switches to the
|
|
838
|
+
span-clipped :func:`date_span_ticks` with the first datum pinned as the
|
|
839
|
+
leading tick and :class:`_StartLabeledConcise` to spell that date out --
|
|
840
|
+
the same treatment the compact axis gives it. Pandas' own date axis
|
|
841
|
+
cannot do this: its formatter only labels the positions its own locator
|
|
842
|
+
produced, so a pinned tick comes out blank; that is why pinning the start
|
|
843
|
+
means taking the axis over here.
|
|
844
|
+
|
|
845
|
+
Taking over changes the label **text style** too (concise hierarchical
|
|
846
|
+
labels instead of pandas' own date formatting).
|
|
847
|
+
|
|
848
|
+
The tilt is always set explicitly, never inherited: pandas leaves the
|
|
849
|
+
x labels rotated 30 degrees on the ``x_compat`` path this axis needs, so
|
|
850
|
+
``rotation=None`` has to *actively* flatten them to keep the documented
|
|
851
|
+
"horizontal unless asked" behaviour.
|
|
767
852
|
"""
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
853
|
+
if pin_start and data_min is not None and data_max is not None:
|
|
854
|
+
ticks = date_span_ticks(
|
|
855
|
+
data_min, data_max, maxticks=maxticks, pin_start=True
|
|
856
|
+
)
|
|
857
|
+
fixed = FixedLocator(ticks)
|
|
858
|
+
ax.xaxis.set_major_locator(fixed)
|
|
859
|
+
ax.xaxis.set_major_formatter(_StartLabeledConcise(fixed, ticks[0]))
|
|
860
|
+
else:
|
|
861
|
+
locator = AutoDateLocator(minticks=2, maxticks=maxticks)
|
|
862
|
+
ax.xaxis.set_major_locator(locator)
|
|
863
|
+
ax.xaxis.set_major_formatter(ConciseDateFormatter(locator))
|
|
864
|
+
rot = 0.0 if rotation is None else float(rotation)
|
|
865
|
+
ha = _date_label_ha(None if rot == 0 else rot)
|
|
775
866
|
ax.tick_params( # type: ignore[reportUnknownMemberType]
|
|
776
867
|
axis="x", rotation=rot
|
|
777
868
|
)
|
|
@@ -780,7 +871,8 @@ def apply_date_axis(
|
|
|
780
871
|
fig.canvas.draw() # type: ignore[reportUnknownMemberType]
|
|
781
872
|
for label in ax.get_xticklabels():
|
|
782
873
|
label.set_horizontalalignment(ha)
|
|
783
|
-
|
|
874
|
+
if rot != 0:
|
|
875
|
+
label.set_rotation_mode("anchor")
|
|
784
876
|
|
|
785
877
|
|
|
786
878
|
def compact_y_axis(ax: Axes) -> None:
|
|
@@ -802,42 +894,100 @@ def compact_y_axis(ax: Axes) -> None:
|
|
|
802
894
|
)
|
|
803
895
|
|
|
804
896
|
|
|
805
|
-
def
|
|
897
|
+
def resolve_legend_position(position: str, *, compact: bool) -> str:
|
|
898
|
+
"""Resolve a :class:`Legend` ``position`` to the band to draw in.
|
|
899
|
+
|
|
900
|
+
``"above"`` / ``"below"`` are honoured as given, in **both** modes.
|
|
901
|
+
``"auto"`` keeps the house default of each mode: the compact band read
|
|
902
|
+
from ``compact.legend_position`` (see :func:`compact_legend_position`)
|
|
903
|
+
and ``"below"`` for a full-size chart, which is where every non-compact
|
|
904
|
+
chart has always put it. Returns ``"above"`` or ``"below"``.
|
|
905
|
+
"""
|
|
906
|
+
if position != "auto":
|
|
907
|
+
return position
|
|
908
|
+
return compact_legend_position() if compact else "below"
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def legend_band(
|
|
806
912
|
ax: Axes,
|
|
807
913
|
handles: list[Any],
|
|
808
914
|
labels: list[str],
|
|
809
915
|
*,
|
|
810
916
|
ncol: int,
|
|
917
|
+
position: str = "above",
|
|
811
918
|
fontsize: float | None = None,
|
|
812
919
|
handlelength: float | None = None,
|
|
813
920
|
handletextpad: float | None = None,
|
|
814
921
|
columnspacing: float | None = None,
|
|
922
|
+
frameon: bool | None = None,
|
|
815
923
|
) -> MplLegend:
|
|
816
|
-
"""Place the legend in a band **
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
924
|
+
"""Place the legend in a band **outside the axes, spanning the figure**.
|
|
925
|
+
|
|
926
|
+
``position`` picks the edge: ``"above"`` (``loc="outside upper
|
|
927
|
+
center"``) or ``"below"`` (``loc="outside lower center"``). Either way
|
|
928
|
+
constrained layout reserves a full-width strip on that side of the
|
|
929
|
+
*figure*, so the legend may use the whole figure width -- wider than the
|
|
930
|
+
narrow compact axes, whose left edge is eaten by the y-tick numbers and
|
|
931
|
+
whose right edge is eaten by the end-of-series value labels. Spanning
|
|
932
|
+
the figure lets the maximum number of columns share one row, which
|
|
933
|
+
**minimises the vertical space the legend steals from the plot** (fewer
|
|
934
|
+
rows = taller chart), whichever side it sits on.
|
|
935
|
+
|
|
936
|
+
Above is the compact default (the bottom edge already spends space on
|
|
937
|
+
the date ticks); below is the classic report look and the default of the
|
|
938
|
+
full-size charts. ``fontsize`` / ``handlelength`` / ``handletextpad`` /
|
|
939
|
+
``columnspacing`` / ``frameon`` style the legend (``None`` keeps the
|
|
940
|
+
``rcParams`` default); the compact mode passes a smaller font, shorter
|
|
941
|
+
handles and no box so more labels fit per row.
|
|
830
942
|
"""
|
|
831
943
|
fig = ax.get_figure()
|
|
944
|
+
loc = (
|
|
945
|
+
"outside upper center"
|
|
946
|
+
if position == "above"
|
|
947
|
+
else "outside lower center"
|
|
948
|
+
)
|
|
832
949
|
return fig.legend( # type: ignore[reportUnknownMemberType, reportOptionalMemberAccess]
|
|
833
950
|
handles,
|
|
834
951
|
labels,
|
|
835
|
-
loc=
|
|
952
|
+
loc=loc,
|
|
836
953
|
ncol=ncol,
|
|
837
954
|
fontsize=fontsize,
|
|
838
955
|
handlelength=handlelength,
|
|
839
956
|
handletextpad=handletextpad,
|
|
840
957
|
columnspacing=columnspacing,
|
|
958
|
+
frameon=frameon,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def legend_above(
|
|
963
|
+
ax: Axes,
|
|
964
|
+
handles: list[Any],
|
|
965
|
+
labels: list[str],
|
|
966
|
+
*,
|
|
967
|
+
ncol: int,
|
|
968
|
+
fontsize: float | None = None,
|
|
969
|
+
handlelength: float | None = None,
|
|
970
|
+
handletextpad: float | None = None,
|
|
971
|
+
columnspacing: float | None = None,
|
|
972
|
+
) -> MplLegend:
|
|
973
|
+
"""Frameless legend band above the axes.
|
|
974
|
+
|
|
975
|
+
Thin shortcut for ``legend_band(..., position="above",
|
|
976
|
+
frameon=False)``, kept because it is the call the compact toolkit has
|
|
977
|
+
always exported for third-party artists. New code should call
|
|
978
|
+
:func:`legend_band` (or better :func:`place_legend`) so the band side
|
|
979
|
+
stays configurable.
|
|
980
|
+
"""
|
|
981
|
+
return legend_band(
|
|
982
|
+
ax,
|
|
983
|
+
handles,
|
|
984
|
+
labels,
|
|
985
|
+
ncol=ncol,
|
|
986
|
+
position="above",
|
|
987
|
+
fontsize=fontsize,
|
|
988
|
+
handlelength=handlelength,
|
|
989
|
+
handletextpad=handletextpad,
|
|
990
|
+
columnspacing=columnspacing,
|
|
841
991
|
frameon=False, # no box: less clutter in the tight compact figure
|
|
842
992
|
)
|
|
843
993
|
|
|
@@ -918,19 +1068,23 @@ def place_legend(
|
|
|
918
1068
|
*,
|
|
919
1069
|
compact: bool,
|
|
920
1070
|
ncol: int | None = None,
|
|
1071
|
+
position: str = "auto",
|
|
921
1072
|
) -> MplLegend | None:
|
|
922
1073
|
"""Place the figure legend, compact-aware, from the labelled artists.
|
|
923
1074
|
|
|
924
|
-
Compact charts get
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
``
|
|
929
|
-
|
|
1075
|
+
Compact charts get a frameless band spanning the figure width with the
|
|
1076
|
+
compact legend metrics; ``ncol`` is auto-fitted to that width and
|
|
1077
|
+
hard-capped by :func:`compact_legend_max_ncol` unless the caller passes
|
|
1078
|
+
an explicit ``ncol``. Non-compact charts get the same band with the
|
|
1079
|
+
``rcParams`` metrics. ``position`` (``"above"`` / ``"below"`` /
|
|
1080
|
+
``"auto"``) chooses the side through :func:`resolve_legend_position`, so
|
|
1081
|
+
a compact chart can carry its legend under the plot instead of over it.
|
|
1082
|
+
Returns ``None`` (drawing nothing) when there are no labels.
|
|
930
1083
|
"""
|
|
931
1084
|
handles, labels = ax.get_legend_handles_labels()
|
|
932
1085
|
if not labels:
|
|
933
1086
|
return None
|
|
1087
|
+
side = resolve_legend_position(position, compact=compact)
|
|
934
1088
|
fig_w_px: float = fig.get_size_inches()[0] * fig.dpi
|
|
935
1089
|
if compact:
|
|
936
1090
|
legend_kw = compact_legend_kwargs()
|
|
@@ -941,15 +1095,37 @@ def place_legend(
|
|
|
941
1095
|
max_ncol = compact_legend_max_ncol()
|
|
942
1096
|
if max_ncol is not None:
|
|
943
1097
|
ncol = min(ncol, max_ncol)
|
|
944
|
-
return
|
|
1098
|
+
return legend_band(
|
|
1099
|
+
ax,
|
|
1100
|
+
handles,
|
|
1101
|
+
labels,
|
|
1102
|
+
ncol=ncol,
|
|
1103
|
+
position=side,
|
|
1104
|
+
frameon=False,
|
|
1105
|
+
**legend_kw,
|
|
1106
|
+
)
|
|
945
1107
|
if ncol is None:
|
|
946
1108
|
ncol = auto_ncol(ax, labels, available_width_px=fig_w_px)
|
|
947
|
-
return
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1109
|
+
return legend_band(ax, handles, labels, ncol=ncol, position=side)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def legend_layout(legend: Legend | None) -> dict[str, Any]:
|
|
1113
|
+
"""``place_legend`` layout kwargs from an optional :class:`Legend` holder.
|
|
1114
|
+
|
|
1115
|
+
Every artist carries a ``legend: Legend | None`` (absent means "house
|
|
1116
|
+
defaults"), so this maps the holder to the ``ncol`` / ``position`` pair
|
|
1117
|
+
in one place instead of each artist repeating a
|
|
1118
|
+
``self.legend.x if self.legend else <default>`` per knob -- and a new
|
|
1119
|
+
knob then reaches every artist at once::
|
|
1120
|
+
|
|
1121
|
+
place_legend(
|
|
1122
|
+
fig, ax, compact=self.compact, **legend_layout(self.legend)
|
|
1123
|
+
)
|
|
1124
|
+
"""
|
|
1125
|
+
return {
|
|
1126
|
+
"ncol": legend.ncol if legend is not None else None,
|
|
1127
|
+
"position": legend.position if legend is not None else "auto",
|
|
1128
|
+
}
|
|
953
1129
|
|
|
954
1130
|
|
|
955
1131
|
def declutter_spines(ax: Axes, side: str = "left") -> None:
|
|
@@ -45,6 +45,7 @@ from tesorotools.artists._common import (
|
|
|
45
45
|
compact_figure_kwargs,
|
|
46
46
|
compact_font_pt,
|
|
47
47
|
dynamic_dpi,
|
|
48
|
+
legend_layout,
|
|
48
49
|
place_legend,
|
|
49
50
|
resolve_data,
|
|
50
51
|
style_baseline,
|
|
@@ -323,9 +324,10 @@ class BarLinePlot:
|
|
|
323
324
|
if self.compact:
|
|
324
325
|
style_compact_axes(ax, font_pt, side="right")
|
|
325
326
|
|
|
326
|
-
legend_ncol = self.legend.ncol if self.legend else None
|
|
327
327
|
if self.legend is not None:
|
|
328
|
-
place_legend(
|
|
328
|
+
place_legend(
|
|
329
|
+
fig, ax, compact=self.compact, **legend_layout(self.legend)
|
|
330
|
+
)
|
|
329
331
|
|
|
330
332
|
if self.plot_size is not None and not self.compact:
|
|
331
333
|
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
{tesorotools_python-0.0.53 → tesorotools_python-0.0.55}/src/tesorotools/artists/barh_plot.py
RENAMED
|
@@ -40,6 +40,7 @@ from tesorotools.artists._common import (
|
|
|
40
40
|
compact_figure_kwargs,
|
|
41
41
|
compact_font_pt,
|
|
42
42
|
dynamic_dpi,
|
|
43
|
+
legend_layout,
|
|
43
44
|
place_legend,
|
|
44
45
|
resolve_data,
|
|
45
46
|
)
|
|
@@ -928,11 +929,12 @@ class GroupedBarChart:
|
|
|
928
929
|
axis="both", labelsize=font_pt
|
|
929
930
|
)
|
|
930
931
|
|
|
931
|
-
#
|
|
932
|
-
#
|
|
933
|
-
#
|
|
934
|
-
|
|
935
|
-
|
|
932
|
+
# the legend lives in a band spanning the figure width: above the
|
|
933
|
+
# axes in compact (columns auto-fitted and capped), below it at full
|
|
934
|
+
# size. !legend {position} overrides the side.
|
|
935
|
+
place_legend(
|
|
936
|
+
fig, ax, compact=self.compact, **legend_layout(self.legend)
|
|
937
|
+
)
|
|
936
938
|
|
|
937
939
|
# compact fixes the figure size, so the axes-sizing pass would fight
|
|
938
940
|
# it; skip it there (mirrors LinePlot).
|
|
@@ -53,8 +53,9 @@ from tesorotools.artists._common import (
|
|
|
53
53
|
compact_font_pt,
|
|
54
54
|
compact_legend_kwargs,
|
|
55
55
|
dynamic_dpi,
|
|
56
|
-
|
|
56
|
+
legend_band,
|
|
57
57
|
resolve_data,
|
|
58
|
+
resolve_legend_position,
|
|
58
59
|
style_compact_axes,
|
|
59
60
|
style_spines,
|
|
60
61
|
)
|
|
@@ -478,19 +479,28 @@ class BoxPlot:
|
|
|
478
479
|
if not draw_last:
|
|
479
480
|
handles, labels = handles[:-1], labels[:-1]
|
|
480
481
|
fig_w_px: float = fig.get_size_inches()[0] * fig.dpi
|
|
482
|
+
position = resolve_legend_position(
|
|
483
|
+
self.legend.position, compact=self.compact
|
|
484
|
+
)
|
|
481
485
|
if self.compact:
|
|
482
486
|
kw = compact_legend_kwargs()
|
|
483
487
|
ncol = self.legend.ncol or auto_ncol(
|
|
484
488
|
ax, labels, available_width_px=fig_w_px, **kw
|
|
485
489
|
)
|
|
486
|
-
|
|
490
|
+
legend_band(
|
|
491
|
+
ax,
|
|
492
|
+
handles,
|
|
493
|
+
labels,
|
|
494
|
+
ncol=ncol,
|
|
495
|
+
position=position,
|
|
496
|
+
frameon=False,
|
|
497
|
+
**kw,
|
|
498
|
+
)
|
|
487
499
|
else:
|
|
488
500
|
ncol = self.legend.ncol or auto_ncol(
|
|
489
501
|
ax, labels, available_width_px=fig_w_px
|
|
490
502
|
)
|
|
491
|
-
|
|
492
|
-
handles, labels, loc="outside lower center", ncol=ncol
|
|
493
|
-
)
|
|
503
|
+
legend_band(ax, handles, labels, ncol=ncol, position=position)
|
|
494
504
|
|
|
495
505
|
def save(
|
|
496
506
|
self,
|
|
@@ -18,11 +18,19 @@ into :mod:`tesorotools.artists._common`. The recipe is:
|
|
|
18
18
|
point size (equal to what the reader sees in Word).
|
|
19
19
|
3. Draw the chart, then ``style_compact_axes(ax, font_pt, side=...)`` to
|
|
20
20
|
pin the tick label size and the y-axis side / spines.
|
|
21
|
-
4. ``place_legend(fig, ax, compact=compact)``
|
|
22
|
-
with auto-fitted, capped columns
|
|
21
|
+
4. ``place_legend(fig, ax, compact=compact, **legend_layout(legend))``
|
|
22
|
+
-- the full-figure-width legend band with auto-fitted, capped columns,
|
|
23
|
+
above the axes by default and below them when the chart's
|
|
24
|
+
``!legend {position: below}`` (or ``compact.legend_position``) asks for
|
|
25
|
+
it.
|
|
23
26
|
5. For a date x-axis, ``compact_date_axis(ax, lo, hi)`` thins the ticks to
|
|
24
27
|
a few concise labels.
|
|
25
28
|
|
|
29
|
+
Legend side: the band sits above the axes by default (the bottom edge
|
|
30
|
+
already spends space on the date ticks). ``compact.legend_position`` in
|
|
31
|
+
``plots.yaml`` flips that default for every compact chart and
|
|
32
|
+
``legend: !legend {position: above|below}`` decides it per chart.
|
|
33
|
+
|
|
26
34
|
Legends note: a compact figure is only ~7.4 cm wide, so a few long labels
|
|
27
35
|
(e.g. ``"Pre-conflicto 2026-02-27"``) collapse the column fit to a single
|
|
28
36
|
tall column that eats the chart. Shorten labels for compact charts and
|
|
@@ -41,10 +49,14 @@ from tesorotools.artists._common import (
|
|
|
41
49
|
compact_legend_font_pt,
|
|
42
50
|
compact_legend_kwargs,
|
|
43
51
|
compact_legend_max_ncol,
|
|
52
|
+
compact_legend_position,
|
|
44
53
|
compact_y_axis,
|
|
45
54
|
declutter_spines,
|
|
46
55
|
legend_above,
|
|
56
|
+
legend_band,
|
|
57
|
+
legend_layout,
|
|
47
58
|
place_legend,
|
|
59
|
+
resolve_legend_position,
|
|
48
60
|
style_compact_axes,
|
|
49
61
|
)
|
|
50
62
|
|
|
@@ -58,9 +70,13 @@ __all__ = [
|
|
|
58
70
|
"compact_legend_font_pt",
|
|
59
71
|
"compact_legend_kwargs",
|
|
60
72
|
"compact_legend_max_ncol",
|
|
73
|
+
"compact_legend_position",
|
|
61
74
|
"compact_y_axis",
|
|
62
75
|
"declutter_spines",
|
|
63
76
|
"legend_above",
|
|
77
|
+
"legend_band",
|
|
78
|
+
"legend_layout",
|
|
64
79
|
"place_legend",
|
|
80
|
+
"resolve_legend_position",
|
|
65
81
|
"style_compact_axes",
|
|
66
82
|
]
|