tesorotools-python 0.0.45__tar.gz → 0.0.47__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.45 → tesorotools_python-0.0.47}/.gitignore +3 -1
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/PKG-INFO +1 -1
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/__init__.py +9 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/artists/__init__.py +6 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/artists/_common.py +136 -5
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/artists/line_plot.py +88 -15
- tesorotools_python-0.0.47/src/tesorotools/artists/matrix.py +302 -0
- tesorotools_python-0.0.47/src/tesorotools/artists/vector_plot.py +315 -0
- tesorotools_python-0.0.47/src/tesorotools/artists/waterfall.py +322 -0
- tesorotools_python-0.0.47/src/tesorotools/assets/plots.yaml +95 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/tesoro.mplstyle +2 -2
- tesorotools_python-0.0.45/src/tesorotools/assets/plots.yaml +0 -44
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/pyproject.toml +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/artists/barh_plot.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/artists/stacked.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/artists/type_curve.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/providers/bde.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/providers/lseg.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/utils/template.py +0 -0
|
@@ -49,9 +49,12 @@ from tesorotools.artists import (
|
|
|
49
49
|
HorizontalBarChart,
|
|
50
50
|
Legend,
|
|
51
51
|
LinePlot,
|
|
52
|
+
MatrixChart,
|
|
52
53
|
StackedAreaPlot,
|
|
53
54
|
StackedBarPlot,
|
|
54
55
|
TypeCurve,
|
|
56
|
+
VectorPlot,
|
|
57
|
+
Waterfall,
|
|
55
58
|
)
|
|
56
59
|
from tesorotools.orchestration import CompositeRegistry, iter_contexts
|
|
57
60
|
from tesorotools.providers.base import (
|
|
@@ -79,6 +82,9 @@ def _register_builtins() -> None:
|
|
|
79
82
|
register_artist("barh", HorizontalBarChart)
|
|
80
83
|
register_artist("grouped_barh", GroupedBarChart)
|
|
81
84
|
register_artist("type_curve", TypeCurve)
|
|
85
|
+
register_artist("vector_plot", VectorPlot)
|
|
86
|
+
register_artist("matrix", MatrixChart)
|
|
87
|
+
register_artist("waterfall", Waterfall)
|
|
82
88
|
|
|
83
89
|
register_tag("format", Format)
|
|
84
90
|
register_tag("legend", Legend)
|
|
@@ -111,6 +117,7 @@ __all__ = [
|
|
|
111
117
|
"Legend",
|
|
112
118
|
"LinePlot",
|
|
113
119
|
"LSEGProvider",
|
|
120
|
+
"MatrixChart",
|
|
114
121
|
"RegistryProtocol",
|
|
115
122
|
"Report",
|
|
116
123
|
"Section",
|
|
@@ -121,6 +128,8 @@ __all__ = [
|
|
|
121
128
|
"Text",
|
|
122
129
|
"Title",
|
|
123
130
|
"TypeCurve",
|
|
131
|
+
"VectorPlot",
|
|
132
|
+
"Waterfall",
|
|
124
133
|
"YamlConstructor",
|
|
125
134
|
"all_artists",
|
|
126
135
|
"all_providers",
|
|
@@ -29,8 +29,11 @@ import matplotlib.style
|
|
|
29
29
|
from tesorotools.artists._common import Artist, Format, Legend
|
|
30
30
|
from tesorotools.artists.barh_plot import GroupedBarChart, HorizontalBarChart
|
|
31
31
|
from tesorotools.artists.line_plot import LinePlot
|
|
32
|
+
from tesorotools.artists.matrix import MatrixChart
|
|
32
33
|
from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
|
|
33
34
|
from tesorotools.artists.type_curve import TypeCurve
|
|
35
|
+
from tesorotools.artists.vector_plot import VectorPlot
|
|
36
|
+
from tesorotools.artists.waterfall import Waterfall
|
|
34
37
|
from tesorotools.utils.globals import STYLE_SHEET
|
|
35
38
|
|
|
36
39
|
matplotlib.style.use(STYLE_SHEET)
|
|
@@ -42,7 +45,10 @@ __all__ = [
|
|
|
42
45
|
"HorizontalBarChart",
|
|
43
46
|
"Legend",
|
|
44
47
|
"LinePlot",
|
|
48
|
+
"MatrixChart",
|
|
45
49
|
"StackedAreaPlot",
|
|
46
50
|
"StackedBarPlot",
|
|
47
51
|
"TypeCurve",
|
|
52
|
+
"VectorPlot",
|
|
53
|
+
"Waterfall",
|
|
48
54
|
]
|
|
@@ -29,9 +29,14 @@ from typing import Any, Protocol, Self, cast, runtime_checkable
|
|
|
29
29
|
import matplotlib.pyplot as plt
|
|
30
30
|
import pandas as pd
|
|
31
31
|
from matplotlib.axes import Axes
|
|
32
|
+
from matplotlib.dates import AutoDateLocator, ConciseDateFormatter
|
|
33
|
+
from matplotlib.dates import (
|
|
34
|
+
date2num, # type: ignore[reportUnknownVariableType]
|
|
35
|
+
)
|
|
32
36
|
from matplotlib.figure import Figure
|
|
33
37
|
from matplotlib.font_manager import FontProperties
|
|
34
|
-
from matplotlib.
|
|
38
|
+
from matplotlib.legend import Legend as MplLegend
|
|
39
|
+
from matplotlib.ticker import FixedLocator, FuncFormatter
|
|
35
40
|
from yaml.nodes import MappingNode
|
|
36
41
|
|
|
37
42
|
from tesorotools.utils.matplotlib import (
|
|
@@ -45,6 +50,9 @@ load_fonts()
|
|
|
45
50
|
|
|
46
51
|
AX_CONFIG: dict[str, Any] = PLOT_CONFIG["ax"]
|
|
47
52
|
FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
|
|
53
|
+
COMPACT_CONFIG: dict[str, Any] = PLOT_CONFIG.get("compact", {})
|
|
54
|
+
|
|
55
|
+
CM_TO_IN: float = 1.0 / 2.54
|
|
48
56
|
|
|
49
57
|
|
|
50
58
|
# ----------------------------------------------------------------------
|
|
@@ -295,6 +303,117 @@ def export_legend(
|
|
|
295
303
|
legend.remove()
|
|
296
304
|
|
|
297
305
|
|
|
306
|
+
# ----------------------------------------------------------------------
|
|
307
|
+
# Compact mode (charts that live inside a Word table, two per row)
|
|
308
|
+
# ----------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def compact_figsize() -> tuple[float, float]:
|
|
312
|
+
"""Return the compact figure size ``(width, height)`` in inches.
|
|
313
|
+
|
|
314
|
+
The width is the **total** figure width (chart + legend),
|
|
315
|
+
read from the ``compact`` section of ``plots.yaml`` in
|
|
316
|
+
centimetres and converted to inches. Defaults to
|
|
317
|
+
7.4 cm x 5.5 cm when the config is absent.
|
|
318
|
+
"""
|
|
319
|
+
width_cm: float = float(COMPACT_CONFIG.get("width_cm", 7.4))
|
|
320
|
+
height_cm: float = float(COMPACT_CONFIG.get("height_cm", 5.5))
|
|
321
|
+
return (width_cm * CM_TO_IN, height_cm * CM_TO_IN)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def compact_font_pt() -> float | None:
|
|
325
|
+
"""Font size (points) for compact charts, or ``None`` to keep the style.
|
|
326
|
+
|
|
327
|
+
Because the compact figure is rendered at its true
|
|
328
|
+
physical size (7.4 cm) and embedded without rescaling,
|
|
329
|
+
these points equal the points the reader sees in Word --
|
|
330
|
+
set ``compact.font_pt`` in ``plots.yaml`` to the document
|
|
331
|
+
body-text size so chart text matches running text.
|
|
332
|
+
"""
|
|
333
|
+
raw: Any = COMPACT_CONFIG.get("font_pt")
|
|
334
|
+
return None if raw is None else float(raw)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def compact_date_axis(
|
|
338
|
+
ax: Axes,
|
|
339
|
+
data_min: Any,
|
|
340
|
+
data_max: Any,
|
|
341
|
+
*,
|
|
342
|
+
maxticks: int = 4,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Compact date x-axis: few horizontal ticks, last one near the data end.
|
|
345
|
+
|
|
346
|
+
Ticks are computed over the **data span** ``[data_min,
|
|
347
|
+
data_max]`` and pinned with a ``FixedLocator``, so none
|
|
348
|
+
float in the right-hand margin that the value labels open
|
|
349
|
+
up -- the last tick lands as close to the final data
|
|
350
|
+
point as the tick step allows. Requires the series to
|
|
351
|
+
have been plotted with ``x_compat=True`` (pandas' own
|
|
352
|
+
date plotting ignores this formatter).
|
|
353
|
+
"""
|
|
354
|
+
lo: float = float(cast(Any, date2num(data_min)))
|
|
355
|
+
hi: float = float(cast(Any, date2num(data_max)))
|
|
356
|
+
locator = AutoDateLocator(minticks=2, maxticks=maxticks)
|
|
357
|
+
candidates: list[float] = cast(
|
|
358
|
+
"list[float]", locator.tick_values(data_min, data_max)
|
|
359
|
+
)
|
|
360
|
+
ticks: list[float] = [t for t in candidates if lo <= t <= hi]
|
|
361
|
+
if not ticks:
|
|
362
|
+
ticks = [lo, hi]
|
|
363
|
+
fixed = FixedLocator(ticks)
|
|
364
|
+
ax.xaxis.set_major_locator(fixed)
|
|
365
|
+
ax.xaxis.set_major_formatter(ConciseDateFormatter(fixed))
|
|
366
|
+
ax.tick_params( # type: ignore[reportUnknownMemberType]
|
|
367
|
+
axis="x", rotation=0
|
|
368
|
+
)
|
|
369
|
+
for label in ax.get_xticklabels():
|
|
370
|
+
label.set_horizontalalignment("center")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def legend_above(
|
|
374
|
+
ax: Axes,
|
|
375
|
+
handles: list[Any],
|
|
376
|
+
labels: list[str],
|
|
377
|
+
*,
|
|
378
|
+
ncol: int,
|
|
379
|
+
fontsize: float | None = None,
|
|
380
|
+
) -> MplLegend:
|
|
381
|
+
"""Place the legend **above** the axes, expanded to its full width.
|
|
382
|
+
|
|
383
|
+
Uses ``mode="expand"`` so the legend spans exactly the
|
|
384
|
+
plot width (never wider) and ``bbox_to_anchor`` from the
|
|
385
|
+
``compact`` config (default ``(0, 1.06, 1, 0.12)`` -- the
|
|
386
|
+
``y0 > 1`` lifts it clear of the axes). The legend
|
|
387
|
+
becomes a child of the axes, so constrained layout
|
|
388
|
+
reserves room for it inside the fixed compact figure.
|
|
389
|
+
"""
|
|
390
|
+
bbox: Any = COMPACT_CONFIG.get("legend_bbox", [0.0, 1.06, 1.0, 0.12])
|
|
391
|
+
return ax.legend( # type: ignore[reportUnknownMemberType]
|
|
392
|
+
handles,
|
|
393
|
+
labels,
|
|
394
|
+
loc="lower left",
|
|
395
|
+
bbox_to_anchor=tuple(bbox),
|
|
396
|
+
mode="expand",
|
|
397
|
+
borderaxespad=0.0,
|
|
398
|
+
ncol=ncol,
|
|
399
|
+
fontsize=fontsize,
|
|
400
|
+
frameon=False, # no box: less clutter in the tight compact figure
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def declutter_spines(ax: Axes) -> None:
|
|
405
|
+
"""Hide the spines that carry no ticks (top and right).
|
|
406
|
+
|
|
407
|
+
Compact line charts move the y-ticks to the **left** (so
|
|
408
|
+
the right edge is free for the end-of-series value labels)
|
|
409
|
+
and keep x-ticks on the bottom, leaving the top and right
|
|
410
|
+
spines as pure clutter -- drop them to free space and
|
|
411
|
+
reduce visual noise.
|
|
412
|
+
"""
|
|
413
|
+
ax.spines["top"].set_visible(False)
|
|
414
|
+
ax.spines["right"].set_visible(False)
|
|
415
|
+
|
|
416
|
+
|
|
298
417
|
# ----------------------------------------------------------------------
|
|
299
418
|
# Cartesian (vertical-y) styling
|
|
300
419
|
# ----------------------------------------------------------------------
|
|
@@ -378,6 +497,9 @@ def annotate_last_values(
|
|
|
378
497
|
labels: dict[str, str] | None = None,
|
|
379
498
|
series_styles: dict[str, dict[str, Any]] | None = None,
|
|
380
499
|
annotate_color: str | None = None,
|
|
500
|
+
weight: str | None = None,
|
|
501
|
+
fontsize: float | None = None,
|
|
502
|
+
right_pad_px: float = 10.0,
|
|
381
503
|
) -> None:
|
|
382
504
|
"""Label the last non-NaN value of each column on the right.
|
|
383
505
|
|
|
@@ -392,8 +514,12 @@ def annotate_last_values(
|
|
|
392
514
|
|
|
393
515
|
Labels are packed vertically in display space so that
|
|
394
516
|
series ending at nearly the same value do not overlap.
|
|
395
|
-
|
|
396
|
-
|
|
517
|
+
They sit just right of the last point and the x-axis
|
|
518
|
+
limit is extended by the widest label plus ``right_pad_px``
|
|
519
|
+
so the text is not clipped by the axes frame; raise
|
|
520
|
+
``right_pad_px`` (e.g. on compact charts) to open more
|
|
521
|
+
blank space on the right and keep the labels clear of both
|
|
522
|
+
the data lines and the right-hand y-tick labels.
|
|
397
523
|
"""
|
|
398
524
|
fig = ax.get_figure()
|
|
399
525
|
if fig is None:
|
|
@@ -428,7 +554,7 @@ def annotate_last_values(
|
|
|
428
554
|
trans = ax.transData
|
|
429
555
|
|
|
430
556
|
sample = ax.text( # type: ignore[reportUnknownMemberType]
|
|
431
|
-
0, 0, "0"
|
|
557
|
+
0, 0, "0", fontsize=fontsize
|
|
432
558
|
)
|
|
433
559
|
text_height = (
|
|
434
560
|
sample.get_window_extent(renderer).height # type: ignore[reportUnknownArgumentType]
|
|
@@ -459,13 +585,18 @@ def annotate_last_values(
|
|
|
459
585
|
color=color,
|
|
460
586
|
va="center",
|
|
461
587
|
ha="left",
|
|
588
|
+
fontweight=weight if weight is not None else "normal",
|
|
589
|
+
fontsize=fontsize,
|
|
462
590
|
)
|
|
463
591
|
width = t.get_window_extent(renderer).width # type: ignore[reportUnknownArgumentType]
|
|
464
592
|
max_label_width = max(max_label_width, width)
|
|
465
593
|
|
|
594
|
+
# labels spill right of the data, so widen the x-limit by the widest
|
|
595
|
+
# label plus right_pad_px to keep them unclipped (and, with a larger
|
|
596
|
+
# pad, clear of the right-hand y-tick labels).
|
|
466
597
|
inv = trans.inverted()
|
|
467
598
|
x0: float = inv.transform((0, 0))[0] # type: ignore[reportUnknownArgumentType]
|
|
468
|
-
x1: float = inv.transform((max_label_width +
|
|
599
|
+
x1: float = inv.transform((max_label_width + right_pad_px, 0))[0] # type: ignore[reportUnknownArgumentType]
|
|
469
600
|
xmin, xmax = ax.get_xlim()
|
|
470
601
|
ax.set_xlim(xmin, xmax + (x1 - x0))
|
|
471
602
|
|
{tesorotools_python-0.0.45 → tesorotools_python-0.0.47}/src/tesorotools/artists/line_plot.py
RENAMED
|
@@ -29,7 +29,12 @@ from tesorotools.artists._common import (
|
|
|
29
29
|
adjust_figure_for_plot_size,
|
|
30
30
|
annotate_last_values,
|
|
31
31
|
auto_ncol,
|
|
32
|
+
compact_date_axis,
|
|
33
|
+
compact_figsize,
|
|
34
|
+
compact_font_pt,
|
|
35
|
+
declutter_spines,
|
|
32
36
|
draw_vlines,
|
|
37
|
+
legend_above,
|
|
33
38
|
style_baseline,
|
|
34
39
|
style_spines,
|
|
35
40
|
)
|
|
@@ -60,6 +65,12 @@ class LinePlot:
|
|
|
60
65
|
(color, linestyle, ...).
|
|
61
66
|
* ``baseline`` -- draw an axhline at 0 (or 100 when
|
|
62
67
|
``base_100``).
|
|
68
|
+
* ``compact`` -- render at a fixed physical size (7.4 cm
|
|
69
|
+
wide *including* the legend, from ``plots.yaml``) for
|
|
70
|
+
placing two charts per row inside a Word table. Moves
|
|
71
|
+
the legend above the plot expanded to its width, uses a
|
|
72
|
+
``ConciseDateFormatter`` date axis and bold end-of-series
|
|
73
|
+
labels.
|
|
63
74
|
|
|
64
75
|
Render lifecycle: ``build()`` returns ``(Figure, Axes)``
|
|
65
76
|
in memory; ``save(fig)`` persists to ``out_path``;
|
|
@@ -87,6 +98,7 @@ class LinePlot:
|
|
|
87
98
|
series_styles: dict[str, dict[str, Any]] | None = None,
|
|
88
99
|
plot_size: tuple[float, float] | None = None,
|
|
89
100
|
vlines: list[dict[str, Any]] | None = None,
|
|
101
|
+
compact: bool = False,
|
|
90
102
|
) -> None:
|
|
91
103
|
if out_path.suffix != ".png":
|
|
92
104
|
raise ValueError(f"out_path must be .png: {out_path}")
|
|
@@ -108,6 +120,7 @@ class LinePlot:
|
|
|
108
120
|
self.series_styles = series_styles or {}
|
|
109
121
|
self.plot_size = plot_size
|
|
110
122
|
self.vlines = vlines or []
|
|
123
|
+
self.compact = compact
|
|
111
124
|
|
|
112
125
|
@classmethod
|
|
113
126
|
def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
|
|
@@ -130,7 +143,7 @@ class LinePlot:
|
|
|
130
143
|
``base_100_date``, ``annotate``, ``annotate_color``,
|
|
131
144
|
``baseline``, ``fmt`` (``!format``), ``legend``
|
|
132
145
|
(``!legend``), ``figsize``, ``series_styles``,
|
|
133
|
-
``plot_size``, ``vlines``.
|
|
146
|
+
``plot_size``, ``vlines``, ``compact``.
|
|
134
147
|
|
|
135
148
|
Example
|
|
136
149
|
-------
|
|
@@ -199,13 +212,30 @@ class LinePlot:
|
|
|
199
212
|
fig_kw = dict(FIG_CONFIG)
|
|
200
213
|
if self.figsize is not None:
|
|
201
214
|
fig_kw["figsize"] = self.figsize
|
|
215
|
+
elif self.compact:
|
|
216
|
+
fig_kw["figsize"] = compact_figsize()
|
|
202
217
|
fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
|
|
203
218
|
**fig_kw
|
|
204
219
|
)
|
|
220
|
+
font_pt = compact_font_pt() if self.compact else None
|
|
221
|
+
|
|
205
222
|
ax = fig.add_subplot()
|
|
206
223
|
for col in plot_data.columns:
|
|
207
224
|
style = self.series_styles.get(col, {})
|
|
208
|
-
|
|
225
|
+
# compact mode needs matplotlib's native date axis so the
|
|
226
|
+
# ConciseDateFormatter takes effect (x_compat disables pandas'
|
|
227
|
+
# own date plotting machinery).
|
|
228
|
+
plot_data[col].plot(
|
|
229
|
+
ax=ax,
|
|
230
|
+
label=self.series[col],
|
|
231
|
+
x_compat=self.compact,
|
|
232
|
+
**style,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if self.compact:
|
|
236
|
+
# no left/right padding: the line starts exactly at the first
|
|
237
|
+
# data point. annotate_last_values reopens room on the right.
|
|
238
|
+
ax.margins(x=0)
|
|
209
239
|
|
|
210
240
|
if self.vlines:
|
|
211
241
|
draw_vlines(ax, self.vlines)
|
|
@@ -219,6 +249,18 @@ class LinePlot:
|
|
|
219
249
|
labels=self.series,
|
|
220
250
|
series_styles=self.series_styles,
|
|
221
251
|
annotate_color=self.annotate_color,
|
|
252
|
+
weight="bold" if self.compact else None,
|
|
253
|
+
fontsize=font_pt,
|
|
254
|
+
# blank space (dynamic, scales with font + dpi) opened past
|
|
255
|
+
# the label. compact has the y-axis on the LEFT, so half an em
|
|
256
|
+
# of clearance is enough; normal keeps the y-axis on the RIGHT,
|
|
257
|
+
# so leave a wider gap to separate the value labels from the
|
|
258
|
+
# y-tick numbers they would otherwise butt against.
|
|
259
|
+
right_pad_px=(
|
|
260
|
+
0.5 * (font_pt or 12.0) * fig.dpi / 72.0
|
|
261
|
+
if self.compact
|
|
262
|
+
else 1.6 * plt.rcParams["font.size"] * fig.dpi / 72.0
|
|
263
|
+
),
|
|
222
264
|
)
|
|
223
265
|
|
|
224
266
|
style_spines(
|
|
@@ -231,23 +273,54 @@ class LinePlot:
|
|
|
231
273
|
reference = 100 if self.base_100 else 0
|
|
232
274
|
style_baseline(ax, reference, **AX_CONFIG["baseline"])
|
|
233
275
|
|
|
276
|
+
if self.compact:
|
|
277
|
+
# pin the date ticks to the data span (after annotate widened the
|
|
278
|
+
# x-limit) so the last tick sits next to the final data point and
|
|
279
|
+
# none float in the value-label margin; match Word's body font.
|
|
280
|
+
compact_date_axis(ax, plot_data.index.min(), plot_data.index.max())
|
|
281
|
+
# compact only: y-numbers on the LEFT so the right edge is free
|
|
282
|
+
# for the value labels, and drop the now-tickless top/right
|
|
283
|
+
# spines. Normal mode keeps the house right-hand y-axis and box.
|
|
284
|
+
ax.yaxis.tick_left()
|
|
285
|
+
declutter_spines(ax)
|
|
286
|
+
if font_pt is not None:
|
|
287
|
+
ax.tick_params( # type: ignore[reportUnknownMemberType]
|
|
288
|
+
axis="both", labelsize=font_pt
|
|
289
|
+
)
|
|
290
|
+
|
|
234
291
|
if self.legend is not None:
|
|
235
292
|
labels = [self.series[c] for c in plot_data.columns]
|
|
236
293
|
handles, label_strs = ax.get_legend_handles_labels()
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
294
|
+
if self.compact:
|
|
295
|
+
# legend ABOVE the plot, expanded to the plot width. ncol is
|
|
296
|
+
# auto-fitted to the (narrow) axes width so labels never
|
|
297
|
+
# overlap; long labels stack into rows. Override the column
|
|
298
|
+
# count via !legend {ncol: N} when you know they fit.
|
|
299
|
+
ncol = (
|
|
300
|
+
self.legend.ncol
|
|
301
|
+
if self.legend.ncol is not None
|
|
302
|
+
else auto_ncol(ax, labels)
|
|
303
|
+
)
|
|
304
|
+
legend_above(
|
|
305
|
+
ax, handles, label_strs, ncol=ncol, fontsize=font_pt
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
fig_width_px: float = fig.get_size_inches()[0] * fig.dpi
|
|
309
|
+
ncol = (
|
|
310
|
+
self.legend.ncol
|
|
311
|
+
if self.legend.ncol is not None
|
|
312
|
+
else auto_ncol(ax, labels, available_width_px=fig_width_px)
|
|
313
|
+
)
|
|
314
|
+
fig.legend( # type: ignore[reportUnknownMemberType]
|
|
315
|
+
handles,
|
|
316
|
+
label_strs,
|
|
317
|
+
loc="outside lower center",
|
|
318
|
+
ncol=ncol,
|
|
319
|
+
)
|
|
249
320
|
|
|
250
|
-
|
|
321
|
+
# compact mode fixes the *figure* size (width includes the legend),
|
|
322
|
+
# so the axes-sizing pass would fight it; skip it there.
|
|
323
|
+
if self.plot_size is not None and not self.compact:
|
|
251
324
|
adjust_figure_for_plot_size(fig, ax, self.plot_size)
|
|
252
325
|
|
|
253
326
|
return fig, ax
|