gsplot 0.4.1__tar.gz → 0.4.2__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.1 → gsplot-0.4.2}/PKG-INFO +1 -1
- {gsplot-0.4.1 → gsplot-0.4.2}/pyproject.toml +1 -1
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/__init__.py +4 -2
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/__init__.pyi +4 -2
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy_api.py +40 -10
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/root_api.py +33 -22
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/types.py +21 -0
- gsplot-0.4.2/src/gsplot/_figure/fit.py +365 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/layout.py +37 -3
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/output.py +16 -2
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/axes.py +86 -16
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/legends.py +392 -81
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/panels.py +224 -55
- {gsplot-0.4.1 → gsplot-0.4.2}/LICENSE +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/README.md +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/config.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/base/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/base/base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/base/base_alias_validator.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/color/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/color/colormap.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/config/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/config/config.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/data/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/data/load_file.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes_inset.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes_range_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/figure_tools.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/show.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/store.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/hello_world/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/hello_world/hello_world.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/logger.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/path/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/path/path.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_colormap_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_colormap_dashed.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_colormap_solid.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/scatter.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/scatter_colormap.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/graph.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/label.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/legend.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/legend_colormap.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/ticks.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/title.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/root.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/root_legacy.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/shim.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/loader.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/model.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/schema.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/errors.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/numerics.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/options.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/plans.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/targets.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/validation.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/backend.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/inset.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/arrays.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/build.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/metadata.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/paths.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/basic.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/colored.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/colormap.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/series.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/paper.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/themes.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/base/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/base/base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/base/base_alias_validator.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/color/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/color/colormap.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/config/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/config/config.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/data/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/data/load_file.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes_inset.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes_range_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/figure_tools.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/show.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/store.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/hello_world/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/hello_world/hello_world.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/logger.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/path/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/path/path.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_colormap_base.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_colormap_dashed.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_colormap_solid.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/scatter.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/scatter_colormap.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/py.typed +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/__init__.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/graph.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/label.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/legend.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/legend_colormap.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/ticks.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/title.py +0 -0
- {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/version.py +0 -0
|
@@ -7,7 +7,7 @@ configuration, logging, metadata-file, or backend initialization.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from typing import TYPE_CHECKING, Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
11
11
|
|
|
12
12
|
from ._compat.root import canonical_names as _canonical_names
|
|
13
13
|
from ._compat.root import legacy_names as _legacy_names
|
|
@@ -20,6 +20,8 @@ if TYPE_CHECKING:
|
|
|
20
20
|
# Runtime lookup stays lazy. Static analyzers see the canonical source
|
|
21
21
|
# objects instead of an ``Any``-typed dynamic facade, so the shipped
|
|
22
22
|
# ``py.typed`` marker provides useful signatures to downstream callers.
|
|
23
|
+
from matplotlib.legend import Legend as _Legend
|
|
24
|
+
|
|
23
25
|
from ._compat.config import load_config
|
|
24
26
|
from ._config.model import Config
|
|
25
27
|
from ._core.errors import (
|
|
@@ -115,7 +117,7 @@ if TYPE_CHECKING:
|
|
|
115
117
|
legend_handlers: Any
|
|
116
118
|
legend_reverse: Any
|
|
117
119
|
legend_get_handlers: Any
|
|
118
|
-
legend_colormap:
|
|
120
|
+
legend_colormap: Callable[..., _Legend]
|
|
119
121
|
ticks_off: Any
|
|
120
122
|
ticks_on: Any
|
|
121
123
|
ticks_on_axes: Any
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from matplotlib.legend import Legend as _Legend
|
|
4
6
|
|
|
5
7
|
from ._compat.config import load_config as load_config
|
|
6
8
|
from ._config.model import Config as Config
|
|
@@ -105,7 +107,7 @@ legend_axes: Any
|
|
|
105
107
|
legend_handlers: Any
|
|
106
108
|
legend_reverse: Any
|
|
107
109
|
legend_get_handlers: Any
|
|
108
|
-
legend_colormap:
|
|
110
|
+
legend_colormap: Callable[..., _Legend]
|
|
109
111
|
ticks_off: Any
|
|
110
112
|
ticks_on: Any
|
|
111
113
|
ticks_on_axes: Any
|
|
@@ -22,6 +22,7 @@ from matplotlib import ticker
|
|
|
22
22
|
from matplotlib.axes import Axes
|
|
23
23
|
from matplotlib.axes._base import _AxesBase
|
|
24
24
|
from matplotlib.figure import Figure
|
|
25
|
+
from matplotlib.legend import Legend
|
|
25
26
|
from mpl_toolkits.axes_grid1.inset_locator import inset_axes as _mpl_inset_axes
|
|
26
27
|
from numpy.typing import ArrayLike, NDArray
|
|
27
28
|
|
|
@@ -42,7 +43,7 @@ from .._style.axes import box_aspect as _box_aspect
|
|
|
42
43
|
from .._style.axes import minor_ticks as _minor_ticks
|
|
43
44
|
from .._style.axes import style_axes as _style_axes
|
|
44
45
|
from .._style.axes import title as _title
|
|
45
|
-
from .._style.legends import
|
|
46
|
+
from .._style.legends import _create_cmap_legend, _legacy_colormap_values
|
|
46
47
|
from .._style.legends import legend as _legend
|
|
47
48
|
from .._style.legends import legend_entries as _legend_entries
|
|
48
49
|
from .._style.legends import legends as _legends
|
|
@@ -826,18 +827,47 @@ def legend_colormap(
|
|
|
826
827
|
vmax: float = 1,
|
|
827
828
|
reverse: bool = False,
|
|
828
829
|
**props: Any,
|
|
829
|
-
) ->
|
|
830
|
-
"""
|
|
830
|
+
) -> Legend:
|
|
831
|
+
"""Create a v0.2-compatible horizontal gradient Legend.
|
|
832
|
+
|
|
833
|
+
Parameters
|
|
834
|
+
----------
|
|
835
|
+
ax
|
|
836
|
+
Explicit target Axes.
|
|
837
|
+
cmap
|
|
838
|
+
Non-empty registered Matplotlib colormap name.
|
|
839
|
+
label
|
|
840
|
+
Optional gradient label. ``None`` returns an empty Legend.
|
|
841
|
+
num_stripes
|
|
842
|
+
Positive requested stripe count; values above 256 are clamped.
|
|
843
|
+
vmin, vmax
|
|
844
|
+
Finite raw values passed directly to the colormap through
|
|
845
|
+
``linspace(vmin, vmax, N_effective)``. Equal and descending bounds are
|
|
846
|
+
valid and retain their sampled order.
|
|
847
|
+
reverse
|
|
848
|
+
Reverse the final sampled RGBA sequence from left to right.
|
|
849
|
+
**props
|
|
850
|
+
Finite Matplotlib Legend properties. If omitted, Matplotlib's
|
|
851
|
+
call-time rcParams provide the defaults.
|
|
852
|
+
|
|
853
|
+
Notes
|
|
854
|
+
-----
|
|
855
|
+
This deprecated adapter always replaces an existing Legend because the
|
|
856
|
+
historical function has no ``replace`` keyword. Its rendering helper is
|
|
857
|
+
shared with :func:`gsplot.cmap_legend`, but its raw ``vmin``/``vmax``
|
|
858
|
+
sampling is deliberately not translated to canonical ``norm`` semantics.
|
|
859
|
+
"""
|
|
831
860
|
|
|
832
861
|
_warn("legend_colormap", "cmap_legend")
|
|
833
|
-
|
|
862
|
+
colors = _legacy_colormap_values(cmap, num_stripes, vmin, vmax, reverse)
|
|
863
|
+
return _create_cmap_legend(
|
|
834
864
|
ax,
|
|
835
|
-
|
|
836
|
-
label
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
865
|
+
colors,
|
|
866
|
+
label,
|
|
867
|
+
replace=True,
|
|
868
|
+
props=None,
|
|
869
|
+
kwargs=props,
|
|
870
|
+
ambient=True,
|
|
841
871
|
)
|
|
842
872
|
|
|
843
873
|
|
|
@@ -473,6 +473,7 @@ def label(
|
|
|
473
473
|
fontdict: Any = _UNSET,
|
|
474
474
|
labelpad: Any = _UNSET,
|
|
475
475
|
loc: Any = _UNSET,
|
|
476
|
+
props: Any = _UNSET,
|
|
476
477
|
) -> Any:
|
|
477
478
|
"""Dispatch explicit concise labels or historical current-Figure records."""
|
|
478
479
|
|
|
@@ -488,6 +489,34 @@ def label(
|
|
|
488
489
|
records = _legacy_label_records(target)
|
|
489
490
|
else:
|
|
490
491
|
records = None
|
|
492
|
+
text_props = {
|
|
493
|
+
name: value
|
|
494
|
+
for name, value in (
|
|
495
|
+
("alpha", alpha),
|
|
496
|
+
("color", color),
|
|
497
|
+
("fontfamily", fontfamily),
|
|
498
|
+
("fontproperties", fontproperties),
|
|
499
|
+
("fontsize", fontsize),
|
|
500
|
+
("fontstretch", fontstretch),
|
|
501
|
+
("fontstyle", fontstyle),
|
|
502
|
+
("fontvariant", fontvariant),
|
|
503
|
+
("fontweight", fontweight),
|
|
504
|
+
("ha", ha),
|
|
505
|
+
("horizontalalignment", horizontalalignment),
|
|
506
|
+
("label", label),
|
|
507
|
+
("linespacing", linespacing),
|
|
508
|
+
("math_fontfamily", math_fontfamily),
|
|
509
|
+
("multialignment", multialignment),
|
|
510
|
+
("parse_math", parse_math),
|
|
511
|
+
("rotation", rotation),
|
|
512
|
+
("rotation_mode", rotation_mode),
|
|
513
|
+
("va", va),
|
|
514
|
+
("verticalalignment", verticalalignment),
|
|
515
|
+
("visible", visible),
|
|
516
|
+
("zorder", zorder),
|
|
517
|
+
)
|
|
518
|
+
if value is not _UNSET
|
|
519
|
+
}
|
|
491
520
|
legacy_controls = _provided(
|
|
492
521
|
{
|
|
493
522
|
"legacy_xpad_layout": legacy_xpad_layout,
|
|
@@ -498,28 +527,6 @@ def label(
|
|
|
498
527
|
"tight_layout": tight_layout,
|
|
499
528
|
"xpad_layout": xpad_layout,
|
|
500
529
|
"ypad_layout": ypad_layout,
|
|
501
|
-
"alpha": alpha,
|
|
502
|
-
"color": color,
|
|
503
|
-
"fontfamily": fontfamily,
|
|
504
|
-
"fontproperties": fontproperties,
|
|
505
|
-
"fontsize": fontsize,
|
|
506
|
-
"fontstretch": fontstretch,
|
|
507
|
-
"fontstyle": fontstyle,
|
|
508
|
-
"fontvariant": fontvariant,
|
|
509
|
-
"fontweight": fontweight,
|
|
510
|
-
"ha": ha,
|
|
511
|
-
"horizontalalignment": horizontalalignment,
|
|
512
|
-
"label": label,
|
|
513
|
-
"linespacing": linespacing,
|
|
514
|
-
"math_fontfamily": math_fontfamily,
|
|
515
|
-
"multialignment": multialignment,
|
|
516
|
-
"parse_math": parse_math,
|
|
517
|
-
"rotation": rotation,
|
|
518
|
-
"rotation_mode": rotation_mode,
|
|
519
|
-
"va": va,
|
|
520
|
-
"verticalalignment": verticalalignment,
|
|
521
|
-
"visible": visible,
|
|
522
|
-
"zorder": zorder,
|
|
523
530
|
"fontdict": fontdict,
|
|
524
531
|
"labelpad": labelpad,
|
|
525
532
|
"loc": loc,
|
|
@@ -557,6 +564,10 @@ def label(
|
|
|
557
564
|
"index": index,
|
|
558
565
|
}
|
|
559
566
|
)
|
|
567
|
+
if props is not _UNSET:
|
|
568
|
+
canonical["props"] = props
|
|
569
|
+
elif text_props:
|
|
570
|
+
canonical["props"] = text_props
|
|
560
571
|
return _label(target, **canonical)
|
|
561
572
|
|
|
562
573
|
if records is None:
|
|
@@ -63,6 +63,8 @@ TickSpec: TypeAlias = Sequence[float]
|
|
|
63
63
|
LabelRecord: TypeAlias = tuple[str, str] | tuple[str, str, Any, Any] | Sequence[Any]
|
|
64
64
|
# Public type alias: LabelRecords; ordered or exact-key per-target label records.
|
|
65
65
|
LabelRecords: TypeAlias = Sequence[LabelRecord] | Mapping[Any, LabelRecord]
|
|
66
|
+
# Public type alias: TextProps; closed Matplotlib Text property mapping.
|
|
67
|
+
TextProps: TypeAlias = Mapping[str, Any]
|
|
66
68
|
|
|
67
69
|
_PUBLIC_TYPE_ALIAS_DOCS = MappingProxyType(
|
|
68
70
|
{
|
|
@@ -910,6 +912,16 @@ class AxesDict(dict[str, Axes]):
|
|
|
910
912
|
*args, **kwargs
|
|
911
913
|
Dictionary initialization mapping label strings to Matplotlib Axes.
|
|
912
914
|
|
|
915
|
+
Notes
|
|
916
|
+
-----
|
|
917
|
+
Mosaic containers iterate in panel-name (alphabetical) order rather than
|
|
918
|
+
Matplotlib ``subplot_mosaic`` insertion order. ``"ACE;BDE"`` therefore
|
|
919
|
+
iterates as ``('A', 'B', 'C', 'D', 'E')`` even though ``C`` and ``E``
|
|
920
|
+
appear before ``B`` and ``D`` in the specification. Iteration, integer
|
|
921
|
+
indexes, slices, and positional value sequences all follow this order, so
|
|
922
|
+
generated panel indexes and positional label records land on the matching
|
|
923
|
+
panel letters, while keyed access such as ``axes["B"]`` is always exact.
|
|
924
|
+
|
|
913
925
|
Examples
|
|
914
926
|
--------
|
|
915
927
|
>>> import gsplot as gs
|
|
@@ -919,6 +931,15 @@ class AxesDict(dict[str, Axes]):
|
|
|
919
931
|
>>> axes["A"] is axes[0]
|
|
920
932
|
True
|
|
921
933
|
>>> figure.clear()
|
|
934
|
+
|
|
935
|
+
Non-alphabetical mosaics still iterate in panel-name order:
|
|
936
|
+
|
|
937
|
+
>>> figure, axes = gs.subplots("ACE;BDE")
|
|
938
|
+
>>> tuple(axes)
|
|
939
|
+
('A', 'B', 'C', 'D', 'E')
|
|
940
|
+
>>> axes[1] is axes["B"]
|
|
941
|
+
True
|
|
942
|
+
>>> figure.clear()
|
|
922
943
|
"""
|
|
923
944
|
|
|
924
945
|
@overload
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Figure-local fitting for independent gsplot text annotations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from matplotlib.backend_bases import RendererBase
|
|
11
|
+
from matplotlib.figure import Figure
|
|
12
|
+
from matplotlib.text import Annotation, Text
|
|
13
|
+
from matplotlib.transforms import Bbox, ScaledTranslation, Transform
|
|
14
|
+
|
|
15
|
+
from .._core.errors import LayoutError
|
|
16
|
+
|
|
17
|
+
_FIGURE_FIT_STATE = "_gsplot_figure_fit_state"
|
|
18
|
+
_MAX_FIT_PASSES = 4
|
|
19
|
+
_DISPLAY_EPSILON = 1e-6
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class _AnnotationRecord:
|
|
24
|
+
"""Base and last-applied placement for one registered annotation."""
|
|
25
|
+
|
|
26
|
+
annotation_points: bool
|
|
27
|
+
base_transform: Transform | None = None
|
|
28
|
+
base_position: tuple[float, float] | None = None
|
|
29
|
+
applied_transform: Transform | None = None
|
|
30
|
+
applied_position: tuple[float, float] | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class _FigureFitState:
|
|
35
|
+
"""Mutable annotation registry owned by one Figure."""
|
|
36
|
+
|
|
37
|
+
annotations: dict[Text, _AnnotationRecord] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def configure_figure_fit(
|
|
41
|
+
figure: Figure,
|
|
42
|
+
enabled: bool,
|
|
43
|
+
*,
|
|
44
|
+
reset: bool = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Configure one Figure's independent-text fitting policy."""
|
|
47
|
+
|
|
48
|
+
if not enabled:
|
|
49
|
+
setattr(figure, _FIGURE_FIT_STATE, None)
|
|
50
|
+
return
|
|
51
|
+
state = None if reset else getattr(figure, _FIGURE_FIT_STATE, None)
|
|
52
|
+
if not isinstance(state, _FigureFitState):
|
|
53
|
+
state = _FigureFitState()
|
|
54
|
+
setattr(figure, _FIGURE_FIT_STATE, state)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _figure_fit_state(figure: Figure) -> _FigureFitState | None:
|
|
58
|
+
"""Return the Figure-local fitting state when the policy is enabled."""
|
|
59
|
+
|
|
60
|
+
state = getattr(figure, _FIGURE_FIT_STATE, None)
|
|
61
|
+
return state if isinstance(state, _FigureFitState) else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_figure_renderer(figure: Figure) -> RendererBase:
|
|
65
|
+
"""Return the renderer for a Figure across Matplotlib backends."""
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
return cast(RendererBase, cast(Any, figure)._get_renderer())
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
raise LayoutError("could not determine the rendered Figure renderer") from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _current_position(text: Text) -> tuple[float, float]:
|
|
74
|
+
"""Return a Text position as a finite two-dimensional tuple."""
|
|
75
|
+
|
|
76
|
+
raw_position = tuple(text.get_position())
|
|
77
|
+
if len(raw_position) != 2:
|
|
78
|
+
raise LayoutError("figure_fit: annotation position is not two-dimensional")
|
|
79
|
+
position = (float(raw_position[0]), float(raw_position[1]))
|
|
80
|
+
if not all(math.isfinite(value) for value in position):
|
|
81
|
+
raise LayoutError("figure_fit: annotation position is not finite")
|
|
82
|
+
return position
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _capture_record(
|
|
86
|
+
text: Text,
|
|
87
|
+
annotation_points: bool,
|
|
88
|
+
previous: _AnnotationRecord | None,
|
|
89
|
+
) -> _AnnotationRecord:
|
|
90
|
+
"""Capture caller placement while avoiding a previous fit translation."""
|
|
91
|
+
|
|
92
|
+
if annotation_points:
|
|
93
|
+
position = _current_position(text)
|
|
94
|
+
if (
|
|
95
|
+
previous is not None
|
|
96
|
+
and previous.annotation_points
|
|
97
|
+
and previous.applied_position is not None
|
|
98
|
+
and position == previous.applied_position
|
|
99
|
+
):
|
|
100
|
+
position = previous.base_position or position
|
|
101
|
+
return _AnnotationRecord(
|
|
102
|
+
annotation_points=True,
|
|
103
|
+
base_position=position,
|
|
104
|
+
applied_position=position,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
transform = cast(Transform, text.get_transform())
|
|
108
|
+
if (
|
|
109
|
+
previous is not None
|
|
110
|
+
and not previous.annotation_points
|
|
111
|
+
and previous.applied_transform is transform
|
|
112
|
+
):
|
|
113
|
+
transform = previous.base_transform or transform
|
|
114
|
+
return _AnnotationRecord(
|
|
115
|
+
annotation_points=False,
|
|
116
|
+
base_transform=transform,
|
|
117
|
+
applied_transform=transform,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _capture_location(
|
|
122
|
+
text: Text,
|
|
123
|
+
annotation_points: bool,
|
|
124
|
+
) -> Transform | tuple[float, float]:
|
|
125
|
+
"""Capture an artist's current position for transactional rollback."""
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
_current_position(text)
|
|
129
|
+
if annotation_points
|
|
130
|
+
else cast(Transform, text.get_transform())
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _restore_location(
|
|
135
|
+
text: Text,
|
|
136
|
+
annotation_points: bool,
|
|
137
|
+
location: Transform | tuple[float, float],
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Restore an artist's exact transform or point position."""
|
|
140
|
+
|
|
141
|
+
if annotation_points:
|
|
142
|
+
cast(Annotation, text).set_position(cast(tuple[float, float], location))
|
|
143
|
+
else:
|
|
144
|
+
text.set_transform(cast(Transform, location))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def register_figure_annotations(
|
|
148
|
+
figure: Figure,
|
|
149
|
+
annotations: Iterable[tuple[Text, bool]],
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Register independent annotations and fit the complete Figure set."""
|
|
152
|
+
|
|
153
|
+
state = _figure_fit_state(figure)
|
|
154
|
+
if state is None:
|
|
155
|
+
return
|
|
156
|
+
items = tuple(annotations)
|
|
157
|
+
previous_annotations = {
|
|
158
|
+
text: replace(record) for text, record in state.annotations.items()
|
|
159
|
+
}
|
|
160
|
+
previous_locations: dict[Text, tuple[bool, Transform | tuple[float, float]]] = {
|
|
161
|
+
text: (
|
|
162
|
+
record.annotation_points,
|
|
163
|
+
_capture_location(text, record.annotation_points),
|
|
164
|
+
)
|
|
165
|
+
for text, record in previous_annotations.items()
|
|
166
|
+
if text.figure is figure
|
|
167
|
+
}
|
|
168
|
+
for text, annotation_points in items:
|
|
169
|
+
if text.figure is figure and text not in previous_locations:
|
|
170
|
+
previous_locations[text] = (
|
|
171
|
+
annotation_points,
|
|
172
|
+
_capture_location(text, annotation_points),
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
for text, annotation_points in items:
|
|
176
|
+
if text.figure is figure:
|
|
177
|
+
state.annotations[text] = _capture_record(
|
|
178
|
+
text,
|
|
179
|
+
annotation_points,
|
|
180
|
+
previous_annotations.get(text),
|
|
181
|
+
)
|
|
182
|
+
fit_figure_annotations(figure)
|
|
183
|
+
except Exception:
|
|
184
|
+
state.annotations.clear()
|
|
185
|
+
state.annotations.update(previous_annotations)
|
|
186
|
+
for text, (annotation_points, location) in previous_locations.items():
|
|
187
|
+
_restore_location(text, annotation_points, location)
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _live_annotations(figure: Figure) -> tuple[tuple[Text, _AnnotationRecord], ...]:
|
|
192
|
+
"""Return visible registered annotations still attached to one Figure."""
|
|
193
|
+
|
|
194
|
+
state = _figure_fit_state(figure)
|
|
195
|
+
if state is None:
|
|
196
|
+
return ()
|
|
197
|
+
live: list[tuple[Text, _AnnotationRecord]] = []
|
|
198
|
+
stale: list[Text] = []
|
|
199
|
+
for text, record in state.annotations.items():
|
|
200
|
+
if text.figure is not figure:
|
|
201
|
+
stale.append(text)
|
|
202
|
+
elif text.get_visible():
|
|
203
|
+
live.append((text, record))
|
|
204
|
+
for text in stale:
|
|
205
|
+
del state.annotations[text]
|
|
206
|
+
return tuple(live)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _reset_annotation(text: Text, record: _AnnotationRecord) -> None:
|
|
210
|
+
"""Remove a previous fit translation while preserving the caller's base."""
|
|
211
|
+
|
|
212
|
+
if record.annotation_points:
|
|
213
|
+
if record.base_position is None:
|
|
214
|
+
raise LayoutError("figure_fit: annotation has no base position")
|
|
215
|
+
cast(Annotation, text).set_position(record.base_position)
|
|
216
|
+
record.applied_position = record.base_position
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
if record.base_transform is None:
|
|
220
|
+
raise LayoutError("figure_fit: annotation has no base transform")
|
|
221
|
+
text.set_transform(record.base_transform)
|
|
222
|
+
record.applied_transform = record.base_transform
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _annotation_box(text: Text, renderer: Any) -> Any:
|
|
226
|
+
"""Return one finite rendered text box or raise a layout error."""
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
boxes = [text.get_window_extent(renderer)]
|
|
230
|
+
patch = text.get_bbox_patch()
|
|
231
|
+
if patch is not None and patch.get_visible():
|
|
232
|
+
boxes.append(patch.get_window_extent(renderer))
|
|
233
|
+
box = Bbox.union(boxes)
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
raise LayoutError("figure_fit: could not measure an annotation") from exc
|
|
236
|
+
values = (box.x0, box.y0, box.x1, box.y1)
|
|
237
|
+
if not all(math.isfinite(float(value)) for value in values):
|
|
238
|
+
raise LayoutError("figure_fit: annotation bounds are not finite")
|
|
239
|
+
return box
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _required_shift(
|
|
243
|
+
lower: float,
|
|
244
|
+
upper: float,
|
|
245
|
+
box_lower: float,
|
|
246
|
+
box_upper: float,
|
|
247
|
+
*,
|
|
248
|
+
dimension: str,
|
|
249
|
+
) -> float:
|
|
250
|
+
"""Return the smallest display-space shift that enters one Figure edge."""
|
|
251
|
+
|
|
252
|
+
if box_upper - box_lower > upper - lower + _DISPLAY_EPSILON:
|
|
253
|
+
raise LayoutError(
|
|
254
|
+
f"figure_fit: annotation does not fit within the Figure {dimension}"
|
|
255
|
+
)
|
|
256
|
+
if box_lower < lower:
|
|
257
|
+
return lower - box_lower
|
|
258
|
+
if box_upper > upper:
|
|
259
|
+
return upper - box_upper
|
|
260
|
+
return 0.0
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _shift_for(
|
|
264
|
+
figure: Figure,
|
|
265
|
+
text: Text,
|
|
266
|
+
renderer: Any,
|
|
267
|
+
*,
|
|
268
|
+
record: _AnnotationRecord,
|
|
269
|
+
) -> tuple[float, float]:
|
|
270
|
+
"""Measure one base-position annotation and apply its minimum correction."""
|
|
271
|
+
|
|
272
|
+
box = _annotation_box(text, renderer)
|
|
273
|
+
figure_box = figure.bbox
|
|
274
|
+
dx = _required_shift(
|
|
275
|
+
figure_box.x0,
|
|
276
|
+
figure_box.x1,
|
|
277
|
+
box.x0,
|
|
278
|
+
box.x1,
|
|
279
|
+
dimension="width",
|
|
280
|
+
)
|
|
281
|
+
dy = _required_shift(
|
|
282
|
+
figure_box.y0,
|
|
283
|
+
figure_box.y1,
|
|
284
|
+
box.y0,
|
|
285
|
+
box.y1,
|
|
286
|
+
dimension="height",
|
|
287
|
+
)
|
|
288
|
+
if dx == 0.0 and dy == 0.0:
|
|
289
|
+
return dx, dy
|
|
290
|
+
|
|
291
|
+
if record.annotation_points:
|
|
292
|
+
if record.base_position is None:
|
|
293
|
+
raise LayoutError("figure_fit: annotation has no base position")
|
|
294
|
+
position = record.base_position
|
|
295
|
+
points_per_display_unit = 72.0 / float(figure.dpi)
|
|
296
|
+
applied_position = (
|
|
297
|
+
float(position[0]) + dx * points_per_display_unit,
|
|
298
|
+
float(position[1]) + dy * points_per_display_unit,
|
|
299
|
+
)
|
|
300
|
+
cast(Annotation, text).set_position(applied_position)
|
|
301
|
+
record.applied_position = applied_position
|
|
302
|
+
else:
|
|
303
|
+
if record.base_transform is None:
|
|
304
|
+
raise LayoutError("figure_fit: annotation has no base transform")
|
|
305
|
+
record.applied_transform = record.base_transform + ScaledTranslation(
|
|
306
|
+
dx / float(figure.dpi),
|
|
307
|
+
dy / float(figure.dpi),
|
|
308
|
+
figure.dpi_scale_trans,
|
|
309
|
+
)
|
|
310
|
+
text.set_transform(record.applied_transform)
|
|
311
|
+
return dx, dy
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def fit_figure_annotations(figure: Figure) -> None:
|
|
315
|
+
"""Keep registered independent annotations inside a fixed Figure canvas."""
|
|
316
|
+
|
|
317
|
+
annotations = _live_annotations(figure)
|
|
318
|
+
if not annotations:
|
|
319
|
+
return
|
|
320
|
+
canvas = cast(Any, figure.canvas)
|
|
321
|
+
for _ in range(_MAX_FIT_PASSES):
|
|
322
|
+
for text, record in annotations:
|
|
323
|
+
_reset_annotation(text, record)
|
|
324
|
+
try:
|
|
325
|
+
canvas.draw()
|
|
326
|
+
renderer = _get_figure_renderer(figure)
|
|
327
|
+
except Exception as exc:
|
|
328
|
+
raise LayoutError("figure_fit: could not render the Figure") from exc
|
|
329
|
+
|
|
330
|
+
moved = False
|
|
331
|
+
for text, record in annotations:
|
|
332
|
+
dx, dy = _shift_for(
|
|
333
|
+
figure,
|
|
334
|
+
text,
|
|
335
|
+
renderer,
|
|
336
|
+
record=record,
|
|
337
|
+
)
|
|
338
|
+
moved = moved or dx != 0.0 or dy != 0.0
|
|
339
|
+
if not moved:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
canvas.draw()
|
|
344
|
+
renderer = _get_figure_renderer(figure)
|
|
345
|
+
except Exception as exc:
|
|
346
|
+
raise LayoutError("figure_fit: could not render the Figure") from exc
|
|
347
|
+
for text, _ in annotations:
|
|
348
|
+
box = _annotation_box(text, renderer)
|
|
349
|
+
figure_box = figure.bbox
|
|
350
|
+
if (
|
|
351
|
+
box.x0 < figure_box.x0 - _DISPLAY_EPSILON
|
|
352
|
+
or box.y0 < figure_box.y0 - _DISPLAY_EPSILON
|
|
353
|
+
or box.x1 > figure_box.x1 + _DISPLAY_EPSILON
|
|
354
|
+
or box.y1 > figure_box.y1 + _DISPLAY_EPSILON
|
|
355
|
+
):
|
|
356
|
+
raise LayoutError(
|
|
357
|
+
"figure_fit: annotations could not be placed inside the Figure"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
__all__ = [
|
|
362
|
+
"configure_figure_fit",
|
|
363
|
+
"fit_figure_annotations",
|
|
364
|
+
"register_figure_annotations",
|
|
365
|
+
]
|