gsplot 0.4.0__tar.gz → 0.4.1__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.
- {gsplot-0.4.0 → gsplot-0.4.1}/PKG-INFO +1 -1
- {gsplot-0.4.0 → gsplot-0.4.1}/pyproject.toml +1 -1
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/__init__.py +2 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/__init__.pyi +2 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy_api.py +16 -10
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/root.py +1 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/root_api.py +65 -63
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/plans.py +4 -3
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/targets.py +4 -3
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/types.py +79 -15
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_figure/layout.py +5 -13
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_figure/output.py +18 -9
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_plot/basic.py +2 -2
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_plot/colored.py +34 -4
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_plot/series.py +4 -4
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_style/axes.py +562 -21
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_style/legends.py +77 -36
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_style/panels.py +143 -12
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_style/paper.py +3 -1
- {gsplot-0.4.0 → gsplot-0.4.1}/LICENSE +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/README.md +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/config.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/base/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/base/base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/base/base_alias_validator.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/color/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/color/colormap.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/config/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/config/config.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/data/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/data/load_file.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/axes.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/axes_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/axes_inset.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/axes_range_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/figure_tools.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/show.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/figure/store.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/hello_world/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/hello_world/hello_world.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/logger.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/path/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/path/path.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/line.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/line_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/line_colormap_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/line_colormap_dashed.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/line_colormap_solid.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/scatter.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/plot/scatter_colormap.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/style/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/style/graph.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/style/label.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/style/legend.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/style/legend_colormap.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/style/ticks.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/legacy/style/title.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/root_legacy.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_compat/shim.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_config/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_config/loader.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_config/model.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_config/schema.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/errors.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/numerics.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/options.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_core/validation.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_figure/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_figure/backend.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_figure/inset.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_io/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_io/arrays.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_io/build.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_io/metadata.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_io/paths.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_plot/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_plot/colormap.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_style/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/_style/themes.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/base/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/base/base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/base/base_alias_validator.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/color/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/color/colormap.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/config/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/config/config.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/data/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/data/load_file.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/axes.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/axes_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/axes_inset.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/axes_range_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/figure_tools.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/show.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/figure/store.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/hello_world/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/hello_world/hello_world.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/logger.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/path/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/path/path.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/line.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/line_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/line_colormap_base.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/line_colormap_dashed.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/line_colormap_solid.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/scatter.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/plot/scatter_colormap.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/py.typed +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/style/__init__.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/style/graph.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/style/label.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/style/legend.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/style/legend_colormap.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/style/ticks.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/style/title.py +0 -0
- {gsplot-0.4.0 → gsplot-0.4.1}/src/gsplot/version.py +0 -0
|
@@ -75,6 +75,7 @@ if TYPE_CHECKING:
|
|
|
75
75
|
square,
|
|
76
76
|
style_axes,
|
|
77
77
|
suptitle,
|
|
78
|
+
ticks,
|
|
78
79
|
title,
|
|
79
80
|
)
|
|
80
81
|
from ._style.legends import cmap_legend, legend, legend_entries, legends
|
|
@@ -139,6 +140,7 @@ __all__ = [
|
|
|
139
140
|
"title",
|
|
140
141
|
"suptitle",
|
|
141
142
|
"minor_ticks",
|
|
143
|
+
"ticks",
|
|
142
144
|
"box_aspect",
|
|
143
145
|
"panel_labels",
|
|
144
146
|
"index",
|
|
@@ -60,6 +60,7 @@ from ._style.axes import minor_ticks as minor_ticks
|
|
|
60
60
|
from ._style.axes import square as square
|
|
61
61
|
from ._style.axes import style_axes as style_axes
|
|
62
62
|
from ._style.axes import suptitle as suptitle
|
|
63
|
+
from ._style.axes import ticks as ticks
|
|
63
64
|
from ._style.axes import title as title
|
|
64
65
|
from ._style.legends import cmap_legend as cmap_legend
|
|
65
66
|
from ._style.legends import legend as legend
|
|
@@ -129,6 +130,7 @@ __all__ = [
|
|
|
129
130
|
"title",
|
|
130
131
|
"suptitle",
|
|
131
132
|
"minor_ticks",
|
|
133
|
+
"ticks",
|
|
132
134
|
"box_aspect",
|
|
133
135
|
"panel_labels",
|
|
134
136
|
"index",
|
|
@@ -20,6 +20,7 @@ from typing import Any, Literal, cast
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
from matplotlib import ticker
|
|
22
22
|
from matplotlib.axes import Axes
|
|
23
|
+
from matplotlib.axes._base import _AxesBase
|
|
23
24
|
from matplotlib.figure import Figure
|
|
24
25
|
from mpl_toolkits.axes_grid1.inset_locator import inset_axes as _mpl_inset_axes
|
|
25
26
|
from numpy.typing import ArrayLike, NDArray
|
|
@@ -310,16 +311,21 @@ def _legacy_limits(
|
|
|
310
311
|
if value is None:
|
|
311
312
|
return None, "linear", None, None
|
|
312
313
|
if isinstance(value, (str, bytes)):
|
|
314
|
+
if value in ("", "*"):
|
|
315
|
+
return None, "linear", None, None
|
|
313
316
|
raise LayoutError("legacy limits must be a finite sequence")
|
|
314
317
|
values = tuple(value)
|
|
315
318
|
if len(values) < 2:
|
|
316
319
|
raise LayoutError("legacy limits must contain two values")
|
|
317
|
-
|
|
318
|
-
limits =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
if values[0] in (None, "", "*") and values[1] in (None, "", "*"):
|
|
321
|
+
limits = None
|
|
322
|
+
else:
|
|
323
|
+
try:
|
|
324
|
+
limits = (float(cast(Any, values[0])), float(cast(Any, values[1])))
|
|
325
|
+
except (TypeError, ValueError) as exc:
|
|
326
|
+
raise LayoutError("legacy limits must contain finite values") from exc
|
|
327
|
+
if not np.all(np.isfinite(limits)) or limits[0] == limits[1]:
|
|
328
|
+
raise LayoutError("legacy limits must contain finite unequal values")
|
|
323
329
|
scale = values[2] if len(values) > 2 and isinstance(values[2], str) else "linear"
|
|
324
330
|
if scale not in {"linear", "log", "symlog", "logit"}:
|
|
325
331
|
raise LayoutError(f"unsupported legacy scale: {scale!r}")
|
|
@@ -835,25 +841,25 @@ def legend_colormap(
|
|
|
835
841
|
)
|
|
836
842
|
|
|
837
843
|
|
|
838
|
-
def ticks_off(ax: Axes, mode: str = "xy") -> None:
|
|
844
|
+
def ticks_off(ax: Axes | _AxesBase, mode: str = "xy") -> None:
|
|
839
845
|
"""Adapt legacy minor-tick disabling to the canonical selector."""
|
|
840
846
|
|
|
841
847
|
_warn("ticks_off", "minor_ticks")
|
|
842
848
|
selected = {"xy": "both"}.get(mode, mode)
|
|
843
849
|
_minor_ticks(
|
|
844
|
-
ax,
|
|
850
|
+
cast(Any, ax),
|
|
845
851
|
False,
|
|
846
852
|
axis=cast(Literal["x", "y", "both"], selected),
|
|
847
853
|
)
|
|
848
854
|
|
|
849
855
|
|
|
850
|
-
def ticks_on(ax: Axes, mode: str = "xy") -> None:
|
|
856
|
+
def ticks_on(ax: Axes | _AxesBase, mode: str = "xy") -> None:
|
|
851
857
|
"""Adapt legacy minor-tick enabling to the canonical selector."""
|
|
852
858
|
|
|
853
859
|
_warn("ticks_on", "minor_ticks")
|
|
854
860
|
selected = {"xy": "both"}.get(mode, mode)
|
|
855
861
|
_minor_ticks(
|
|
856
|
-
ax,
|
|
862
|
+
cast(Any, ax),
|
|
857
863
|
True,
|
|
858
864
|
axis=cast(Literal["x", "y", "both"], selected),
|
|
859
865
|
)
|
|
@@ -86,6 +86,7 @@ _CANONICAL_EXPORTS: Final[dict[str, tuple[str, str]]] = {
|
|
|
86
86
|
"title": ("gsplot._compat.root_api", "title"),
|
|
87
87
|
"suptitle": ("gsplot._style.axes", "suptitle"),
|
|
88
88
|
"minor_ticks": ("gsplot._style.axes", "minor_ticks"),
|
|
89
|
+
"ticks": ("gsplot._style.axes", "ticks"),
|
|
89
90
|
"box_aspect": ("gsplot._style.axes", "box_aspect"),
|
|
90
91
|
"panel_labels": ("gsplot._style.panels", "panel_labels"),
|
|
91
92
|
"index": ("gsplot._style.panels", "index"),
|
|
@@ -15,6 +15,7 @@ from typing import Any, get_type_hints
|
|
|
15
15
|
from weakref import WeakKeyDictionary
|
|
16
16
|
|
|
17
17
|
from matplotlib.axes import Axes
|
|
18
|
+
from matplotlib.axes._base import _AxesBase
|
|
18
19
|
from matplotlib.figure import Figure
|
|
19
20
|
from numpy.typing import ArrayLike
|
|
20
21
|
|
|
@@ -32,7 +33,7 @@ from .._style.legends import legend as _legend
|
|
|
32
33
|
from .._style.paper import PAPER_CYCLE_RGBA as _LEGACY_COLORS
|
|
33
34
|
|
|
34
35
|
_UNSET = object()
|
|
35
|
-
_LEGACY_PLOT_COUNTS: WeakKeyDictionary[Axes, int] = WeakKeyDictionary()
|
|
36
|
+
_LEGACY_PLOT_COUNTS: WeakKeyDictionary[Axes | _AxesBase, int] = WeakKeyDictionary()
|
|
36
37
|
|
|
37
38
|
_LEGACY_LEGEND_KEYS = {
|
|
38
39
|
"handlers",
|
|
@@ -389,7 +390,7 @@ def scatter(
|
|
|
389
390
|
def _legacy_label_records(value: Any) -> tuple[Any, ...] | None:
|
|
390
391
|
"""Recognize non-empty historical records without reading pyplot state."""
|
|
391
392
|
|
|
392
|
-
if isinstance(value, (str, bytes, Mapping, Axes)):
|
|
393
|
+
if isinstance(value, (str, bytes, Mapping, Axes, _AxesBase)):
|
|
393
394
|
return None
|
|
394
395
|
try:
|
|
395
396
|
records = tuple(value)
|
|
@@ -430,6 +431,14 @@ def label(
|
|
|
430
431
|
pad: Any = _UNSET,
|
|
431
432
|
xpad: Any = _UNSET,
|
|
432
433
|
ypad: Any = _UNSET,
|
|
434
|
+
margin: Any = _UNSET,
|
|
435
|
+
xmargin: Any = _UNSET,
|
|
436
|
+
ymargin: Any = _UNSET,
|
|
437
|
+
top: Any = _UNSET,
|
|
438
|
+
bottom: Any = _UNSET,
|
|
439
|
+
left: Any = _UNSET,
|
|
440
|
+
right: Any = _UNSET,
|
|
441
|
+
direction: Any = _UNSET,
|
|
433
442
|
square: Any = _UNSET,
|
|
434
443
|
index: Any = _UNSET,
|
|
435
444
|
lab_lims: Any = _UNSET,
|
|
@@ -536,6 +545,14 @@ def label(
|
|
|
536
545
|
"pad": pad,
|
|
537
546
|
"xpad": xpad,
|
|
538
547
|
"ypad": ypad,
|
|
548
|
+
"margin": margin,
|
|
549
|
+
"xmargin": xmargin,
|
|
550
|
+
"ymargin": ymargin,
|
|
551
|
+
"top": top,
|
|
552
|
+
"bottom": bottom,
|
|
553
|
+
"left": left,
|
|
554
|
+
"right": right,
|
|
555
|
+
"direction": direction,
|
|
539
556
|
"square": square,
|
|
540
557
|
"index": index,
|
|
541
558
|
}
|
|
@@ -559,6 +576,14 @@ def label(
|
|
|
559
576
|
"pad": pad,
|
|
560
577
|
"xpad": xpad,
|
|
561
578
|
"ypad": ypad,
|
|
579
|
+
"margin": margin,
|
|
580
|
+
"xmargin": xmargin,
|
|
581
|
+
"ymargin": ymargin,
|
|
582
|
+
"top": top,
|
|
583
|
+
"bottom": bottom,
|
|
584
|
+
"left": left,
|
|
585
|
+
"right": right,
|
|
586
|
+
"direction": direction,
|
|
562
587
|
"square": square,
|
|
563
588
|
"index": index,
|
|
564
589
|
}
|
|
@@ -677,11 +702,41 @@ def legend(
|
|
|
677
702
|
) -> Any:
|
|
678
703
|
"""Dispatch canonical ``legend`` or finite legacy options."""
|
|
679
704
|
|
|
680
|
-
|
|
705
|
+
is_legacy = (
|
|
706
|
+
legacy_handles is not _UNSET
|
|
707
|
+
or legacy_labels is not _UNSET
|
|
708
|
+
or handlers is not _UNSET
|
|
709
|
+
or ncol is not _UNSET
|
|
710
|
+
)
|
|
711
|
+
if is_legacy:
|
|
712
|
+
if (legacy_handles is not _UNSET or legacy_labels is not _UNSET) and (
|
|
713
|
+
handles is not None or labels is not None
|
|
714
|
+
):
|
|
715
|
+
raise OptionError("legend cannot combine positional and canonical entries")
|
|
716
|
+
if handlers is not _UNSET:
|
|
717
|
+
if handler_map is not None:
|
|
718
|
+
raise OptionError("legend cannot combine handlers and handler_map")
|
|
719
|
+
handler_map = handlers
|
|
720
|
+
if ncol is not _UNSET:
|
|
721
|
+
if ncols is not _UNSET:
|
|
722
|
+
raise OptionError(
|
|
723
|
+
"legend: props cannot contain both 'ncol' and 'ncols'"
|
|
724
|
+
)
|
|
725
|
+
ncols = ncol
|
|
726
|
+
if legacy_handles is not _UNSET:
|
|
727
|
+
handles = legacy_handles
|
|
728
|
+
if legacy_labels is not _UNSET:
|
|
729
|
+
labels = legacy_labels
|
|
730
|
+
_warn("legend")
|
|
731
|
+
|
|
732
|
+
direct = _provided(
|
|
681
733
|
{
|
|
682
|
-
"
|
|
734
|
+
"loc": loc,
|
|
735
|
+
"frameon": frameon,
|
|
736
|
+
"fancybox": fancybox,
|
|
737
|
+
"labelspacing": labelspacing,
|
|
738
|
+
"handlelength": handlelength,
|
|
683
739
|
"ncols": ncols,
|
|
684
|
-
"ncol": ncol,
|
|
685
740
|
"fontsize": fontsize,
|
|
686
741
|
"title": title,
|
|
687
742
|
"title_fontsize": title_fontsize,
|
|
@@ -702,59 +757,6 @@ def legend(
|
|
|
702
757
|
"labelcolor": labelcolor,
|
|
703
758
|
}
|
|
704
759
|
)
|
|
705
|
-
if legacy_handles is not _UNSET:
|
|
706
|
-
legacy["_legacy_handles"] = legacy_handles
|
|
707
|
-
if legacy_labels is not _UNSET:
|
|
708
|
-
legacy["_legacy_labels"] = legacy_labels
|
|
709
|
-
if (legacy_handles is not _UNSET or legacy_labels is not _UNSET) and (
|
|
710
|
-
handles is not None or labels is not None
|
|
711
|
-
):
|
|
712
|
-
raise OptionError("legend cannot combine positional and canonical entries")
|
|
713
|
-
if legacy and props is not None:
|
|
714
|
-
raise OptionError(
|
|
715
|
-
"gsplot.legend cannot combine canonical props with legacy options"
|
|
716
|
-
)
|
|
717
|
-
direct = _provided(
|
|
718
|
-
{
|
|
719
|
-
"loc": loc,
|
|
720
|
-
"frameon": frameon,
|
|
721
|
-
"fancybox": fancybox,
|
|
722
|
-
"labelspacing": labelspacing,
|
|
723
|
-
"handlelength": handlelength,
|
|
724
|
-
}
|
|
725
|
-
)
|
|
726
|
-
if not legacy:
|
|
727
|
-
return _legend(
|
|
728
|
-
target,
|
|
729
|
-
handles=handles,
|
|
730
|
-
labels=labels,
|
|
731
|
-
handler_map=handler_map,
|
|
732
|
-
reverse=reverse,
|
|
733
|
-
replace=replace,
|
|
734
|
-
props=props,
|
|
735
|
-
**direct,
|
|
736
|
-
)
|
|
737
|
-
positional_handles = legacy.pop("_legacy_handles", _UNSET)
|
|
738
|
-
positional_labels = legacy.pop("_legacy_labels", _UNSET)
|
|
739
|
-
if handlers is not _UNSET:
|
|
740
|
-
if handler_map is not None:
|
|
741
|
-
raise OptionError("legend cannot combine handlers and handler_map")
|
|
742
|
-
handler_map = handlers
|
|
743
|
-
legacy.pop("handlers", None)
|
|
744
|
-
translated = _translate_props(
|
|
745
|
-
"legend",
|
|
746
|
-
legacy,
|
|
747
|
-
{"ncol": "ncols"},
|
|
748
|
-
)
|
|
749
|
-
if positional_handles is not _UNSET:
|
|
750
|
-
if handles is not None:
|
|
751
|
-
raise OptionError("legend received handles twice")
|
|
752
|
-
handles = positional_handles
|
|
753
|
-
if positional_labels is not _UNSET:
|
|
754
|
-
if labels is not None:
|
|
755
|
-
raise OptionError("legend received labels twice")
|
|
756
|
-
labels = positional_labels
|
|
757
|
-
_warn("legend")
|
|
758
760
|
return _legend(
|
|
759
761
|
target,
|
|
760
762
|
handles=handles,
|
|
@@ -762,7 +764,7 @@ def legend(
|
|
|
762
764
|
handler_map=handler_map,
|
|
763
765
|
reverse=reverse,
|
|
764
766
|
replace=replace,
|
|
765
|
-
props=
|
|
767
|
+
props=props,
|
|
766
768
|
**direct,
|
|
767
769
|
)
|
|
768
770
|
|
|
@@ -834,10 +836,10 @@ def title(
|
|
|
834
836
|
"y": y,
|
|
835
837
|
}
|
|
836
838
|
)
|
|
837
|
-
if isinstance(ax, Axes):
|
|
838
|
-
if text is _UNSET or title is not _UNSET
|
|
839
|
-
raise OptionError("canonical title requires text
|
|
840
|
-
return _title(ax, text, props=props)
|
|
839
|
+
if isinstance(ax, (Axes, _AxesBase)):
|
|
840
|
+
if text is _UNSET or title is not _UNSET:
|
|
841
|
+
raise OptionError("canonical title requires an explicit text value")
|
|
842
|
+
return _title(ax, text, props=props, **legacy)
|
|
841
843
|
if text is not _UNSET:
|
|
842
844
|
raise TypeError("legacy Figure title accepts one text value")
|
|
843
845
|
if title is not _UNSET:
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from typing import Any, Generic, Literal, TypeVar
|
|
8
8
|
|
|
9
9
|
from matplotlib.axes import Axes
|
|
10
|
+
from matplotlib.axes._base import _AxesBase
|
|
10
11
|
from matplotlib.figure import Figure
|
|
11
12
|
|
|
12
13
|
from .errors import PlotError
|
|
@@ -24,7 +25,7 @@ def _operation_name(value: str) -> str:
|
|
|
24
25
|
return value
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def _axis_root_figure(axis:
|
|
28
|
+
def _axis_root_figure(axis: Any) -> Figure | None:
|
|
28
29
|
"""Return an Axes root Figure without requiring ``root=True`` support."""
|
|
29
30
|
|
|
30
31
|
owner: Any = axis.get_figure()
|
|
@@ -47,7 +48,7 @@ class TargetPlan:
|
|
|
47
48
|
|
|
48
49
|
operation: str
|
|
49
50
|
figure: Figure
|
|
50
|
-
axes: tuple[Axes, ...]
|
|
51
|
+
axes: tuple[Axes | _AxesBase, ...]
|
|
51
52
|
keys: tuple[object, ...]
|
|
52
53
|
kind: TargetKind
|
|
53
54
|
|
|
@@ -61,7 +62,7 @@ class TargetPlan:
|
|
|
61
62
|
raise PlotError(f"{self.operation}: target plan is empty or incomplete")
|
|
62
63
|
if self.kind not in {"single", "sequence", "array", "mapping"}:
|
|
63
64
|
raise PlotError(f"{self.operation}: target kind is invalid")
|
|
64
|
-
if any(not isinstance(axis, Axes) for axis in self.axes):
|
|
65
|
+
if any(not isinstance(axis, (Axes, _AxesBase)) for axis in self.axes):
|
|
65
66
|
raise PlotError(f"{self.operation}: target contains a non-Axes value")
|
|
66
67
|
if len({id(axis) for axis in self.axes}) != len(self.axes):
|
|
67
68
|
raise PlotError(f"{self.operation}: target contains a duplicate Axes")
|
|
@@ -7,6 +7,7 @@ from typing import Any, TypeVar
|
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
from matplotlib.axes import Axes
|
|
10
|
+
from matplotlib.axes._base import _AxesBase
|
|
10
11
|
from matplotlib.figure import Figure
|
|
11
12
|
|
|
12
13
|
from .errors import PlotError
|
|
@@ -16,7 +17,7 @@ from .types import AxesTarget
|
|
|
16
17
|
T = TypeVar("T")
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def _root_figure(axis: Axes, operation: str) -> Figure:
|
|
20
|
+
def _root_figure(axis: Axes | _AxesBase, operation: str) -> Figure:
|
|
20
21
|
"""Resolve an Axes root Figure without version-specific root arguments."""
|
|
21
22
|
|
|
22
23
|
owner = _axis_root_figure(axis)
|
|
@@ -30,7 +31,7 @@ def _snapshot_target(
|
|
|
30
31
|
) -> tuple[TargetKind, tuple[object, ...], tuple[Any, ...]]:
|
|
31
32
|
"""Snapshot one supported target shape without retaining its container."""
|
|
32
33
|
|
|
33
|
-
if isinstance(target, Axes):
|
|
34
|
+
if isinstance(target, (Axes, _AxesBase)):
|
|
34
35
|
return "single", (target,), (target,)
|
|
35
36
|
if isinstance(target, Mapping):
|
|
36
37
|
items = tuple(target.items())
|
|
@@ -67,7 +68,7 @@ def normalize_axes(target: AxesTarget, *, operation: str) -> TargetPlan:
|
|
|
67
68
|
kind, keys, values = _snapshot_target(target, operation)
|
|
68
69
|
if not values:
|
|
69
70
|
raise PlotError(f"{operation}: target must contain at least one Axes")
|
|
70
|
-
if any(not isinstance(value, Axes) for value in values):
|
|
71
|
+
if any(not isinstance(value, (Axes, _AxesBase)) for value in values):
|
|
71
72
|
raise PlotError(f"{operation}: target contains a non-Axes value")
|
|
72
73
|
axes = tuple(values)
|
|
73
74
|
if len({id(axis) for axis in axes}) != len(axes):
|
|
@@ -11,6 +11,7 @@ from typing import Any, Literal, Protocol, TypeAlias, cast, overload
|
|
|
11
11
|
|
|
12
12
|
from matplotlib.artist import Artist
|
|
13
13
|
from matplotlib.axes import Axes
|
|
14
|
+
from matplotlib.axes._base import _AxesBase
|
|
14
15
|
from matplotlib.colors import is_color_like
|
|
15
16
|
from matplotlib.legend_handler import HandlerBase
|
|
16
17
|
from matplotlib.typing import LineStyleType, MarkerType
|
|
@@ -25,7 +26,13 @@ ColorSpec: TypeAlias = str | RGBColor
|
|
|
25
26
|
# Public type alias: MosaicSpec; a Matplotlib mosaic string or label rows.
|
|
26
27
|
MosaicSpec: TypeAlias = str | Sequence[Sequence[str | None]]
|
|
27
28
|
# Public type alias: AxesTarget; one Axes or a deterministic finite collection.
|
|
28
|
-
AxesTarget: TypeAlias =
|
|
29
|
+
AxesTarget: TypeAlias = (
|
|
30
|
+
Axes
|
|
31
|
+
| _AxesBase
|
|
32
|
+
| Sequence[Axes | _AxesBase]
|
|
33
|
+
| Mapping[Any, Axes | _AxesBase]
|
|
34
|
+
| NDArray[Any]
|
|
35
|
+
)
|
|
29
36
|
# Public type alias: PerTarget; ordered or exact-key per-target values.
|
|
30
37
|
PerTarget: TypeAlias = Sequence[Any] | Mapping[Any, Any]
|
|
31
38
|
# Public type alias: LineStyle; a named style or finite dash-tuple form.
|
|
@@ -45,7 +52,9 @@ StyleMode: TypeAlias = Literal["auto", "paper"] | None
|
|
|
45
52
|
# Public type alias: ZoomCorners; two explicit parent/inset connector pairs.
|
|
46
53
|
ZoomCorners: TypeAlias = tuple[tuple[int, int], tuple[int, int]]
|
|
47
54
|
# Public type alias: Limit; finite two-value axis limits after validation.
|
|
48
|
-
Limit: TypeAlias =
|
|
55
|
+
Limit: TypeAlias = (
|
|
56
|
+
tuple[float | str | None, float | str | None] | Sequence[float | str | None]
|
|
57
|
+
)
|
|
49
58
|
# Public type alias: Scale; supported Cartesian scale names.
|
|
50
59
|
Scale: TypeAlias = Literal["linear", "log", "symlog", "logit"]
|
|
51
60
|
# Public type alias: TickSpec; finite numeric tick locations after validation.
|
|
@@ -204,6 +213,7 @@ NormalizeSpec: TypeAlias = tuple[float, float] | _NormalizeProtocol
|
|
|
204
213
|
|
|
205
214
|
|
|
206
215
|
_SCALES = {"linear", "log", "symlog", "logit"}
|
|
216
|
+
_DIRECTIONS = {"in", "out", "inout"}
|
|
207
217
|
_LOCATIONS = {
|
|
208
218
|
"upper right",
|
|
209
219
|
"upper left",
|
|
@@ -273,13 +283,13 @@ def _color(value: Any, name: str) -> ColorSpec:
|
|
|
273
283
|
return cast(tuple[float, float, float, float], channels)
|
|
274
284
|
|
|
275
285
|
|
|
276
|
-
def _limits(value: Any, name: str) -> tuple[float, float] | None:
|
|
277
|
-
"""Validate finite
|
|
286
|
+
def _limits(value: Any, name: str) -> tuple[float | None, float | None] | None:
|
|
287
|
+
"""Validate finite limits or partial limits while preserving their order."""
|
|
278
288
|
|
|
279
289
|
if value is None:
|
|
280
290
|
return None
|
|
281
291
|
if isinstance(value, (str, bytes)):
|
|
282
|
-
if value
|
|
292
|
+
if value in ("", "*"):
|
|
283
293
|
return None
|
|
284
294
|
raise LayoutError(f"{name} must contain exactly two finite values")
|
|
285
295
|
try:
|
|
@@ -288,15 +298,19 @@ def _limits(value: Any, name: str) -> tuple[float, float] | None:
|
|
|
288
298
|
raise LayoutError(f"{name} must contain exactly two finite values") from exc
|
|
289
299
|
if len(values) != 2:
|
|
290
300
|
raise LayoutError(f"{name} must contain exactly two finite values")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
_finite(values[0], f"{name}[0]", LayoutError)
|
|
295
|
-
_finite(values[1], f"{name}[1]", LayoutError),
|
|
301
|
+
low = (
|
|
302
|
+
None
|
|
303
|
+
if values[0] in (None, "", "*")
|
|
304
|
+
else _finite(values[0], f"{name}[0]", LayoutError)
|
|
296
305
|
)
|
|
297
|
-
|
|
306
|
+
high = (
|
|
307
|
+
None
|
|
308
|
+
if values[1] in (None, "", "*")
|
|
309
|
+
else _finite(values[1], f"{name}[1]", LayoutError)
|
|
310
|
+
)
|
|
311
|
+
if low is not None and high is not None and low == high:
|
|
298
312
|
raise LayoutError(f"{name} values must not be equal")
|
|
299
|
-
return
|
|
313
|
+
return (low, high)
|
|
300
314
|
|
|
301
315
|
|
|
302
316
|
def _ticks(value: Any, name: str) -> tuple[float, ...] | None:
|
|
@@ -322,6 +336,22 @@ def _optional_finite(value: Any, name: str) -> float | None:
|
|
|
322
336
|
return None if value is None else _finite(value, name, LayoutError)
|
|
323
337
|
|
|
324
338
|
|
|
339
|
+
def _nonnegative_finite(value: Any, name: str, default: float = 0.05) -> float:
|
|
340
|
+
"""Validate a non-negative finite margin value."""
|
|
341
|
+
|
|
342
|
+
if value is None:
|
|
343
|
+
return default
|
|
344
|
+
if isinstance(value, bool):
|
|
345
|
+
raise LayoutError(f"{name} must be non-negative")
|
|
346
|
+
try:
|
|
347
|
+
num = float(value)
|
|
348
|
+
except (TypeError, ValueError) as exc:
|
|
349
|
+
raise LayoutError(f"{name} must be non-negative") from exc
|
|
350
|
+
if not math.isfinite(num) or num < 0:
|
|
351
|
+
raise LayoutError(f"{name} must be non-negative")
|
|
352
|
+
return num
|
|
353
|
+
|
|
354
|
+
|
|
325
355
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
326
356
|
class AxisSpec:
|
|
327
357
|
"""Immutable Cartesian labels, limits, scales, ticks, and padding.
|
|
@@ -340,6 +370,12 @@ class AxisSpec:
|
|
|
340
370
|
Optional minor-tick enable flags.
|
|
341
371
|
xlabelpad, ylabelpad
|
|
342
372
|
Optional finite label padding values.
|
|
373
|
+
xmargin, ymargin
|
|
374
|
+
Optional non-negative margin ratios for automatic endpoints.
|
|
375
|
+
top, bottom, left, right
|
|
376
|
+
Optional boolean flags for edge tick and label visibility.
|
|
377
|
+
direction
|
|
378
|
+
Optional tick direction: ``"in"``, ``"out"``, or ``"inout"``.
|
|
343
379
|
|
|
344
380
|
Notes
|
|
345
381
|
-----
|
|
@@ -349,15 +385,17 @@ class AxisSpec:
|
|
|
349
385
|
Examples
|
|
350
386
|
--------
|
|
351
387
|
>>> import gsplot as gs
|
|
352
|
-
>>> spec = gs.AxisSpec(xlabel="time", xscale="linear")
|
|
388
|
+
>>> spec = gs.AxisSpec(xlabel="time", xscale="linear", right=False)
|
|
353
389
|
>>> spec.xlabel
|
|
354
390
|
'time'
|
|
391
|
+
>>> spec.right
|
|
392
|
+
False
|
|
355
393
|
"""
|
|
356
394
|
|
|
357
395
|
xlabel: str | None = None
|
|
358
396
|
ylabel: str | None = None
|
|
359
|
-
xlim:
|
|
360
|
-
ylim:
|
|
397
|
+
xlim: Limit | None = None
|
|
398
|
+
ylim: Limit | None = None
|
|
361
399
|
xscale: Literal["linear", "log", "symlog", "logit"] = "linear"
|
|
362
400
|
yscale: Literal["linear", "log", "symlog", "logit"] = "linear"
|
|
363
401
|
xticks: tuple[float, ...] | None = None
|
|
@@ -366,6 +404,13 @@ class AxisSpec:
|
|
|
366
404
|
yminor: bool | None = None
|
|
367
405
|
xlabelpad: float | None = None
|
|
368
406
|
ylabelpad: float | None = None
|
|
407
|
+
xmargin: float = 0.05
|
|
408
|
+
ymargin: float = 0.05
|
|
409
|
+
top: bool | None = None
|
|
410
|
+
bottom: bool | None = None
|
|
411
|
+
left: bool | None = None
|
|
412
|
+
right: bool | None = None
|
|
413
|
+
direction: Literal["in", "out", "inout"] | None = None
|
|
369
414
|
|
|
370
415
|
def __post_init__(self) -> None:
|
|
371
416
|
"""Validate every field and normalize sequence-like inputs."""
|
|
@@ -394,6 +439,25 @@ class AxisSpec:
|
|
|
394
439
|
object.__setattr__(
|
|
395
440
|
self, "ylabelpad", _optional_finite(self.ylabelpad, "ylabelpad")
|
|
396
441
|
)
|
|
442
|
+
object.__setattr__(
|
|
443
|
+
self, "xmargin", _nonnegative_finite(self.xmargin, "xmargin")
|
|
444
|
+
)
|
|
445
|
+
object.__setattr__(
|
|
446
|
+
self, "ymargin", _nonnegative_finite(self.ymargin, "ymargin")
|
|
447
|
+
)
|
|
448
|
+
for edge_name, edge_val in (
|
|
449
|
+
("top", self.top),
|
|
450
|
+
("bottom", self.bottom),
|
|
451
|
+
("left", self.left),
|
|
452
|
+
("right", self.right),
|
|
453
|
+
):
|
|
454
|
+
if edge_val is not None and not isinstance(edge_val, bool):
|
|
455
|
+
raise LayoutError(f"{edge_name} must be a boolean or None")
|
|
456
|
+
if self.direction is not None:
|
|
457
|
+
if not isinstance(self.direction, str) or self.direction not in _DIRECTIONS:
|
|
458
|
+
raise LayoutError(
|
|
459
|
+
f"direction must be one of: {', '.join(sorted(_DIRECTIONS))}"
|
|
460
|
+
)
|
|
397
461
|
|
|
398
462
|
|
|
399
463
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
@@ -531,21 +531,12 @@ def _layout_kind(figure: Figure) -> str:
|
|
|
531
531
|
def _validate_reuse(
|
|
532
532
|
figure: Figure,
|
|
533
533
|
*,
|
|
534
|
-
size: SizeSpec,
|
|
535
|
-
size_inches: tuple[float, float] | None,
|
|
536
534
|
layout: LayoutMode,
|
|
535
|
+
clear: bool = False,
|
|
537
536
|
) -> None:
|
|
538
537
|
"""Reject incompatible Figure reuse before clearing or adding Axes."""
|
|
539
538
|
|
|
540
|
-
if
|
|
541
|
-
size not in {"auto", None}
|
|
542
|
-
and size_inches is not None
|
|
543
|
-
and not np.allclose(figure.get_size_inches(), size_inches, rtol=0, atol=1e-9)
|
|
544
|
-
):
|
|
545
|
-
raise LayoutError(
|
|
546
|
-
"requested size does not match the reused Figure; omit size to preserve it"
|
|
547
|
-
)
|
|
548
|
-
if layout == "auto":
|
|
539
|
+
if layout == "auto" or clear:
|
|
549
540
|
return
|
|
550
541
|
current = _layout_kind(figure)
|
|
551
542
|
if current != layout and not (
|
|
@@ -854,9 +845,8 @@ def subplots(
|
|
|
854
845
|
if resolved_fig is not None:
|
|
855
846
|
_validate_reuse(
|
|
856
847
|
resolved_fig,
|
|
857
|
-
size=options["size"],
|
|
858
|
-
size_inches=size_inches,
|
|
859
848
|
layout=options["layout"],
|
|
849
|
+
clear=selected_clear,
|
|
860
850
|
)
|
|
861
851
|
|
|
862
852
|
new_figure = resolved_fig is None
|
|
@@ -872,6 +862,8 @@ def subplots(
|
|
|
872
862
|
)
|
|
873
863
|
else:
|
|
874
864
|
target = cast(Figure, resolved_fig)
|
|
865
|
+
if size_inches is not None:
|
|
866
|
+
target.set_size_inches(size_inches, forward=True)
|
|
875
867
|
if selected_clear:
|
|
876
868
|
target.clear()
|
|
877
869
|
target.set_layout_engine(
|
|
@@ -524,24 +524,28 @@ def _formats(
|
|
|
524
524
|
return selected, destinations
|
|
525
525
|
|
|
526
526
|
|
|
527
|
-
def _save_props(
|
|
527
|
+
def _save_props(
|
|
528
|
+
props: Mapping[str, Any] | None,
|
|
529
|
+
kwargs: Mapping[str, Any] | None = None,
|
|
530
|
+
) -> dict[str, Any]:
|
|
528
531
|
"""Validate a closed Matplotlib savefig property mapping."""
|
|
529
532
|
|
|
530
|
-
if props is None:
|
|
531
|
-
return {}
|
|
532
|
-
if not isinstance(props, Mapping):
|
|
533
|
+
if props is not None and not isinstance(props, Mapping):
|
|
533
534
|
raise OutputError("props must be a mapping")
|
|
534
|
-
|
|
535
|
+
merged = dict(props or {})
|
|
536
|
+
if kwargs:
|
|
537
|
+
merged.update(kwargs)
|
|
538
|
+
if any(not isinstance(key, str) for key in merged):
|
|
535
539
|
raise OutputError("props keys must be strings")
|
|
536
|
-
duplicate = sorted(set(
|
|
540
|
+
duplicate = sorted(set(merged) & _CONTROLLED_PROPS)
|
|
537
541
|
if duplicate:
|
|
538
542
|
joined = ", ".join(repr(key) for key in duplicate)
|
|
539
543
|
raise TypeError(f"props cannot contain gsplot-controlled key(s): {joined}")
|
|
540
|
-
unknown = sorted(set(
|
|
544
|
+
unknown = sorted(set(merged) - _SAVEFIG_PROPS)
|
|
541
545
|
if unknown:
|
|
542
546
|
joined = ", ".join(repr(key) for key in unknown)
|
|
543
547
|
raise OptionError(f"savefig props contains unknown key(s): {joined}")
|
|
544
|
-
return
|
|
548
|
+
return merged
|
|
545
549
|
|
|
546
550
|
|
|
547
551
|
def _is_interactive_figure(figure: Figure) -> bool:
|
|
@@ -606,6 +610,7 @@ def savefig(
|
|
|
606
610
|
overwrite: bool = False,
|
|
607
611
|
show: bool = True,
|
|
608
612
|
props: Mapping[str, Any] | None = None,
|
|
613
|
+
**kwargs: Any,
|
|
609
614
|
) -> tuple[Path, ...]:
|
|
610
615
|
"""Save an explicit Figure and optionally display it after all writes.
|
|
611
616
|
|
|
@@ -626,6 +631,10 @@ def savefig(
|
|
|
626
631
|
props
|
|
627
632
|
A finite mapping of Matplotlib save properties; gsplot controls may
|
|
628
633
|
not be supplied through this mapping.
|
|
634
|
+
**kwargs
|
|
635
|
+
Optional direct Matplotlib save properties (e.g. ``transparent``,
|
|
636
|
+
``facecolor``, ``edgecolor``). Direct keyword arguments are merged with
|
|
637
|
+
and take precedence over ``props``.
|
|
629
638
|
|
|
630
639
|
Returns
|
|
631
640
|
-------
|
|
@@ -665,7 +674,7 @@ def savefig(
|
|
|
665
674
|
if not math.isfinite(dpi_value) or dpi_value <= 0:
|
|
666
675
|
raise OutputError("dpi must be a positive finite number")
|
|
667
676
|
dpi = dpi_value
|
|
668
|
-
selected_props = _save_props(props)
|
|
677
|
+
selected_props = _save_props(props, kwargs)
|
|
669
678
|
destination = _resolved_path(path, "path")
|
|
670
679
|
selected_formats, destinations = _formats(destination, formats)
|
|
671
680
|
parents = {item.parent for item in destinations}
|