plotnine 0.15.2__py3-none-any.whl → 0.16.0a1__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 (60) hide show
  1. plotnine/_mpl/gridspec.py +50 -6
  2. plotnine/_mpl/layout_manager/__init__.py +2 -5
  3. plotnine/_mpl/layout_manager/_composition_layout_items.py +98 -0
  4. plotnine/_mpl/layout_manager/_composition_side_space.py +461 -0
  5. plotnine/_mpl/layout_manager/_engine.py +19 -58
  6. plotnine/_mpl/layout_manager/_grid.py +94 -0
  7. plotnine/_mpl/layout_manager/_layout_tree.py +402 -817
  8. plotnine/_mpl/layout_manager/{_layout_items.py → _plot_layout_items.py} +55 -278
  9. plotnine/_mpl/layout_manager/{_spaces.py → _plot_side_space.py} +111 -291
  10. plotnine/_mpl/layout_manager/_side_space.py +176 -0
  11. plotnine/_mpl/utils.py +259 -1
  12. plotnine/_utils/__init__.py +23 -3
  13. plotnine/_utils/context.py +1 -1
  14. plotnine/_utils/dataclasses.py +24 -0
  15. plotnine/animation.py +13 -12
  16. plotnine/composition/__init__.py +6 -0
  17. plotnine/composition/_beside.py +13 -11
  18. plotnine/composition/_compose.py +263 -99
  19. plotnine/composition/_plot_annotation.py +75 -0
  20. plotnine/composition/_plot_layout.py +143 -0
  21. plotnine/composition/_plot_spacer.py +1 -1
  22. plotnine/composition/_stack.py +13 -11
  23. plotnine/composition/_types.py +28 -0
  24. plotnine/composition/_wrap.py +60 -0
  25. plotnine/facets/facet.py +9 -12
  26. plotnine/facets/facet_grid.py +2 -2
  27. plotnine/facets/facet_wrap.py +1 -1
  28. plotnine/geoms/geom.py +2 -2
  29. plotnine/geoms/geom_map.py +4 -5
  30. plotnine/geoms/geom_path.py +8 -7
  31. plotnine/geoms/geom_rug.py +6 -10
  32. plotnine/geoms/geom_text.py +5 -5
  33. plotnine/ggplot.py +63 -9
  34. plotnine/guides/guide.py +24 -6
  35. plotnine/guides/guide_colorbar.py +88 -46
  36. plotnine/guides/guide_legend.py +47 -20
  37. plotnine/guides/guides.py +2 -2
  38. plotnine/iapi.py +17 -1
  39. plotnine/scales/scale.py +1 -1
  40. plotnine/stats/binning.py +15 -43
  41. plotnine/stats/smoothers.py +7 -3
  42. plotnine/stats/stat.py +2 -2
  43. plotnine/stats/stat_density_2d.py +10 -6
  44. plotnine/stats/stat_pointdensity.py +8 -1
  45. plotnine/stats/stat_qq.py +5 -5
  46. plotnine/stats/stat_qq_line.py +6 -1
  47. plotnine/stats/stat_sina.py +19 -20
  48. plotnine/stats/stat_summary.py +4 -2
  49. plotnine/stats/stat_summary_bin.py +7 -1
  50. plotnine/themes/elements/element_line.py +2 -0
  51. plotnine/themes/elements/element_text.py +12 -1
  52. plotnine/themes/theme.py +18 -24
  53. plotnine/themes/themeable.py +17 -3
  54. plotnine/typing.py +6 -1
  55. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/METADATA +2 -2
  56. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/RECORD +59 -51
  57. plotnine/composition/_plotspec.py +0 -50
  58. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/WHEEL +0 -0
  59. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
  60. {plotnine-0.15.2.dist-info → plotnine-0.16.0a1.dist-info}/top_level.txt +0 -0
plotnine/iapi.py CHANGED
@@ -14,7 +14,7 @@ from functools import cached_property
14
14
  from typing import TYPE_CHECKING
15
15
 
16
16
  if TYPE_CHECKING:
17
- from typing import Any, Iterator, Optional, Sequence
17
+ from typing import Any, Iterator, Literal, Optional, Sequence
18
18
 
19
19
  from matplotlib.axes import Axes
20
20
  from matplotlib.figure import Figure
@@ -26,8 +26,10 @@ if TYPE_CHECKING:
26
26
  FloatArrayLike,
27
27
  HorizontalJustification,
28
28
  ScaledAestheticsName,
29
+ Side,
29
30
  StripPosition,
30
31
  VerticalJustification,
32
+ VerticalTextJustification,
31
33
  )
32
34
 
33
35
  from ._mpl.offsetbox import FlexibleAnchoredOffsetbox
@@ -385,3 +387,17 @@ class legend_artists:
385
387
  )
386
388
  inside = (l.box for l in self.inside)
387
389
  return list(itertools.chain([*lrtb, *inside]))
390
+
391
+
392
+ @dataclass
393
+ class guide_text:
394
+ """
395
+ Processed guide text
396
+ """
397
+
398
+ margins: Sequence[float]
399
+ aligns: Sequence[Side | Literal["center"]]
400
+ fontsize: float
401
+ has: Sequence[HorizontalJustification]
402
+ vas: Sequence[VerticalTextJustification]
403
+ is_blank: bool
plotnine/scales/scale.py CHANGED
@@ -31,8 +31,8 @@ if TYPE_CHECKING:
31
31
 
32
32
  @dataclass(kw_only=True)
33
33
  class scale(
34
- Generic[RangeT, BreaksUserT, LimitsUserT, GuideTypeT],
35
34
  ABC,
35
+ Generic[RangeT, BreaksUserT, LimitsUserT, GuideTypeT],
36
36
  metaclass=Register,
37
37
  ):
38
38
  """
plotnine/stats/binning.py CHANGED
@@ -47,7 +47,7 @@ def breaks_from_binwidth(
47
47
  binwidth: float,
48
48
  center: Optional[float] = None,
49
49
  boundary: Optional[float] = None,
50
- ):
50
+ ) -> FloatArray:
51
51
  """
52
52
  Calculate breaks given binwidth
53
53
 
@@ -82,13 +82,12 @@ def breaks_from_binwidth(
82
82
  if center is not None:
83
83
  boundary = center - boundary
84
84
 
85
- epsilon = np.finfo(float).eps
86
85
  shift = np.floor((x_range[0] - boundary) / binwidth)
87
86
  origin = boundary + shift * binwidth
88
- # The (1-epsilon) factor prevents numerical roundoff in the
87
+ # The nextafter reduction prevents numerical roundoff in the
89
88
  # binwidth from creating an extra break beyond the one that
90
89
  # includes x_range[1].
91
- max_x = x_range[1] + binwidth * (1 - epsilon)
90
+ max_x = np.nextafter(x_range[1] + binwidth, -np.inf)
92
91
  breaks = np.arange(origin, max_x, binwidth)
93
92
  return breaks
94
93
 
@@ -98,7 +97,7 @@ def breaks_from_bins(
98
97
  bins: int = 30,
99
98
  center: Optional[float] = None,
100
99
  boundary: Optional[float] = None,
101
- ):
100
+ ) -> FloatArray:
102
101
  """
103
102
  Calculate breaks given binwidth
104
103
 
@@ -303,48 +302,21 @@ def fuzzybreaks(
303
302
 
304
303
  # To minimise precision errors, we do not pass the boundary and
305
304
  # binwidth into np.arange as params. The resulting breaks
306
- # can then be adjusted with finer(epsilon based rather than
307
- # some arbitrary small number) precision.
305
+ # can then be adjusted to the next floating point number.
308
306
  breaks = np.arange(boundary, srange[1] + binwidth, binwidth)
309
307
  return _adjust_breaks(breaks, right)
310
308
 
311
309
 
312
310
  def _adjust_breaks(breaks: FloatArray, right: bool) -> FloatArray:
313
- epsilon = np.finfo(float).eps
314
- plus = 1 + epsilon
315
- minus = 1 - epsilon
316
-
317
- sign = np.sign(breaks)
318
- pos_idx = np.where(sign == 1)[0]
319
- neg_idx = np.where(sign == -1)[0]
320
- zero_idx = np.where(sign == 0)[0]
321
-
322
- fuzzy = breaks.copy()
323
- if right:
324
- # [_](_](_](_]
325
- lbreak = breaks[0]
326
- fuzzy[pos_idx] *= plus
327
- fuzzy[neg_idx] *= minus
328
- fuzzy[zero_idx] = epsilon
329
- # Left closing break
330
- if lbreak == 0:
331
- fuzzy[0] = -epsilon
332
- elif lbreak < 0:
333
- fuzzy[0] = lbreak * plus
334
- else:
335
- fuzzy[0] = lbreak * minus
336
- else:
337
- # [_)[_)[_)[_]
338
- rbreak = breaks[-1]
339
- fuzzy[pos_idx] *= minus
340
- fuzzy[neg_idx] *= plus
341
- fuzzy[zero_idx] = -epsilon
342
- # Right closing break
343
- if rbreak == 0:
344
- fuzzy[-1] = epsilon
345
- elif rbreak > 0:
346
- fuzzy[-1] = rbreak * plus
347
- else:
348
- fuzzy[-1] = rbreak * minus
311
+ """
312
+ Adjust breaks to include/exclude every right break
349
313
 
314
+ If right=True, the breaks create intervals closed on right
315
+ i.e. [_] (_] (_] (_]
316
+ If right=False, the breaks create intervals closed on the left
317
+ i.e. [_) [_) [_) [_]
318
+ """
319
+ limit, idx = (np.inf, 0) if right else (-np.inf, -1)
320
+ fuzzy = np.nextafter(breaks, limit)
321
+ fuzzy[idx] = np.nextafter(breaks[idx], -limit)
350
322
  return fuzzy
@@ -13,6 +13,8 @@ from ..exceptions import PlotnineError, PlotnineWarning
13
13
  if TYPE_CHECKING:
14
14
  import statsmodels.api as sm
15
15
 
16
+ from plotnine.typing import FloatArray
17
+
16
18
 
17
19
  def predictdf(data, xseq, params) -> pd.DataFrame:
18
20
  """
@@ -64,7 +66,7 @@ def lm(data, xseq, params) -> pd.DataFrame:
64
66
 
65
67
  X = sm.add_constant(data["x"])
66
68
  Xseq = sm.add_constant(xseq)
67
- weights = data.get("weights", None)
69
+ weights = data.get("weight", None)
68
70
 
69
71
  if weights is None:
70
72
  init_kwargs, fit_kwargs = separate_method_kwargs(
@@ -454,12 +456,14 @@ def gpr(data, xseq, params):
454
456
  if params["se"]:
455
457
  y, stderr = regressor.predict(Xseq, return_std=True)
456
458
  data["y"] = y
457
- data["se"] = stderr
459
+ data["se"] = cast("FloatArray", stderr)
458
460
  data["ymin"], data["ymax"] = tdist_ci(
459
461
  y, n - 1, stderr, params["level"]
460
462
  )
461
463
  else:
462
- data["y"] = regressor.predict(Xseq, return_std=True)
464
+ data["y"] = cast(
465
+ "FloatArray", regressor.predict(Xseq, return_std=False)
466
+ )
463
467
 
464
468
  return data
465
469
 
plotnine/stats/stat.py CHANGED
@@ -144,10 +144,10 @@ class stat(ABC, metaclass=Register):
144
144
  shallow = {"_kwargs"}
145
145
  for key, item in old.items():
146
146
  if key in shallow:
147
- new[key] = item
147
+ new[key] = item # pyright: ignore[reportIndexIssue]
148
148
  memo[id(new[key])] = new[key]
149
149
  else:
150
- new[key] = deepcopy(item, memo)
150
+ new[key] = deepcopy(item, memo) # pyright: ignore[reportIndexIssue]
151
151
 
152
152
  return result
153
153
 
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, cast
4
4
 
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
 
8
+ from ..doctools import document
8
9
  from .density import get_var_type, kde
9
10
  from .stat import stat
10
11
 
@@ -12,6 +13,7 @@ if TYPE_CHECKING:
12
13
  from plotnine.typing import FloatArrayLike
13
14
 
14
15
 
16
+ @document
15
17
  class stat_density_2d(stat):
16
18
  """
17
19
  Compute 2D kernel density estimation
@@ -92,17 +94,19 @@ class stat_density_2d(stat):
92
94
  group = data["group"].iloc[0]
93
95
  range_x = scales.x.dimension()
94
96
  range_y = scales.y.dimension()
95
- x = np.linspace(range_x[0], range_x[1], params["n"])
96
- y = np.linspace(range_y[0], range_y[1], params["n"])
97
+ _x = np.linspace(range_x[0], range_x[1], params["n"])
98
+ _y = np.linspace(range_y[0], range_y[1], params["n"])
97
99
 
98
100
  # The grid must have a "similar" shape (n, p) to the var_data
99
- X, Y = np.meshgrid(x, y)
100
- var_data = np.array([data["x"].to_numpy(), data["y"].to_numpy()]).T
101
+ X, Y = np.meshgrid(_x, _y)
102
+ x = cast("FloatArrayLike", data["x"].to_numpy())
103
+ y = cast("FloatArrayLike", data["y"].to_numpy())
104
+ var_data = np.array([x, y]).T
101
105
  grid = np.array([X.flatten(), Y.flatten()]).T
102
106
  density = kde(var_data, grid, package, **kde_params)
103
107
 
104
108
  if params["contour"]:
105
- Z = density.reshape(len(x), len(y))
109
+ Z = density.reshape(len(_x), len(_y))
106
110
  data = contour_lines(X, Y, Z, params["levels"])
107
111
  # Each piece should have a distinct group
108
112
  groups = str(group) + "-00" + data["piece"].astype(str)
@@ -1,3 +1,5 @@
1
+ from typing import TYPE_CHECKING, cast
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
@@ -6,6 +8,9 @@ from ..mapping.evaluation import after_stat
6
8
  from .density import get_var_type, kde
7
9
  from .stat import stat
8
10
 
11
+ if TYPE_CHECKING:
12
+ from plotnine.typing import FloatArray
13
+
9
14
 
10
15
  @document
11
16
  class stat_pointdensity(stat):
@@ -67,8 +72,10 @@ class stat_pointdensity(stat):
67
72
  def compute_group(self, data, scales):
68
73
  package = self.params["package"]
69
74
  kde_params = self.params["kde_params"]
75
+ x = cast("FloatArray", data["x"].to_numpy())
76
+ y = cast("FloatArray", data["y"].to_numpy())
70
77
 
71
- var_data = np.array([data["x"].to_numpy(), data["y"].to_numpy()]).T
78
+ var_data = np.array([x, y]).T
72
79
  density = kde(var_data, var_data, package, **kde_params)
73
80
 
74
81
  data = pd.DataFrame(
plotnine/stats/stat_qq.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, cast
4
4
 
5
5
  import numpy as np
6
6
  import pandas as pd
@@ -11,9 +11,9 @@ from ..mapping.evaluation import after_stat
11
11
  from .stat import stat
12
12
 
13
13
  if TYPE_CHECKING:
14
- from typing import Any, Sequence
14
+ from typing import Any
15
15
 
16
- from plotnine.typing import FloatArray
16
+ from plotnine.typing import FloatArray, FloatArrayLike
17
17
 
18
18
 
19
19
  # Note: distribution should be a name from scipy.stat.distribution
@@ -76,7 +76,7 @@ class stat_qq(stat):
76
76
  }
77
77
 
78
78
  def compute_group(self, data, scales):
79
- sample = data["sample"].sort_values().to_numpy()
79
+ sample = cast("FloatArray", data["sample"].sort_values().to_numpy())
80
80
  theoretical = theoretical_qq(
81
81
  sample,
82
82
  self.params["distribution"],
@@ -93,7 +93,7 @@ def theoretical_qq(
93
93
  distribution: str,
94
94
  alpha: float,
95
95
  beta: float,
96
- quantiles: Sequence[float] | None,
96
+ quantiles: FloatArrayLike | None,
97
97
  distribution_params: dict[str, Any],
98
98
  ) -> FloatArray:
99
99
  """
@@ -1,3 +1,5 @@
1
+ from typing import TYPE_CHECKING, cast
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
@@ -6,6 +8,9 @@ from ..exceptions import PlotnineError
6
8
  from .stat import stat
7
9
  from .stat_qq import theoretical_qq
8
10
 
11
+ if TYPE_CHECKING:
12
+ from plotnine.typing import FloatArray
13
+
9
14
 
10
15
  @document
11
16
  class stat_qq_line(stat):
@@ -75,7 +80,7 @@ class stat_qq_line(stat):
75
80
  dparams = self.params["dparams"]
76
81
 
77
82
  # Compute theoretical values
78
- sample = data["sample"].sort_values().to_numpy()
83
+ sample = cast("FloatArray", data["sample"].sort_values().to_numpy())
79
84
  theoretical = theoretical_qq(
80
85
  sample,
81
86
  self.params["distribution"],
@@ -1,7 +1,9 @@
1
+ from typing import TYPE_CHECKING, cast
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
4
- from .._utils import array_kind, jitter, resolution
6
+ from .._utils import array_kind, jitter, nextafter_range, resolution
5
7
  from ..doctools import document
6
8
  from ..exceptions import PlotnineError
7
9
  from ..mapping.aes import has_groups
@@ -9,6 +11,9 @@ from .binning import breaks_from_bins, breaks_from_binwidth
9
11
  from .stat import stat
10
12
  from .stat_density import compute_density
11
13
 
14
+ if TYPE_CHECKING:
15
+ from plotnine.typing import FloatArray, IntArray
16
+
12
17
 
13
18
  @document
14
19
  class stat_sina(stat):
@@ -142,17 +147,6 @@ class stat_sina(stat):
142
147
  params = self.params
143
148
  maxwidth = params["maxwidth"]
144
149
  random_state = params["random_state"]
145
- fuzz = 1e-8
146
- y_dim = scales.y.dimension()
147
- y_dim_fuzzed = (y_dim[0] - fuzz, y_dim[1] + fuzz)
148
-
149
- if params["binwidth"] is not None:
150
- params["bins"] = breaks_from_binwidth(
151
- y_dim_fuzzed, params["binwidth"]
152
- )
153
- else:
154
- params["bins"] = breaks_from_bins(y_dim_fuzzed, params["bins"])
155
-
156
150
  data = super().compute_panel(data, scales)
157
151
 
158
152
  if not len(data):
@@ -198,8 +192,8 @@ class stat_sina(stat):
198
192
  return data
199
193
 
200
194
  def compute_group(self, data, scales):
195
+ binwidth = self.params["binwidth"]
201
196
  maxwidth = self.params["maxwidth"]
202
- bins = self.params["bins"]
203
197
  bin_limit = self.params["bin_limit"]
204
198
  weight = None
205
199
  y = data["y"]
@@ -228,8 +222,14 @@ class stat_sina(stat):
228
222
  data["density"] = densf(y)
229
223
  data["scaled"] = data["density"] / dens["density"].max()
230
224
  else:
225
+ expanded_y_range = nextafter_range(scales.y.dimension())
226
+ if binwidth is not None:
227
+ bins = breaks_from_binwidth(expanded_y_range, binwidth)
228
+ else:
229
+ bins = breaks_from_bins(expanded_y_range, self.params["bins"])
230
+
231
231
  # bin based estimation
232
- bin_index = pd.cut(y, bins, include_lowest=True, labels=False)
232
+ bin_index = pd.cut(y, bins, include_lowest=True, labels=False) # pyright: ignore[reportCallIssue,reportArgumentType]
233
233
  data["density"] = (
234
234
  pd.Series(bin_index)
235
235
  .groupby(bin_index)
@@ -254,19 +254,18 @@ class stat_sina(stat):
254
254
  def finish_layer(self, data):
255
255
  # Rescale x in case positions have been adjusted
256
256
  style = self.params["style"]
257
- x_mean = data["x"].to_numpy()
257
+ x_mean = cast("FloatArray", data["x"].to_numpy())
258
258
  x_mod = (data["xmax"] - data["xmin"]) / data["width"]
259
259
  data["x"] = data["x"] + data["x_diff"] * x_mod
260
- x = data["x"].to_numpy()
261
- even = data["group"].to_numpy() % 2 == 0
260
+ group = cast("IntArray", data["group"].to_numpy())
261
+ x = cast("FloatArray", data["x"].to_numpy())
262
+ even = group % 2 == 0
262
263
 
263
264
  def mirror_x(bool_idx):
264
265
  """
265
266
  Mirror x locations along the mean value
266
267
  """
267
- data.loc[bool_idx, "x"] = (
268
- 2 * x_mean[bool_idx] - data.loc[bool_idx, "x"]
269
- )
268
+ data.loc[bool_idx, "x"] = 2 * x_mean[bool_idx] - x[bool_idx]
270
269
 
271
270
  match style:
272
271
  case "left":
@@ -1,3 +1,5 @@
1
+ from typing import cast
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
@@ -314,8 +316,8 @@ class stat_summary(stat):
314
316
  summaries = []
315
317
  for (group, x), df in data.groupby(["group", "x"]):
316
318
  summary = func(df)
317
- summary["x"] = x
318
- summary["group"] = group
319
+ summary["x"] = x # pyright: ignore[reportCallIssue,reportArgumentType]
320
+ summary["group"] = cast("int", group)
319
321
  summary["n"] = len(df)
320
322
  unique = uniquecols(df)
321
323
  if "y" in unique:
@@ -1,3 +1,5 @@
1
+ from typing import TYPE_CHECKING, cast
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
@@ -9,6 +11,9 @@ from .binning import fuzzybreaks
9
11
  from .stat import stat
10
12
  from .stat_summary import make_summary_fun
11
13
 
14
+ if TYPE_CHECKING:
15
+ from plotnine.typing import IntArray
16
+
12
17
 
13
18
  @document
14
19
  class stat_summary_bin(stat):
@@ -157,7 +162,8 @@ class stat_summary_bin(stat):
157
162
  # This is a plyr::ddply
158
163
  out = groupby_apply(data, "bin", func_wrapper)
159
164
  centers = (breaks[:-1] + breaks[1:]) * 0.5
160
- bin_centers = centers[out["bin"].to_numpy()]
165
+ bin = cast("IntArray", out["bin"].to_numpy())
166
+ bin_centers = centers[bin]
161
167
  out["x"] = bin_centers
162
168
  out["bin"] += 1
163
169
  if isinstance(scales.x, scale_discrete):
@@ -37,6 +37,7 @@ class element_line(element_base):
37
37
  *,
38
38
  color: (
39
39
  str
40
+ | Sequence[str]
40
41
  | tuple[float, float, float]
41
42
  | tuple[float, float, float, float]
42
43
  | None
@@ -46,6 +47,7 @@ class element_line(element_base):
46
47
  lineend: Literal["butt", "projecting", "round"] | None = None,
47
48
  colour: (
48
49
  str
50
+ | Sequence[str]
49
51
  | tuple[float, float, float]
50
52
  | tuple[float, float, float, float]
51
53
  | None
@@ -86,10 +86,21 @@ class element_text(element_base):
86
86
  | None
87
87
  ) = None,
88
88
  size: float | Sequence[float] | None = None,
89
- ha: Literal["center", "left", "right"] | float | None = None,
89
+ ha: (
90
+ Literal["center", "left", "right"]
91
+ | float
92
+ | Sequence[Literal["center", "left", "right"] | float]
93
+ | None
94
+ ) = None,
90
95
  va: (
91
96
  Literal["center", "top", "bottom", "baseline", "center_baseline"]
92
97
  | float
98
+ | Sequence[
99
+ Literal[
100
+ "center", "top", "bottom", "baseline", "center_baseline"
101
+ ]
102
+ | float
103
+ ]
93
104
  | None
94
105
  ) = None,
95
106
  ma: Literal["center", "left", "right"] | float | None = None,
plotnine/themes/theme.py CHANGED
@@ -295,7 +295,13 @@ class theme:
295
295
  for th in self.T.values():
296
296
  th.apply(self)
297
297
 
298
- def setup(self, plot: ggplot):
298
+ def _setup(
299
+ self,
300
+ figure: Figure,
301
+ axs: list[Axes] | None = None,
302
+ title: str | None = None,
303
+ subtitle: str | None = None,
304
+ ):
299
305
  """
300
306
  Setup theme for applying
301
307
 
@@ -306,24 +312,14 @@ class theme:
306
312
 
307
313
  It also initialises where the artists to be themed will be stored.
308
314
  """
309
- self.plot = plot
310
- self.figure = plot.figure
311
- self.axs = plot.axs
312
- self.targets = ThemeTargets()
313
- self._add_default_themeable_properties()
314
- self.T.setup(self)
315
-
316
- def _add_default_themeable_properties(self):
317
- """
318
- Add default themeable properties that depend depend on the plot
315
+ self.figure = figure
316
+ self.axs = axs if axs is not None else []
319
317
 
320
- Some properties may be left unset (None) and their final values are
321
- best worked out dynamically after the plot has been built, but
322
- before the themeables are applied.
318
+ if title or subtitle:
319
+ self._smart_title_and_subtitle_ha(title, subtitle)
323
320
 
324
- This is where the theme is modified to add those values.
325
- """
326
- self._smart_title_and_subtitle_ha()
321
+ self.targets = ThemeTargets()
322
+ self.T.setup(self)
327
323
 
328
324
  @property
329
325
  def rcParams(self):
@@ -466,18 +462,16 @@ class theme:
466
462
  dpi = self.getp("dpi")
467
463
  return self + theme(dpi=dpi * 2)
468
464
 
469
- def _smart_title_and_subtitle_ha(self):
465
+ def _smart_title_and_subtitle_ha(
466
+ self, title: str | None, subtitle: str | None
467
+ ):
470
468
  """
471
469
  Smartly add the horizontal alignment for the title and subtitle
472
470
  """
473
471
  from .elements import element_text
474
472
 
475
- has_title = bool(
476
- self.plot.labels.get("title", "")
477
- ) and not self.T.is_blank("plot_title")
478
- has_subtitle = bool(
479
- self.plot.labels.get("subtitle", "")
480
- ) and not self.T.is_blank("plot_subtitle")
473
+ has_title = bool(title) and not self.T.is_blank("plot_title")
474
+ has_subtitle = bool(subtitle) and not self.T.is_blank("plot_subtitle")
481
475
 
482
476
  title_ha = self.getp(("plot_title", "ha"))
483
477
  subtitle_ha = self.getp(("plot_subtitle", "ha"))
@@ -2368,9 +2368,23 @@ class legend_text_position(themeable):
2368
2368
 
2369
2369
  Parameters
2370
2370
  ----------
2371
- theme_element : Literal["top", "bottom", "left", "right"] | None
2372
- Position of the legend key text. The default depends on the
2373
- position of the legend.
2371
+ theme_element : Literal["top", "bottom", "left", "right"] | \
2372
+ Sequence[Literal["top", "bottom"]] | \
2373
+ Sequence[Literal["left", "right"]] | \
2374
+ Literal["top-bottom", "bottom-top"] | \
2375
+ Literal["left-right", "right-left"] | \
2376
+ None
2377
+ Position of the legend key text.
2378
+ It must be compatible with the position of the legend e.g.
2379
+ when the legend is at the top or bottom, text can only be top
2380
+ or bottom as well.
2381
+ The default depends on the position of the legend.
2382
+ Use a sequence to specify the position of each text, or
2383
+ hyphenated values like `"left-right"` to alternate the position.
2384
+
2385
+ Notes
2386
+ -----
2387
+ Sequences and alternation only works well for colorbars.
2374
2388
  """
2375
2389
 
2376
2390
 
plotnine/typing.py CHANGED
@@ -122,8 +122,13 @@ GuideKind: TypeAlias = Literal["legend", "colorbar", "colourbar"]
122
122
  NoGuide: TypeAlias = Literal["none", False]
123
123
  VerticalJustification: TypeAlias = Literal["bottom", "center", "top"]
124
124
  HorizontalJustification: TypeAlias = Literal["left", "center", "right"]
125
+ Justification: TypeAlias = HorizontalJustification | VerticalJustification
126
+ HorizontalTextJustification: TypeAlias = HorizontalJustification
127
+ VerticalTextJustification: TypeAlias = (
128
+ VerticalJustification | Literal["baseline", "center_baseline"]
129
+ )
125
130
  TextJustification: TypeAlias = (
126
- VerticalJustification | HorizontalJustification | Literal["baseline"]
131
+ HorizontalTextJustification | VerticalTextJustification
127
132
  )
128
133
 
129
134
  # Type Variables
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotnine
3
- Version: 0.15.2
3
+ Version: 0.16.0a1
4
4
  Summary: A Grammar of Graphics for Python
5
5
  Author-email: Hassan Kibirige <has2k1@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -82,7 +82,7 @@ Requires-Dist: twine; extra == "dev"
82
82
  Requires-Dist: plotnine[typing]; extra == "dev"
83
83
  Requires-Dist: pre-commit; extra == "dev"
84
84
  Provides-Extra: typing
85
- Requires-Dist: pyright==1.1.404; extra == "typing"
85
+ Requires-Dist: pyright==1.1.408; extra == "typing"
86
86
  Requires-Dist: ipython; extra == "typing"
87
87
  Requires-Dist: pandas-stubs; extra == "typing"
88
88
  Dynamic: license-file