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.
Files changed (124) hide show
  1. {gsplot-0.4.1 → gsplot-0.4.2}/PKG-INFO +1 -1
  2. {gsplot-0.4.1 → gsplot-0.4.2}/pyproject.toml +1 -1
  3. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/__init__.py +4 -2
  4. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/__init__.pyi +4 -2
  5. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy_api.py +40 -10
  6. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/root_api.py +33 -22
  7. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/types.py +21 -0
  8. gsplot-0.4.2/src/gsplot/_figure/fit.py +365 -0
  9. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/layout.py +37 -3
  10. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/output.py +16 -2
  11. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/axes.py +86 -16
  12. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/legends.py +392 -81
  13. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/panels.py +224 -55
  14. {gsplot-0.4.1 → gsplot-0.4.2}/LICENSE +0 -0
  15. {gsplot-0.4.1 → gsplot-0.4.2}/README.md +0 -0
  16. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/__init__.py +0 -0
  17. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/config.py +0 -0
  18. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/__init__.py +0 -0
  19. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/base/__init__.py +0 -0
  20. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/base/base.py +0 -0
  21. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/base/base_alias_validator.py +0 -0
  22. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/color/__init__.py +0 -0
  23. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/color/colormap.py +0 -0
  24. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/config/__init__.py +0 -0
  25. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/config/config.py +0 -0
  26. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/data/__init__.py +0 -0
  27. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/data/load_file.py +0 -0
  28. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/__init__.py +0 -0
  29. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes.py +0 -0
  30. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes_base.py +0 -0
  31. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes_inset.py +0 -0
  32. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/axes_range_base.py +0 -0
  33. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/figure_tools.py +0 -0
  34. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/show.py +0 -0
  35. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/figure/store.py +0 -0
  36. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/hello_world/__init__.py +0 -0
  37. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/hello_world/hello_world.py +0 -0
  38. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/logger.py +0 -0
  39. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/path/__init__.py +0 -0
  40. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/path/path.py +0 -0
  41. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/__init__.py +0 -0
  42. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line.py +0 -0
  43. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_base.py +0 -0
  44. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_colormap_base.py +0 -0
  45. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_colormap_dashed.py +0 -0
  46. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/line_colormap_solid.py +0 -0
  47. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/scatter.py +0 -0
  48. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/plot/scatter_colormap.py +0 -0
  49. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/__init__.py +0 -0
  50. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/graph.py +0 -0
  51. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/label.py +0 -0
  52. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/legend.py +0 -0
  53. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/legend_colormap.py +0 -0
  54. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/ticks.py +0 -0
  55. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/legacy/style/title.py +0 -0
  56. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/root.py +0 -0
  57. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/root_legacy.py +0 -0
  58. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_compat/shim.py +0 -0
  59. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/__init__.py +0 -0
  60. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/loader.py +0 -0
  61. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/model.py +0 -0
  62. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_config/schema.py +0 -0
  63. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/__init__.py +0 -0
  64. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/errors.py +0 -0
  65. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/numerics.py +0 -0
  66. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/options.py +0 -0
  67. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/plans.py +0 -0
  68. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/targets.py +0 -0
  69. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_core/validation.py +0 -0
  70. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/__init__.py +0 -0
  71. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/backend.py +0 -0
  72. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_figure/inset.py +0 -0
  73. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/__init__.py +0 -0
  74. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/arrays.py +0 -0
  75. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/build.py +0 -0
  76. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/metadata.py +0 -0
  77. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_io/paths.py +0 -0
  78. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/__init__.py +0 -0
  79. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/basic.py +0 -0
  80. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/colored.py +0 -0
  81. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/colormap.py +0 -0
  82. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_plot/series.py +0 -0
  83. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/__init__.py +0 -0
  84. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/paper.py +0 -0
  85. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/_style/themes.py +0 -0
  86. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/base/__init__.py +0 -0
  87. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/base/base.py +0 -0
  88. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/base/base_alias_validator.py +0 -0
  89. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/color/__init__.py +0 -0
  90. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/color/colormap.py +0 -0
  91. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/config/__init__.py +0 -0
  92. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/config/config.py +0 -0
  93. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/data/__init__.py +0 -0
  94. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/data/load_file.py +0 -0
  95. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/__init__.py +0 -0
  96. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes.py +0 -0
  97. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes_base.py +0 -0
  98. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes_inset.py +0 -0
  99. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/axes_range_base.py +0 -0
  100. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/figure_tools.py +0 -0
  101. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/show.py +0 -0
  102. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/figure/store.py +0 -0
  103. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/hello_world/__init__.py +0 -0
  104. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/hello_world/hello_world.py +0 -0
  105. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/logger.py +0 -0
  106. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/path/__init__.py +0 -0
  107. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/path/path.py +0 -0
  108. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/__init__.py +0 -0
  109. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line.py +0 -0
  110. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_base.py +0 -0
  111. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_colormap_base.py +0 -0
  112. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_colormap_dashed.py +0 -0
  113. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/line_colormap_solid.py +0 -0
  114. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/scatter.py +0 -0
  115. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/plot/scatter_colormap.py +0 -0
  116. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/py.typed +0 -0
  117. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/__init__.py +0 -0
  118. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/graph.py +0 -0
  119. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/label.py +0 -0
  120. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/legend.py +0 -0
  121. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/legend_colormap.py +0 -0
  122. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/ticks.py +0 -0
  123. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/style/title.py +0 -0
  124. {gsplot-0.4.1 → gsplot-0.4.2}/src/gsplot/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gsplot
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Concise publication-quality scientific plotting built on Matplotlib
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gsplot"
3
- version = "0.4.1"
3
+ version = "0.4.2"
4
4
  description = "Concise publication-quality scientific plotting built on Matplotlib"
5
5
  authors = [
6
6
  { name = "Giordano Mattoni" },
@@ -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: Any
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: Any
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 cmap_legend as _cmap_legend
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
- ) -> Any:
830
- """Adapt legacy colormap legends to :func:`gsplot.cmap_legend`."""
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
- return _cmap_legend(
862
+ colors = _legacy_colormap_values(cmap, num_stripes, vmin, vmax, reverse)
863
+ return _create_cmap_legend(
834
864
  ax,
835
- cmap=cmap,
836
- label=label,
837
- stripes=num_stripes,
838
- norm=(vmin, vmax),
839
- reverse=reverse,
840
- props=props or None,
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
+ ]