plotnine 0.14.5__py3-none-any.whl → 0.15.0a2__py3-none-any.whl

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 (92) hide show
  1. plotnine/__init__.py +31 -37
  2. plotnine/_mpl/gridspec.py +265 -0
  3. plotnine/_mpl/layout_manager/__init__.py +6 -0
  4. plotnine/_mpl/layout_manager/_engine.py +87 -0
  5. plotnine/_mpl/layout_manager/_layout_items.py +957 -0
  6. plotnine/_mpl/layout_manager/_layout_tree.py +905 -0
  7. plotnine/_mpl/layout_manager/_spaces.py +1154 -0
  8. plotnine/_mpl/patches.py +70 -34
  9. plotnine/_mpl/text.py +159 -37
  10. plotnine/_mpl/utils.py +78 -10
  11. plotnine/_utils/__init__.py +35 -9
  12. plotnine/_utils/dev.py +45 -27
  13. plotnine/_utils/yippie.py +115 -0
  14. plotnine/animation.py +1 -1
  15. plotnine/coords/coord.py +3 -3
  16. plotnine/coords/coord_trans.py +1 -1
  17. plotnine/data/__init__.py +43 -8
  18. plotnine/data/anscombe-quartet.csv +45 -0
  19. plotnine/doctools.py +2 -2
  20. plotnine/facets/facet.py +34 -43
  21. plotnine/facets/facet_grid.py +14 -6
  22. plotnine/facets/facet_wrap.py +3 -5
  23. plotnine/facets/strips.py +20 -33
  24. plotnine/geoms/annotate.py +3 -3
  25. plotnine/geoms/annotation_logticks.py +2 -0
  26. plotnine/geoms/annotation_stripes.py +2 -0
  27. plotnine/geoms/geom.py +3 -3
  28. plotnine/geoms/geom_bar.py +10 -2
  29. plotnine/geoms/geom_col.py +6 -0
  30. plotnine/geoms/geom_crossbar.py +2 -3
  31. plotnine/geoms/geom_path.py +2 -2
  32. plotnine/geoms/geom_violin.py +24 -7
  33. plotnine/ggplot.py +95 -66
  34. plotnine/guides/guide.py +19 -20
  35. plotnine/guides/guide_colorbar.py +6 -6
  36. plotnine/guides/guide_legend.py +15 -16
  37. plotnine/guides/guides.py +8 -8
  38. plotnine/helpers.py +49 -0
  39. plotnine/iapi.py +33 -7
  40. plotnine/labels.py +8 -3
  41. plotnine/layer.py +4 -4
  42. plotnine/mapping/_env.py +2 -2
  43. plotnine/mapping/_eval_environment.py +85 -0
  44. plotnine/mapping/aes.py +14 -30
  45. plotnine/mapping/evaluation.py +7 -65
  46. plotnine/options.py +14 -7
  47. plotnine/plot_composition/__init__.py +10 -0
  48. plotnine/plot_composition/_compose.py +462 -0
  49. plotnine/plot_composition/_plotspec.py +50 -0
  50. plotnine/plot_composition/_spacer.py +32 -0
  51. plotnine/positions/position_dodge.py +1 -1
  52. plotnine/positions/position_dodge2.py +1 -1
  53. plotnine/positions/position_stack.py +1 -2
  54. plotnine/qplot.py +1 -2
  55. plotnine/scales/__init__.py +0 -6
  56. plotnine/scales/limits.py +7 -7
  57. plotnine/scales/scale.py +4 -4
  58. plotnine/scales/scale_continuous.py +2 -1
  59. plotnine/scales/scale_identity.py +10 -2
  60. plotnine/scales/scale_manual.py +6 -2
  61. plotnine/stats/binning.py +5 -2
  62. plotnine/stats/smoothers.py +3 -5
  63. plotnine/stats/stat.py +3 -3
  64. plotnine/stats/stat_bindot.py +1 -3
  65. plotnine/stats/stat_density.py +2 -2
  66. plotnine/stats/stat_qq_line.py +1 -1
  67. plotnine/stats/stat_sina.py +34 -1
  68. plotnine/themes/elements/__init__.py +3 -0
  69. plotnine/themes/elements/element_text.py +35 -24
  70. plotnine/themes/elements/margin.py +137 -61
  71. plotnine/themes/targets.py +3 -1
  72. plotnine/themes/theme.py +21 -7
  73. plotnine/themes/theme_538.py +0 -1
  74. plotnine/themes/theme_bw.py +0 -1
  75. plotnine/themes/theme_dark.py +0 -1
  76. plotnine/themes/theme_gray.py +32 -34
  77. plotnine/themes/theme_light.py +1 -1
  78. plotnine/themes/theme_matplotlib.py +28 -31
  79. plotnine/themes/theme_seaborn.py +36 -36
  80. plotnine/themes/theme_void.py +25 -27
  81. plotnine/themes/theme_xkcd.py +0 -1
  82. plotnine/themes/themeable.py +369 -169
  83. plotnine/typing.py +3 -3
  84. plotnine/watermark.py +3 -3
  85. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/METADATA +8 -5
  86. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/RECORD +89 -78
  87. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/WHEEL +1 -1
  88. plotnine/_mpl/_plot_side_space.py +0 -888
  89. plotnine/_mpl/_plotnine_tight_layout.py +0 -293
  90. plotnine/_mpl/layout_engine.py +0 -110
  91. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info/licenses}/LICENSE +0 -0
  92. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/top_level.txt +0 -0
@@ -43,6 +43,8 @@ class scale_color_identity(MapTrainMixin, scale_discrete):
43
43
  """
44
44
 
45
45
  _aesthetics = ["color"]
46
+ _: KW_ONLY
47
+ guide: Literal["legend"] | None = None
46
48
 
47
49
 
48
50
  @dataclass
@@ -52,6 +54,8 @@ class scale_fill_identity(scale_color_identity):
52
54
  """
53
55
 
54
56
  _aesthetics = ["fill"]
57
+ _: KW_ONLY
58
+ guide: Literal["legend"] | None = None
55
59
 
56
60
 
57
61
  @dataclass
@@ -61,6 +65,8 @@ class scale_shape_identity(MapTrainMixin, scale_discrete):
61
65
  """
62
66
 
63
67
  _aesthetics = ["shape"]
68
+ _: KW_ONLY
69
+ guide: Literal["legend"] | None = None
64
70
 
65
71
 
66
72
  @dataclass
@@ -70,6 +76,8 @@ class scale_linetype_identity(MapTrainMixin, scale_discrete):
70
76
  """
71
77
 
72
78
  _aesthetics = ["linetype"]
79
+ _: KW_ONLY
80
+ guide: Literal["legend"] | None = None
73
81
 
74
82
 
75
83
  @dataclass
@@ -82,7 +90,7 @@ class scale_alpha_identity(
82
90
 
83
91
  _aesthetics = ["alpha"]
84
92
  _: KW_ONLY
85
- guide: Literal["legend"] | None = "legend"
93
+ guide: Literal["legend"] | None = None
86
94
 
87
95
 
88
96
  @dataclass
@@ -95,7 +103,7 @@ class scale_size_identity(
95
103
 
96
104
  _aesthetics = ["size"]
97
105
  _: KW_ONLY
98
- guide: Literal["legend"] | None = "legend"
106
+ guide: Literal["legend"] | None = None
99
107
 
100
108
 
101
109
  # American to British spelling
@@ -21,11 +21,15 @@ class _scale_manual(scale_discrete):
21
21
  """
22
22
 
23
23
  def __post_init__(self, values):
24
- from collections.abc import Sized
24
+ from collections.abc import Iterable, Sized
25
25
 
26
26
  super().__post_init__()
27
27
 
28
- if isinstance(self.breaks, Sized) and len(self.breaks) == len(values):
28
+ if (
29
+ isinstance(self.breaks, Iterable)
30
+ and isinstance(self.breaks, Sized)
31
+ and len(self.breaks) == len(values)
32
+ ):
29
33
  values = dict(zip(self.breaks, values))
30
34
 
31
35
  def palette(n):
plotnine/stats/binning.py CHANGED
@@ -73,7 +73,7 @@ def breaks_from_binwidth(
73
73
 
74
74
  if boundary is not None and center is not None:
75
75
  raise PlotnineError(
76
- "Only one of 'boundary' and 'center' " "may be specified."
76
+ "Only one of 'boundary' and 'center' may be specified."
77
77
  )
78
78
  elif boundary is None:
79
79
  # When center is None, put the min and max of data in outer
@@ -165,7 +165,10 @@ def assign_bins(
165
165
  if weight is None:
166
166
  weight = np.ones(len(x))
167
167
  else:
168
- weight = np.asarray(weight)
168
+ # If weight is a dtype that isn't writeable
169
+ # and does not own it's memory. Using a list
170
+ # as an intermediate easily solves this.
171
+ weight = np.array(list(weight))
169
172
  weight[np.isnan(weight)] = 0
170
173
 
171
174
  bin_idx = pd.cut(
@@ -37,7 +37,7 @@ def predictdf(data, xseq, **params) -> pd.DataFrame:
37
37
  "gpr": gpr,
38
38
  }
39
39
 
40
- method = cast(str | Callable[..., pd.DataFrame], params["method"])
40
+ method = cast("str | Callable[..., pd.DataFrame]", params["method"])
41
41
 
42
42
  if isinstance(method, str):
43
43
  try:
@@ -163,8 +163,7 @@ def rlm(data, xseq, **params) -> pd.DataFrame:
163
163
 
164
164
  if params["se"]:
165
165
  warnings.warn(
166
- "Confidence intervals are not yet implemented"
167
- " for RLM smoothing.",
166
+ "Confidence intervals are not yet implemented for RLM smoothing.",
168
167
  PlotnineWarning,
169
168
  )
170
169
 
@@ -190,8 +189,7 @@ def rlm_formula(data, xseq, **params) -> pd.DataFrame:
190
189
 
191
190
  if params["se"]:
192
191
  warnings.warn(
193
- "Confidence intervals are not yet implemented"
194
- " for RLM smoothing.",
192
+ "Confidence intervals are not yet implemented for RLM smoothing.",
195
193
  PlotnineWarning,
196
194
  )
197
195
 
plotnine/stats/stat.py CHANGED
@@ -392,7 +392,7 @@ class stat(ABC, metaclass=Register):
392
392
  msg = "{} should implement this method."
393
393
  raise NotImplementedError(msg.format(cls.__name__))
394
394
 
395
- def __radd__(self, plot: ggplot) -> ggplot:
395
+ def __radd__(self, other: ggplot) -> ggplot:
396
396
  """
397
397
  Add layer representing stat object on the right
398
398
 
@@ -406,8 +406,8 @@ class stat(ABC, metaclass=Register):
406
406
  out :
407
407
  ggplot object with added layer
408
408
  """
409
- plot += self.to_layer() # Add layer
410
- return plot
409
+ other += self.to_layer() # Add layer
410
+ return other
411
411
 
412
412
  def to_layer(self) -> layer:
413
413
  """
@@ -281,9 +281,7 @@ def densitybin(
281
281
  if all(pd.isna(x)):
282
282
  return pd.DataFrame()
283
283
 
284
- if weight is None:
285
- weight = np.ones(len(x))
286
- weight = np.asarray(weight)
284
+ weight = np.ones(len(x)) if weight is None else np.array(list(weight))
287
285
  weight[np.isnan(weight)] = 0
288
286
 
289
287
  if rangee is None:
@@ -102,9 +102,9 @@ class stat_density(stat):
102
102
  # useful for stacked density plots
103
103
 
104
104
  'scaled' # density estimate, scaled to maximum of 1
105
+ 'n' # Number of observations at a position
105
106
  ```
106
107
 
107
- 'n' # Number of observations at a position
108
108
 
109
109
  """
110
110
  REQUIRED_AES = {"x"}
@@ -171,7 +171,7 @@ def compute_density(x, weight, range, **params):
171
171
  x = np.asarray(x, dtype=float)
172
172
  not_nan = ~np.isnan(x)
173
173
  x = x[not_nan]
174
- bw = cast(str | float, params["bw"])
174
+ bw = cast("str | float", params["bw"])
175
175
  kernel = params["kernel"]
176
176
  bounds = params["bounds"]
177
177
  has_bounds = not (np.isneginf(bounds[0]) and np.isposinf(bounds[1]))
@@ -62,7 +62,7 @@ class stat_qq_line(stat):
62
62
  def setup_params(self, data):
63
63
  if len(self.params["line_p"]) != 2:
64
64
  raise PlotnineError(
65
- "Cannot fit line quantiles. " "'line_p' must be of length 2"
65
+ "Cannot fit line quantiles. 'line_p' must be of length 2"
66
66
  )
67
67
  return self.params
68
68
 
@@ -57,6 +57,15 @@ class stat_sina(stat):
57
57
  - `area` - Scale by the largest density/bin among the different sinas
58
58
  - `count` - areas are scaled proportionally to the number of points
59
59
  - `width` - Only scale according to the maxwidth parameter.
60
+ style :
61
+ Type of sina plot to draw. The options are
62
+ ```python
63
+ 'full' # Regular (2 sided)
64
+ 'left' # Left-sided half
65
+ 'right' # Right-sided half
66
+ 'left-right' # Alternate (left first) half by the group
67
+ 'right-left' # Alternate (right first) half by the group
68
+ ```
60
69
 
61
70
  See Also
62
71
  --------
@@ -91,6 +100,7 @@ class stat_sina(stat):
91
100
  "bin_limit": 1,
92
101
  "random_state": None,
93
102
  "scale": "area",
103
+ "style": "full",
94
104
  }
95
105
  CREATES = {"scaled"}
96
106
 
@@ -101,7 +111,7 @@ class stat_sina(stat):
101
111
  and (data["x"] != data["x"].iloc[0]).any()
102
112
  ):
103
113
  raise TypeError(
104
- "Continuous x aesthetic -- did you forget " "aes(group=...)?"
114
+ "Continuous x aesthetic -- did you forget aes(group=...)?"
105
115
  )
106
116
  return data
107
117
 
@@ -245,6 +255,29 @@ class stat_sina(stat):
245
255
 
246
256
  def finish_layer(self, data, params):
247
257
  # Rescale x in case positions have been adjusted
258
+ style = params["style"]
259
+ x_mean = data["x"].to_numpy()
248
260
  x_mod = (data["xmax"] - data["xmin"]) / data["width"]
249
261
  data["x"] = data["x"] + data["x_diff"] * x_mod
262
+ x = data["x"].to_numpy()
263
+ even = data["group"].to_numpy() % 2 == 0
264
+
265
+ def mirror_x(bool_idx):
266
+ """
267
+ Mirror x locations along the mean value
268
+ """
269
+ data.loc[bool_idx, "x"] = (
270
+ 2 * x_mean[bool_idx] - data.loc[bool_idx, "x"]
271
+ )
272
+
273
+ match style:
274
+ case "left":
275
+ mirror_x(x_mean < x)
276
+ case "right":
277
+ mirror_x(x < x_mean)
278
+ case "left-right":
279
+ mirror_x(even & (x < x_mean) | ~even & (x_mean < x))
280
+ case "right-left":
281
+ mirror_x(even & (x_mean < x) | ~even & (x < x_mean))
282
+
250
283
  return data
@@ -2,10 +2,13 @@ from .element_blank import element_blank
2
2
  from .element_line import element_line
3
3
  from .element_rect import element_rect
4
4
  from .element_text import element_text
5
+ from .margin import margin, margin_auto
5
6
 
6
7
  __all__ = (
7
8
  "element_blank",
8
9
  "element_line",
9
10
  "element_rect",
10
11
  "element_text",
12
+ "margin",
13
+ "margin_auto",
11
14
  )
@@ -8,10 +8,10 @@ from contextlib import suppress
8
8
  from typing import TYPE_CHECKING
9
9
 
10
10
  from .element_base import element_base
11
- from .margin import Margin
11
+ from .margin import margin as Margin
12
12
 
13
13
  if TYPE_CHECKING:
14
- from typing import Any, Literal, Optional, Sequence
14
+ from typing import Any, Literal, Sequence
15
15
 
16
16
  from plotnine import theme
17
17
 
@@ -56,7 +56,7 @@ class element_text(element_base):
56
56
  Margin around the text. The keys are
57
57
  `t`, `b`, `l`, `r` and `units`.
58
58
  The `tblr` keys are floats.
59
- The `units` is one of `pt`, `lines` or `in`.
59
+ The `unit` is one of `pt`, `lines` or `in`.
60
60
  Not all text themeables support margin parameters and other
61
61
  than the `units`, only some of the other keys may apply.
62
62
  kwargs :
@@ -71,10 +71,10 @@ class element_text(element_base):
71
71
 
72
72
  def __init__(
73
73
  self,
74
- family: Optional[str | list[str]] = None,
75
- style: Optional[str | Sequence[str]] = None,
76
- weight: Optional[int | str | Sequence[int | str]] = None,
77
- color: Optional[
74
+ family: str | list[str] | None = None,
75
+ style: str | Sequence[str] | None = None,
76
+ weight: int | str | Sequence[int | str] | None = None,
77
+ color: (
78
78
  str
79
79
  | tuple[float, float, float]
80
80
  | tuple[float, float, float, float]
@@ -83,22 +83,25 @@ class element_text(element_base):
83
83
  | tuple[float, float, float]
84
84
  | tuple[float, float, float, float]
85
85
  ]
86
- ] = None,
87
- size: Optional[float | Sequence[float]] = None,
88
- ha: Optional[Literal["center", "left", "right"] | float] = None,
89
- va: Optional[
86
+ | None
87
+ ) = None,
88
+ size: float | Sequence[float] | None = None,
89
+ ha: Literal["center", "left", "right"] | float | None = None,
90
+ va: (
90
91
  Literal["center", "top", "bottom", "baseline", "center_baseline"]
91
92
  | float
92
- ] = None,
93
- ma: Optional[Literal["center", "left", "right"] | float] = None,
94
- rotation: Optional[
93
+ | None
94
+ ) = None,
95
+ ma: Literal["center", "left", "right"] | float | None = None,
96
+ rotation: (
95
97
  Literal["vertical", "horizontal"]
96
98
  | float
97
99
  | Sequence[Literal["vertical", "horizontal"]]
98
100
  | Sequence[float]
99
- ] = None,
100
- linespacing: Optional[float] = None,
101
- backgroundcolor: Optional[
101
+ | None
102
+ ) = None,
103
+ linespacing: float | None = None,
104
+ backgroundcolor: (
102
105
  str
103
106
  | tuple[float, float, float]
104
107
  | tuple[float, float, float, float]
@@ -107,10 +110,11 @@ class element_text(element_base):
107
110
  | tuple[float, float, float]
108
111
  | tuple[float, float, float, float]
109
112
  ]
110
- ] = None,
111
- margin: Optional[
112
- dict[Literal["t", "b", "l", "r", "units"], Any]
113
- ] = None,
113
+ | None
114
+ ) = None,
115
+ margin: (
116
+ Margin | dict[Literal["t", "b", "l", "r", "unit"], Any] | None
117
+ ) = None,
114
118
  rotation_mode: Literal["default", "anchor"] | None = None,
115
119
  **kwargs: Any,
116
120
  ):
@@ -139,8 +143,15 @@ class element_text(element_base):
139
143
 
140
144
  super().__init__()
141
145
  self.properties.update(**kwargs)
146
+
142
147
  if margin is not None:
143
- self.properties["margin"] = Margin(self, **margin)
148
+ if isinstance(margin, dict):
149
+ if "units" in margin:
150
+ # for backward compatibility
151
+ margin["unit"] = margin.pop("units") # pyright: ignore[reportArgumentType]
152
+ margin = Margin(**margin)
153
+
154
+ self.properties["margin"] = margin
144
155
 
145
156
  # Use the parameters that have been set
146
157
  names = (
@@ -153,6 +164,7 @@ class element_text(element_base):
153
164
  "size",
154
165
  "style",
155
166
  "va",
167
+ "ma",
156
168
  "weight",
157
169
  "rotation_mode",
158
170
  )
@@ -166,8 +178,7 @@ class element_text(element_base):
166
178
  Setup the theme_element before drawing
167
179
  """
168
180
  if m := self.properties.get("margin"):
169
- m.theme = theme
170
- m.themeable_name = themeable_name
181
+ m = m.setup(theme, themeable_name)
171
182
 
172
183
  def _translate_hjust(
173
184
  self, just: float
@@ -1,10 +1,12 @@
1
1
  """
2
- Theme elements used to decorate the graph.
2
+ Margin
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from dataclasses import dataclass
7
+ from contextlib import suppress
8
+ from copy import copy
9
+ from dataclasses import dataclass, field
8
10
  from typing import TYPE_CHECKING
9
11
 
10
12
  if TYPE_CHECKING:
@@ -12,80 +14,154 @@ if TYPE_CHECKING:
12
14
 
13
15
  from plotnine import theme
14
16
 
15
- from .element_base import element_base
16
-
17
17
 
18
18
  @dataclass
19
- class Margin:
20
- element: element_base
19
+ class margin:
20
+ """
21
+ Margin
22
+ """
23
+
21
24
  t: float = 0
25
+ """
26
+ Top margin
27
+ """
28
+
29
+ r: float = 0
30
+ """
31
+ Right margin
32
+ """
33
+
22
34
  b: float = 0
35
+ """
36
+ Bottom margin
37
+ """
38
+
23
39
  l: float = 0
24
- r: float = 0
25
- units: Literal["pt", "in", "lines", "fig"] = "pt"
26
-
27
- def __post_init__(self):
28
- self.theme: theme
29
- self.themeable_name: str
30
-
31
- if self.units in ("pts", "points", "px", "pixels"):
32
- self.units = "pt"
33
- elif self.units in ("in", "inch", "inches"):
34
- self.units = "in"
35
- elif self.units in ("line", "lines"):
36
- self.units = "lines"
37
-
38
- def __eq__(self, other: object) -> bool:
39
- def _size(m: Margin):
40
- return m.element.properties.get("size")
41
-
42
- return other is self or (
43
- isinstance(other, type(self))
44
- and other.t == self.t
45
- and other.b == self.b
46
- and other.l == self.l
47
- and other.r == self.r
48
- and other.units == self.units
49
- and _size(other) == _size(self)
50
- )
51
-
52
- def get_as(
53
- self,
54
- loc: Literal["t", "b", "l", "r"],
55
- units: Literal["pt", "in", "lines", "fig"] = "pt",
56
- ) -> float:
40
+ """
41
+ Left Margin
42
+ """
43
+
44
+ unit: Literal["pt", "in", "lines", "fig"] = "pt"
45
+ """
46
+ The units (coordinate space) of the values
47
+ """
48
+
49
+ # These are set by the themeable when it is applied
50
+ fontsize: float = field(init=False, default=0)
51
+ """
52
+ Font size of text that this margin applies to
53
+ """
54
+
55
+ figure_size: tuple[float, float] = field(init=False, default=(0, 0))
56
+ """
57
+ Size of the figure in inches
58
+ """
59
+
60
+ def setup(self, theme: theme, themeable_name: str):
61
+ """
62
+ Setup the margin to be used in the layout
63
+
64
+ For the margin's values to be useful, we need to be able to
65
+ convert them to different units as is required. Here we get
66
+ all the parameters that we shall need to do the conversions.
67
+ """
68
+ self.themeable_name = themeable_name
69
+ self.fontsize = theme.getp((themeable_name, "size"), 11)
70
+ self.figure_size = theme.getp("figure_size")
71
+
72
+ @property
73
+ def pt(self) -> margin:
74
+ """
75
+ Return margin in points
76
+
77
+ These are the units of the display coordinate system
78
+ """
79
+ return self.to("pt")
80
+
81
+ @property
82
+ def inch(self) -> margin:
83
+ """
84
+ Return margin in inches
85
+
86
+ These are the units of the figure-inches coordinate system
87
+ """
88
+ return self.to("in")
89
+
90
+ @property
91
+ def lines(self) -> margin:
92
+ """
93
+ Return margin in lines units
94
+ """
95
+ return self.to("lines")
96
+
97
+ @property
98
+ def fig(self) -> margin:
99
+ """
100
+ Return margin in figure units
101
+
102
+ These are the units of the figure coordinate system
103
+ """
104
+ return self.to("fig")
105
+
106
+ def to(self, unit: Literal["pt", "in", "lines", "fig"]) -> margin:
57
107
  """
58
- Return key in given units
108
+ Return margin in request unit
59
109
  """
110
+ m = copy(self)
111
+ if self.unit == unit:
112
+ return m
113
+
114
+ conversion = f"{self.unit}-{unit}"
115
+ W, H = self.figure_size
116
+
117
+ with suppress(ZeroDivisionError):
118
+ m.t = self._convert(conversion, H, self.t)
119
+ with suppress(ZeroDivisionError):
120
+ m.r = self._convert(conversion, W, self.r)
121
+ with suppress(ZeroDivisionError):
122
+ m.b = self._convert(conversion, H, self.b)
123
+ with suppress(ZeroDivisionError):
124
+ m.l = self._convert(conversion, W, self.l)
125
+
126
+ m.unit = unit
127
+ return m
128
+
129
+ def _convert(self, conversion: str, D: float, value: float) -> float:
60
130
  dpi = 72
61
- size: float = self.theme.getp((self.themeable_name, "size"), 11)
62
- from_units = self.units
63
- to_units = units
64
- W: float
65
- H: float
66
- W, H = self.theme.getp("figure_size") # inches
67
- L = (W * dpi) if loc in "tb" else (H * dpi) # pts
131
+ L = D * dpi # pts
68
132
 
69
133
  functions: dict[str, Callable[[float], float]] = {
70
134
  "fig-in": lambda x: x * L / dpi,
71
- "fig-lines": lambda x: x * L / size,
135
+ "fig-lines": lambda x: x * L / self.fontsize,
72
136
  "fig-pt": lambda x: x * L,
73
137
  "in-fig": lambda x: x * dpi / L,
74
- "in-lines": lambda x: x * dpi / size,
138
+ "in-lines": lambda x: x * dpi / self.fontsize,
75
139
  "in-pt": lambda x: x * dpi,
76
- "lines-fig": lambda x: x * size / L,
77
- "lines-in": lambda x: x * size / dpi,
78
- "lines-pt": lambda x: x * size,
140
+ "lines-fig": lambda x: x * self.fontsize / L,
141
+ "lines-in": lambda x: x * self.fontsize / dpi,
142
+ "lines-pt": lambda x: x * self.fontsize,
79
143
  "pt-fig": lambda x: x / L,
80
144
  "pt-in": lambda x: x / dpi,
81
- "pt-lines": lambda x: x / size,
145
+ "pt-lines": lambda x: x / self.fontsize,
82
146
  }
83
147
 
84
- value: float = getattr(self, loc)
85
- if from_units != to_units:
86
- conversion = f"{self.units}-{units}"
87
- try:
88
- value = functions[conversion](value)
89
- except ZeroDivisionError:
90
- value = 0
91
- return value
148
+ return functions[conversion](value)
149
+
150
+
151
+ def margin_auto(
152
+ t: float = 0.0,
153
+ r: float | None = None,
154
+ b: float | None = None,
155
+ l: float | None = None,
156
+ unit: Literal["pt", "in", "lines", "fig"] = "pt",
157
+ ) -> margin:
158
+ """
159
+ Create margin with minimal arguments
160
+ """
161
+ if r is None:
162
+ r = t
163
+ if b is None:
164
+ b = t
165
+ if l is None:
166
+ l = r
167
+ return margin(t, r, b, l, unit)
@@ -21,7 +21,7 @@ class ThemeTargets:
21
21
  """
22
22
  Artists that will be themed
23
23
 
24
- This includes only artist that cannot be accessed easily from
24
+ This includes only artist that cannot be easily accessed from
25
25
  the figure or the axes.
26
26
  """
27
27
 
@@ -37,7 +37,9 @@ class ThemeTargets:
37
37
  panel_border: list[Rectangle] = field(default_factory=list)
38
38
  plot_caption: Optional[Text] = None
39
39
  plot_subtitle: Optional[Text] = None
40
+ plot_tag: Optional[Text] = None
40
41
  plot_title: Optional[Text] = None
42
+ plot_background: Optional[Rectangle] = None
41
43
  strip_background_x: list[StripTextPatch] = field(default_factory=list)
42
44
  strip_background_y: list[StripTextPatch] = field(default_factory=list)
43
45
  strip_text_x: list[StripText] = field(default_factory=list)