plotnine 0.15.0.dev2__py3-none-any.whl → 0.15.1__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 (140) hide show
  1. plotnine/__init__.py +2 -0
  2. plotnine/_mpl/layout_manager/_engine.py +1 -1
  3. plotnine/_mpl/layout_manager/_layout_items.py +128 -83
  4. plotnine/_mpl/layout_manager/_layout_tree.py +761 -310
  5. plotnine/_mpl/layout_manager/_spaces.py +320 -103
  6. plotnine/_mpl/patches.py +70 -34
  7. plotnine/_mpl/text.py +144 -63
  8. plotnine/_mpl/utils.py +1 -1
  9. plotnine/_utils/__init__.py +50 -107
  10. plotnine/_utils/context.py +78 -2
  11. plotnine/_utils/ipython.py +35 -51
  12. plotnine/_utils/quarto.py +21 -0
  13. plotnine/_utils/yippie.py +115 -0
  14. plotnine/composition/__init__.py +11 -0
  15. plotnine/composition/_beside.py +55 -0
  16. plotnine/composition/_compose.py +471 -0
  17. plotnine/composition/_plot_spacer.py +60 -0
  18. plotnine/composition/_stack.py +55 -0
  19. plotnine/coords/coord.py +3 -3
  20. plotnine/data/__init__.py +31 -0
  21. plotnine/data/anscombe-quartet.csv +45 -0
  22. plotnine/doctools.py +4 -4
  23. plotnine/facets/facet.py +4 -4
  24. plotnine/facets/strips.py +17 -28
  25. plotnine/geoms/annotate.py +13 -13
  26. plotnine/geoms/annotation_logticks.py +7 -8
  27. plotnine/geoms/annotation_stripes.py +6 -6
  28. plotnine/geoms/geom.py +60 -27
  29. plotnine/geoms/geom_abline.py +3 -2
  30. plotnine/geoms/geom_area.py +2 -2
  31. plotnine/geoms/geom_bar.py +11 -2
  32. plotnine/geoms/geom_bin_2d.py +6 -2
  33. plotnine/geoms/geom_blank.py +0 -3
  34. plotnine/geoms/geom_boxplot.py +8 -4
  35. plotnine/geoms/geom_col.py +8 -2
  36. plotnine/geoms/geom_count.py +6 -2
  37. plotnine/geoms/geom_crossbar.py +3 -3
  38. plotnine/geoms/geom_density_2d.py +6 -2
  39. plotnine/geoms/geom_dotplot.py +2 -2
  40. plotnine/geoms/geom_errorbar.py +2 -2
  41. plotnine/geoms/geom_errorbarh.py +2 -2
  42. plotnine/geoms/geom_histogram.py +1 -1
  43. plotnine/geoms/geom_hline.py +3 -2
  44. plotnine/geoms/geom_linerange.py +2 -2
  45. plotnine/geoms/geom_map.py +5 -5
  46. plotnine/geoms/geom_path.py +11 -12
  47. plotnine/geoms/geom_point.py +4 -5
  48. plotnine/geoms/geom_pointdensity.py +4 -0
  49. plotnine/geoms/geom_pointrange.py +3 -5
  50. plotnine/geoms/geom_polygon.py +2 -3
  51. plotnine/geoms/geom_qq.py +4 -0
  52. plotnine/geoms/geom_qq_line.py +4 -0
  53. plotnine/geoms/geom_quantile.py +4 -0
  54. plotnine/geoms/geom_raster.py +4 -5
  55. plotnine/geoms/geom_rect.py +3 -4
  56. plotnine/geoms/geom_ribbon.py +7 -7
  57. plotnine/geoms/geom_rug.py +1 -1
  58. plotnine/geoms/geom_segment.py +2 -2
  59. plotnine/geoms/geom_sina.py +3 -3
  60. plotnine/geoms/geom_smooth.py +7 -3
  61. plotnine/geoms/geom_step.py +2 -2
  62. plotnine/geoms/geom_text.py +2 -3
  63. plotnine/geoms/geom_violin.py +28 -8
  64. plotnine/geoms/geom_vline.py +3 -2
  65. plotnine/ggplot.py +64 -85
  66. plotnine/guides/guide.py +7 -10
  67. plotnine/guides/guide_colorbar.py +3 -3
  68. plotnine/guides/guide_legend.py +3 -3
  69. plotnine/guides/guides.py +6 -6
  70. plotnine/helpers.py +49 -0
  71. plotnine/iapi.py +28 -5
  72. plotnine/labels.py +3 -3
  73. plotnine/layer.py +36 -19
  74. plotnine/mapping/_atomic.py +178 -0
  75. plotnine/mapping/_env.py +13 -2
  76. plotnine/mapping/_eval_environment.py +85 -0
  77. plotnine/mapping/aes.py +91 -72
  78. plotnine/mapping/evaluation.py +7 -65
  79. plotnine/scales/__init__.py +2 -0
  80. plotnine/scales/limits.py +7 -7
  81. plotnine/scales/scale.py +3 -3
  82. plotnine/scales/scale_color.py +82 -18
  83. plotnine/scales/scale_continuous.py +6 -4
  84. plotnine/scales/scale_datetime.py +28 -14
  85. plotnine/scales/scale_discrete.py +1 -1
  86. plotnine/scales/scale_identity.py +21 -2
  87. plotnine/scales/scale_manual.py +8 -2
  88. plotnine/scales/scale_xy.py +2 -2
  89. plotnine/stats/binning.py +4 -1
  90. plotnine/stats/smoothers.py +23 -36
  91. plotnine/stats/stat.py +20 -32
  92. plotnine/stats/stat_bin.py +6 -5
  93. plotnine/stats/stat_bin_2d.py +11 -9
  94. plotnine/stats/stat_bindot.py +13 -16
  95. plotnine/stats/stat_boxplot.py +6 -6
  96. plotnine/stats/stat_count.py +6 -9
  97. plotnine/stats/stat_density.py +7 -10
  98. plotnine/stats/stat_density_2d.py +12 -8
  99. plotnine/stats/stat_ecdf.py +7 -6
  100. plotnine/stats/stat_ellipse.py +9 -6
  101. plotnine/stats/stat_function.py +10 -8
  102. plotnine/stats/stat_hull.py +6 -3
  103. plotnine/stats/stat_identity.py +5 -2
  104. plotnine/stats/stat_pointdensity.py +5 -7
  105. plotnine/stats/stat_qq.py +46 -20
  106. plotnine/stats/stat_qq_line.py +16 -11
  107. plotnine/stats/stat_quantile.py +15 -9
  108. plotnine/stats/stat_sina.py +45 -14
  109. plotnine/stats/stat_smooth.py +8 -10
  110. plotnine/stats/stat_sum.py +5 -2
  111. plotnine/stats/stat_summary.py +7 -10
  112. plotnine/stats/stat_summary_bin.py +11 -14
  113. plotnine/stats/stat_unique.py +5 -2
  114. plotnine/stats/stat_ydensity.py +8 -11
  115. plotnine/themes/elements/__init__.py +2 -1
  116. plotnine/themes/elements/element_line.py +17 -9
  117. plotnine/themes/elements/margin.py +64 -1
  118. plotnine/themes/theme.py +9 -1
  119. plotnine/themes/theme_538.py +0 -1
  120. plotnine/themes/theme_bw.py +0 -1
  121. plotnine/themes/theme_dark.py +0 -1
  122. plotnine/themes/theme_gray.py +6 -5
  123. plotnine/themes/theme_light.py +1 -1
  124. plotnine/themes/theme_matplotlib.py +5 -5
  125. plotnine/themes/theme_seaborn.py +7 -4
  126. plotnine/themes/theme_void.py +9 -8
  127. plotnine/themes/theme_xkcd.py +0 -1
  128. plotnine/themes/themeable.py +110 -32
  129. plotnine/typing.py +17 -6
  130. plotnine/watermark.py +3 -3
  131. {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/METADATA +13 -6
  132. plotnine-0.15.1.dist-info/RECORD +221 -0
  133. {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/WHEEL +1 -1
  134. plotnine/plot_composition/__init__.py +0 -10
  135. plotnine/plot_composition/_compose.py +0 -436
  136. plotnine/plot_composition/_spacer.py +0 -32
  137. plotnine-0.15.0.dev2.dist-info/RECORD +0 -214
  138. /plotnine/{plot_composition → composition}/_plotspec.py +0 -0
  139. {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/licenses/LICENSE +0 -0
  140. {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/top_level.txt +0 -0
@@ -14,8 +14,9 @@ from __future__ import annotations
14
14
  from abc import ABC
15
15
  from dataclasses import dataclass, field, fields
16
16
  from functools import cached_property
17
- from typing import TYPE_CHECKING
17
+ from typing import TYPE_CHECKING, cast
18
18
 
19
+ from plotnine.exceptions import PlotnineError
19
20
  from plotnine.facets import facet_grid, facet_null, facet_wrap
20
21
 
21
22
  from ._layout_items import LayoutItems
@@ -26,7 +27,8 @@ if TYPE_CHECKING:
26
27
 
27
28
  from plotnine import ggplot
28
29
  from plotnine._mpl.gridspec import p9GridSpec
29
-
30
+ from plotnine.iapi import outside_legend
31
+ from plotnine.typing import Side
30
32
 
31
33
  # Note
32
34
  # Margins around the plot are specified in figure coordinates
@@ -69,16 +71,12 @@ class _side_spaces(ABC):
69
71
  """
70
72
 
71
73
  items: LayoutItems
72
- alignment_margin: float = 0
73
- """
74
- A margin added to align this plot with others in a composition
75
-
76
- This value is calculated during the layout process in a tree structure
77
- that has convenient access to the sides/edges of the panels in the
78
- composition.
79
- """
80
74
 
81
75
  def __post_init__(self):
76
+ self.side: Side = cast("Side", self.__class__.__name__[:-7])
77
+ """
78
+ Side of the panel(s) that this class applies to
79
+ """
82
80
  self._calculate()
83
81
 
84
82
  def _calculate(self):
@@ -132,17 +130,21 @@ class _side_spaces(ABC):
132
130
  values e.g. 0.2, instead of just left, right, top, bottom &
133
131
  center.
134
132
  """
135
- return (0, 0)
133
+ if not self.has_legend:
134
+ return (0, 0)
135
+
136
+ ol: outside_legend = getattr(self.items.legends, self.side)
137
+ return self.items.calc.size(ol.box)
136
138
 
137
139
  @cached_property
138
- def _legend_width(self) -> float:
140
+ def legend_width(self) -> float:
139
141
  """
140
142
  Return width of legend in figure coordinates
141
143
  """
142
144
  return self._legend_size[0]
143
145
 
144
146
  @cached_property
145
- def _legend_height(self) -> float:
147
+ def legend_height(self) -> float:
146
148
  """
147
149
  Return height of legend in figure coordinates
148
150
  """
@@ -204,6 +206,81 @@ class _side_spaces(ABC):
204
206
  """
205
207
  return self.offset + rel_value
206
208
 
209
+ @property
210
+ def has_tag(self) -> bool:
211
+ """
212
+ Return True if the space/margin to this side of the panel has a tag
213
+
214
+ If it does, then it will be included in the layout
215
+ """
216
+ getp = self.items.plot.theme.getp
217
+ return getp("plot_tag_location") == "margin" and self.side in getp(
218
+ "plot_tag_position"
219
+ )
220
+
221
+ @property
222
+ def has_legend(self) -> bool:
223
+ """
224
+ Return True if the space/margin to this side of the panel has a legend
225
+
226
+ If it does, then it will be included in the layout
227
+ """
228
+ if not self.items.legends:
229
+ return False
230
+ return hasattr(self.items.legends, self.side)
231
+
232
+ @property
233
+ def tag_width(self) -> float:
234
+ """
235
+ The width of the tag including the margins
236
+
237
+ The value is zero except if all these are true:
238
+ - The tag is in the margin `theme(plot_tag_position = "margin")`
239
+ - The tag at one one of the the following locations;
240
+ left, right, topleft, topright, bottomleft or bottomright
241
+ """
242
+ return 0
243
+
244
+ @property
245
+ def tag_height(self) -> float:
246
+ """
247
+ The height of the tag including the margins
248
+
249
+ The value is zero except if all these are true:
250
+ - The tag is in the margin `theme(plot_tag_position = "margin")`
251
+ - The tag at one one of the the following locations;
252
+ top, bottom, topleft, topright, bottomleft or bottomright
253
+ """
254
+ return 0
255
+
256
+ @property
257
+ def axis_title_clearance(self) -> float:
258
+ """
259
+ The distance between the axis title and the panel
260
+
261
+ Figure
262
+ ----------------------------
263
+ | Panel |
264
+ | ----------- |
265
+ | | | |
266
+ | | | |
267
+ | Y<--->| | |
268
+ | | | |
269
+ | | | |
270
+ | ----------- |
271
+ | |
272
+ ----------------------------
273
+
274
+ We use this value to when aligning axis titles in a
275
+ plot composition.
276
+ """
277
+
278
+ try:
279
+ return self.total - self.sum_upto("axis_title_alignment")
280
+ except AttributeError as err:
281
+ # There is probably an error in in the layout manager
282
+ raise PlotnineError("Side has no axis title") from err
283
+
207
284
 
208
285
  @dataclass
209
286
  class left_spaces(_side_spaces):
@@ -214,15 +291,66 @@ class left_spaces(_side_spaces):
214
291
  """
215
292
 
216
293
  plot_margin: float = 0
294
+ tag_alignment: float = 0
295
+ """
296
+ Space added to align the tag in this plot with others in a composition
297
+
298
+ This value is calculated during the layout process, and it ensures that
299
+ all tags on this side of the plot take up the same amount of space in
300
+ the margin. e.g. from
301
+
302
+ ------------------------------------
303
+ | plot_margin | tag | artists |
304
+ |------------------------------------|
305
+ | plot_margin | A long tag | artists |
306
+ ------------------------------------
307
+
308
+ to
309
+
310
+ ------------------------------------
311
+ | plot_margin | tag | artists |
312
+ |------------------------------------|
313
+ | plot_margin | A long tag | artists |
314
+ ------------------------------------
315
+
316
+ And the tag is justified within that space e.g if ha="left" we get
317
+
318
+ ------------------------------------
319
+ | plot_margin | tag | artists |
320
+ |------------------------------------|
321
+ | plot_margin | A long tag | artists |
322
+ ------------------------------------
323
+
324
+ So, contrary to the order in which the space items are laid out, the
325
+ tag_alignment does not necessarily come before the plot_tag.
326
+ """
217
327
  plot_tag_margin_left: float = 0
218
328
  plot_tag: float = 0
219
329
  plot_tag_margin_right: float = 0
330
+ margin_alignment: float = 0
331
+ """
332
+ Space added to align this plot with others in a composition
333
+
334
+ This value is calculated during the layout process in a tree structure
335
+ that has convenient access to the sides/edges of the panels in the
336
+ composition.
337
+ """
220
338
  legend: float = 0
221
339
  legend_box_spacing: float = 0
222
340
  axis_title_y_margin_left: float = 0
223
341
  axis_title_y: float = 0
224
342
  axis_title_y_margin_right: float = 0
343
+ axis_title_alignment: float = 0
344
+ """
345
+ Space added to align the axis title with others in a composition
346
+
347
+ This value is calculated during the layout process. The amount is
348
+ the difference between the largest and smallest axis_title_clearance
349
+ among the items in the composition.
350
+ """
351
+ axis_text_y_margin_left: float = 0
225
352
  axis_text_y: float = 0
353
+ axis_text_y_margin_right: float = 0
226
354
  axis_ticks_y: float = 0
227
355
 
228
356
  def _calculate(self):
@@ -230,22 +358,16 @@ class left_spaces(_side_spaces):
230
358
  calc = self.items.calc
231
359
  items = self.items
232
360
 
233
- # If the plot_tag is in the margin, it is included in the layout.
234
- # So we make space for it, including any margins it may have.
235
- plot_tag_in_layout = theme.getp(
236
- "plot_tag_location"
237
- ) == "margin" and "left" in theme.getp("plot_tag_position")
238
-
239
361
  self.plot_margin = theme.getp("plot_margin_left")
240
362
 
241
- if items.plot_tag and plot_tag_in_layout:
363
+ if self.has_tag and items.plot_tag:
242
364
  m = theme.get_margin("plot_tag").fig
243
365
  self.plot_tag_margin_left = m.l
244
366
  self.plot_tag = calc.width(items.plot_tag)
245
367
  self.plot_tag_margin_right = m.r
246
368
 
247
369
  if items.legends and items.legends.left:
248
- self.legend = self._legend_width
370
+ self.legend = self.legend_width
249
371
  self.legend_box_spacing = theme.getp("legend_box_spacing")
250
372
 
251
373
  if items.axis_title_y:
@@ -256,6 +378,11 @@ class left_spaces(_side_spaces):
256
378
 
257
379
  # Account for the space consumed by the axis
258
380
  self.axis_text_y = items.axis_text_y_max_width_at("first_col")
381
+ if self.axis_text_y:
382
+ m = theme.get_margin("axis_text_y").fig
383
+ self.axis_text_y_margin_left = m.l
384
+ self.axis_text_y_margin_right = m.r
385
+
259
386
  self.axis_ticks_y = items.axis_ticks_y_max_width_at("first_col")
260
387
 
261
388
  # Adjust plot_margin to make room for ylabels that protude well
@@ -266,13 +393,6 @@ class left_spaces(_side_spaces):
266
393
  if adjustment > 0:
267
394
  self.plot_margin += adjustment
268
395
 
269
- @cached_property
270
- def _legend_size(self) -> tuple[float, float]:
271
- if not (self.items.legends and self.items.legends.left):
272
- return (0, 0)
273
-
274
- return self.items.calc.size(self.items.legends.left.box)
275
-
276
396
  @property
277
397
  def offset(self) -> float:
278
398
  """
@@ -302,18 +422,18 @@ class left_spaces(_side_spaces):
302
422
  return self.to_figure_space(self.sum_incl(item))
303
423
 
304
424
  @property
305
- def left_relative(self):
425
+ def panel_left_relative(self):
306
426
  """
307
427
  Left (relative to the gridspec) of the panels in figure dimensions
308
428
  """
309
429
  return self.total
310
430
 
311
431
  @property
312
- def left(self):
432
+ def panel_left(self):
313
433
  """
314
434
  Left of the panels in figure space
315
435
  """
316
- return self.to_figure_space(self.left_relative)
436
+ return self.to_figure_space(self.panel_left_relative)
317
437
 
318
438
  @property
319
439
  def plot_left(self):
@@ -322,6 +442,17 @@ class left_spaces(_side_spaces):
322
442
  """
323
443
  return self.x1("legend")
324
444
 
445
+ @property
446
+ def tag_width(self):
447
+ """
448
+ The width of the tag including the margins
449
+ """
450
+ return (
451
+ self.plot_tag_margin_left
452
+ + self.plot_tag
453
+ + self.plot_tag_margin_right
454
+ )
455
+
325
456
 
326
457
  @dataclass
327
458
  class right_spaces(_side_spaces):
@@ -332,36 +463,33 @@ class right_spaces(_side_spaces):
332
463
  """
333
464
 
334
465
  plot_margin: float = 0
466
+ tag_alignment: float = 0
335
467
  plot_tag_margin_right: float = 0
336
468
  plot_tag: float = 0
337
469
  plot_tag_margin_left: float = 0
470
+ margin_alignment: float = 0
338
471
  legend: float = 0
339
472
  legend_box_spacing: float = 0
340
- strip_text_y_width_right: float = 0
473
+ strip_text_y_extra_width: float = 0
341
474
 
342
475
  def _calculate(self):
343
476
  items = self.items
344
477
  theme = self.items.plot.theme
345
478
  calc = self.items.calc
346
- # If the plot_tag is in the margin, it is included in the layout.
347
- # So we make space for it, including any margins it may have.
348
- plot_tag_in_layout = theme.getp(
349
- "plot_tag_location"
350
- ) == "margin" and "right" in theme.getp("plot_tag_position")
351
479
 
352
480
  self.plot_margin = theme.getp("plot_margin_right")
353
481
 
354
- if items.plot_tag and plot_tag_in_layout:
482
+ if self.has_tag and items.plot_tag:
355
483
  m = theme.get_margin("plot_tag").fig
356
484
  self.plot_tag_margin_right = m.r
357
485
  self.plot_tag = calc.width(items.plot_tag)
358
486
  self.plot_tag_margin_left = m.l
359
487
 
360
488
  if items.legends and items.legends.right:
361
- self.legend = self._legend_width
489
+ self.legend = self.legend_width
362
490
  self.legend_box_spacing = theme.getp("legend_box_spacing")
363
491
 
364
- self.strip_text_y_width_right = items.strip_text_y_width("right")
492
+ self.strip_text_y_extra_width = items.strip_text_y_extra_width("right")
365
493
 
366
494
  # Adjust plot_margin to make room for ylabels that protude well
367
495
  # beyond the axes
@@ -371,13 +499,6 @@ class right_spaces(_side_spaces):
371
499
  if adjustment > 0:
372
500
  self.plot_margin += adjustment
373
501
 
374
- @cached_property
375
- def _legend_size(self) -> tuple[float, float]:
376
- if not (self.items.legends and self.items.legends.right):
377
- return (0, 0)
378
-
379
- return self.items.calc.size(self.items.legends.right.box)
380
-
381
502
  @property
382
503
  def offset(self):
383
504
  """
@@ -407,18 +528,18 @@ class right_spaces(_side_spaces):
407
528
  return self.to_figure_space(1 - self.sum_upto(item))
408
529
 
409
530
  @property
410
- def right_relative(self):
531
+ def panel_right_relative(self):
411
532
  """
412
533
  Right (relative to the gridspec) of the panels in figure dimensions
413
534
  """
414
535
  return 1 - self.total
415
536
 
416
537
  @property
417
- def right(self):
538
+ def panel_right(self):
418
539
  """
419
540
  Right of the panels in figure space
420
541
  """
421
- return self.to_figure_space(self.right_relative)
542
+ return self.to_figure_space(self.panel_right_relative)
422
543
 
423
544
  @property
424
545
  def plot_right(self):
@@ -427,6 +548,17 @@ class right_spaces(_side_spaces):
427
548
  """
428
549
  return self.x2("legend")
429
550
 
551
+ @property
552
+ def tag_width(self):
553
+ """
554
+ The width of the tag including the margins
555
+ """
556
+ return (
557
+ self.plot_tag_margin_right
558
+ + self.plot_tag
559
+ + self.plot_tag_margin_left
560
+ )
561
+
430
562
 
431
563
  @dataclass
432
564
  class top_spaces(_side_spaces):
@@ -437,9 +569,11 @@ class top_spaces(_side_spaces):
437
569
  """
438
570
 
439
571
  plot_margin: float = 0
572
+ tag_alignment: float = 0
440
573
  plot_tag_margin_top: float = 0
441
574
  plot_tag: float = 0
442
575
  plot_tag_margin_bottom: float = 0
576
+ margin_alignment: float = 0
443
577
  plot_title_margin_top: float = 0
444
578
  plot_title: float = 0
445
579
  plot_title_margin_bottom: float = 0
@@ -448,7 +582,7 @@ class top_spaces(_side_spaces):
448
582
  plot_subtitle_margin_bottom: float = 0
449
583
  legend: float = 0
450
584
  legend_box_spacing: float = 0
451
- strip_text_x_height_top: float = 0
585
+ strip_text_x_extra_height: float = 0
452
586
 
453
587
  def _calculate(self):
454
588
  items = self.items
@@ -456,15 +590,10 @@ class top_spaces(_side_spaces):
456
590
  calc = self.items.calc
457
591
  W, H = theme.getp("figure_size")
458
592
  F = W / H
459
- # If the plot_tag is in the margin, it is included in the layout.
460
- # So we make space for it, including any margins it may have.
461
- plot_tag_in_layout = theme.getp(
462
- "plot_tag_location"
463
- ) == "margin" and "top" in theme.getp("plot_tag_position")
464
593
 
465
594
  self.plot_margin = theme.getp("plot_margin_top") * F
466
595
 
467
- if items.plot_tag and plot_tag_in_layout:
596
+ if self.has_tag and items.plot_tag:
468
597
  m = theme.get_margin("plot_tag").fig
469
598
  self.plot_tag_margin_top = m.t
470
599
  self.plot_tag = calc.height(items.plot_tag)
@@ -483,10 +612,10 @@ class top_spaces(_side_spaces):
483
612
  self.plot_subtitle_margin_bottom = m.b * F
484
613
 
485
614
  if items.legends and items.legends.top:
486
- self.legend = self._legend_height
615
+ self.legend = self.legend_height
487
616
  self.legend_box_spacing = theme.getp("legend_box_spacing") * F
488
617
 
489
- self.strip_text_x_height_top = items.strip_text_x_height("top")
618
+ self.strip_text_x_extra_height = items.strip_text_x_extra_height("top")
490
619
 
491
620
  # Adjust plot_margin to make room for ylabels that protude well
492
621
  # beyond the axes
@@ -496,13 +625,6 @@ class top_spaces(_side_spaces):
496
625
  if adjustment > 0:
497
626
  self.plot_margin += adjustment
498
627
 
499
- @cached_property
500
- def _legend_size(self) -> tuple[float, float]:
501
- if not (self.items.legends and self.items.legends.top):
502
- return (0, 0)
503
-
504
- return self.items.calc.size(self.items.legends.top.box)
505
-
506
628
  @property
507
629
  def offset(self) -> float:
508
630
  """
@@ -535,18 +657,18 @@ class top_spaces(_side_spaces):
535
657
  return self.to_figure_space(1 - self.sum_upto(item))
536
658
 
537
659
  @property
538
- def top_relative(self):
660
+ def panel_top_relative(self):
539
661
  """
540
662
  Top (relative to the gridspec) of the panels in figure dimensions
541
663
  """
542
664
  return 1 - self.total
543
665
 
544
666
  @property
545
- def top(self):
667
+ def panel_top(self):
546
668
  """
547
669
  Top of the panels in figure space
548
670
  """
549
- return self.to_figure_space(self.top_relative)
671
+ return self.to_figure_space(self.panel_top_relative)
550
672
 
551
673
  @property
552
674
  def plot_top(self):
@@ -555,6 +677,17 @@ class top_spaces(_side_spaces):
555
677
  """
556
678
  return self.y2("legend")
557
679
 
680
+ @property
681
+ def tag_height(self):
682
+ """
683
+ The height of the tag including the margins
684
+ """
685
+ return (
686
+ self.plot_tag_margin_top
687
+ + self.plot_tag
688
+ + self.plot_tag_margin_bottom
689
+ )
690
+
558
691
 
559
692
  @dataclass
560
693
  class bottom_spaces(_side_spaces):
@@ -565,9 +698,11 @@ class bottom_spaces(_side_spaces):
565
698
  """
566
699
 
567
700
  plot_margin: float = 0
701
+ tag_alignment: float = 0
568
702
  plot_tag_margin_bottom: float = 0
569
703
  plot_tag: float = 0
570
704
  plot_tag_margin_top: float = 0
705
+ margin_alignment: float = 0
571
706
  plot_caption_margin_bottom: float = 0
572
707
  plot_caption: float = 0
573
708
  plot_caption_margin_top: float = 0
@@ -576,7 +711,18 @@ class bottom_spaces(_side_spaces):
576
711
  axis_title_x_margin_bottom: float = 0
577
712
  axis_title_x: float = 0
578
713
  axis_title_x_margin_top: float = 0
714
+ axis_title_alignment: float = 0
715
+ """
716
+ Space added to align the axis title with others in a composition
717
+
718
+ This value is calculated during the layout process in a tree structure
719
+ that has convenient access to the sides/edges of the panels in the
720
+ composition. It's amount is the difference in height between this axis
721
+ text (and it's margins) and the tallest axis text (and it's margin).
722
+ """
723
+ axis_text_x_margin_bottom: float = 0
579
724
  axis_text_x: float = 0
725
+ axis_text_x_margin_top: float = 0
580
726
  axis_ticks_x: float = 0
581
727
 
582
728
  def _calculate(self):
@@ -585,15 +731,10 @@ class bottom_spaces(_side_spaces):
585
731
  calc = self.items.calc
586
732
  W, H = theme.getp("figure_size")
587
733
  F = W / H
588
- # If the plot_tag is in the margin, it is included in the layout.
589
- # So we make space for it, including any margins it may have.
590
- plot_tag_in_layout = theme.getp(
591
- "plot_tag_location"
592
- ) == "margin" and "bottom" in theme.getp("plot_tag_position")
593
734
 
594
735
  self.plot_margin = theme.getp("plot_margin_bottom") * F
595
736
 
596
- if items.plot_tag and plot_tag_in_layout:
737
+ if self.has_tag and items.plot_tag:
597
738
  m = theme.get_margin("plot_tag").fig
598
739
  self.plot_tag_margin_bottom = m.b
599
740
  self.plot_tag = calc.height(items.plot_tag)
@@ -606,7 +747,7 @@ class bottom_spaces(_side_spaces):
606
747
  self.plot_caption_margin_top = m.t * F
607
748
 
608
749
  if items.legends and items.legends.bottom:
609
- self.legend = self._legend_height
750
+ self.legend = self.legend_height
610
751
  self.legend_box_spacing = theme.getp("legend_box_spacing") * F
611
752
 
612
753
  if items.axis_title_x:
@@ -616,8 +757,12 @@ class bottom_spaces(_side_spaces):
616
757
  self.axis_title_x_margin_top = m.t * F
617
758
 
618
759
  # Account for the space consumed by the axis
619
- self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row")
620
760
  self.axis_text_x = items.axis_text_x_max_height_at("last_row")
761
+ if self.axis_text_x:
762
+ m = theme.get_margin("axis_text_x").fig
763
+ self.axis_text_x_margin_bottom = m.b
764
+ self.axis_text_x_margin_top = m.t
765
+ self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row")
621
766
 
622
767
  # Adjust plot_margin to make room for ylabels that protude well
623
768
  # beyond the axes
@@ -627,13 +772,6 @@ class bottom_spaces(_side_spaces):
627
772
  if adjustment > 0:
628
773
  self.plot_margin += adjustment
629
774
 
630
- @cached_property
631
- def _legend_size(self) -> tuple[float, float]:
632
- if not (self.items.legends and self.items.legends.bottom):
633
- return (0, 0)
634
-
635
- return self.items.calc.size(self.items.legends.bottom.box)
636
-
637
775
  @property
638
776
  def offset(self) -> float:
639
777
  """
@@ -666,18 +804,18 @@ class bottom_spaces(_side_spaces):
666
804
  return self.to_figure_space(self.sum_incl(item))
667
805
 
668
806
  @property
669
- def bottom_relative(self):
807
+ def panel_bottom_relative(self):
670
808
  """
671
809
  Bottom (relative to the gridspec) of the panels in figure dimensions
672
810
  """
673
811
  return self.total
674
812
 
675
813
  @property
676
- def bottom(self):
814
+ def panel_bottom(self):
677
815
  """
678
816
  Bottom of the panels in figure space
679
817
  """
680
- return self.to_figure_space(self.bottom_relative)
818
+ return self.to_figure_space(self.panel_bottom_relative)
681
819
 
682
820
  @property
683
821
  def plot_bottom(self):
@@ -686,6 +824,17 @@ class bottom_spaces(_side_spaces):
686
824
  """
687
825
  return self.y1("legend")
688
826
 
827
+ @property
828
+ def tag_height(self):
829
+ """
830
+ The height of the tag including the margins
831
+ """
832
+ return (
833
+ self.plot_tag_margin_bottom
834
+ + self.plot_tag
835
+ + self.plot_tag_margin_top
836
+ )
837
+
689
838
 
690
839
  @dataclass
691
840
  class LayoutSpaces:
@@ -736,7 +885,7 @@ class LayoutSpaces:
736
885
  sw: float = field(init=False, default=0)
737
886
  """vertical spacing btn panels w.r.t figure"""
738
887
 
739
- gsparams: GridSpecParams = field(init=False)
888
+ gsparams: GridSpecParams = field(init=False, repr=False)
740
889
  """Grid spacing btn panels w.r.t figure"""
741
890
 
742
891
  def __post_init__(self):
@@ -789,14 +938,76 @@ class LayoutSpaces:
789
938
  """
790
939
  Width [figure dimensions] of panels
791
940
  """
792
- return self.r.right - self.l.left
941
+ return self.r.panel_right - self.l.panel_left
793
942
 
794
943
  @property
795
944
  def panel_height(self) -> float:
796
945
  """
797
946
  Height [figure dimensions] of panels
798
947
  """
799
- return self.t.top - self.b.bottom
948
+ return self.t.panel_top - self.b.panel_bottom
949
+
950
+ @property
951
+ def tag_width(self) -> float:
952
+ """
953
+ Width [figure dimensions] of space taken up by the tag
954
+ """
955
+ # Atleast one of these is zero
956
+ return max(self.l.tag_width, self.r.tag_width)
957
+
958
+ @property
959
+ def tag_height(self) -> float:
960
+ """
961
+ Height [figure dimensions] of space taken up by the tag
962
+ """
963
+ # Atleast one of these is zero
964
+ return max(self.t.tag_height, self.b.tag_height)
965
+
966
+ @property
967
+ def left_tag_width(self) -> float:
968
+ """
969
+ Width [figure dimensions] of space taken up by a left tag
970
+ """
971
+ return self.l.tag_width
972
+
973
+ @property
974
+ def right_tag_width(self) -> float:
975
+ """
976
+ Width [figure dimensions] of space taken up by a right tag
977
+ """
978
+ return self.r.tag_width
979
+
980
+ @property
981
+ def top_tag_height(self) -> float:
982
+ """
983
+ Width [figure dimensions] of space taken up by a top tag
984
+ """
985
+ return self.t.tag_height
986
+
987
+ @property
988
+ def bottom_tag_height(self) -> float:
989
+ """
990
+ Height [figure dimensions] of space taken up by a bottom tag
991
+ """
992
+ return self.b.tag_height
993
+
994
+ @property
995
+ def left_axis_title_clearance(self) -> float:
996
+ """
997
+ Distance between the left y-axis title and the panel
998
+
999
+ In figure dimensions.
1000
+ """
1001
+ return self.l.axis_title_clearance
1002
+
1003
+ @property
1004
+ def bottom_axis_title_clearance(self) -> float:
1005
+ """
1006
+ Distance between the bottom x-axis title and the panel
1007
+
1008
+ In figure dimensions.
1009
+ """
1010
+ return self.b.axis_title_clearance
800
1011
 
801
1012
  def increase_horizontal_plot_margin(self, dw: float):
802
1013
  """
@@ -834,8 +1045,8 @@ class LayoutSpaces:
834
1045
 
835
1046
  This is the area in which the panels are drawn.
836
1047
  """
837
- x1, x2 = self.l.left, self.r.right
838
- y1, y2 = self.b.bottom, self.t.top
1048
+ x1, x2 = self.l.panel_left, self.r.panel_right
1049
+ y1, y2 = self.b.panel_bottom, self.t.panel_top
839
1050
  return ((x1, y1), (x2, y2))
840
1051
 
841
1052
  def _calculate_panel_spacing(self) -> GridSpecParams:
@@ -856,10 +1067,10 @@ class LayoutSpaces:
856
1067
  raise TypeError(f"Unknown type of facet: {type(self.plot.facet)}")
857
1068
 
858
1069
  return GridSpecParams(
859
- self.l.left_relative,
860
- self.r.right_relative,
861
- self.t.top_relative,
862
- self.b.bottom_relative,
1070
+ self.l.panel_left_relative,
1071
+ self.r.panel_right_relative,
1072
+ self.t.panel_top_relative,
1073
+ self.b.panel_bottom_relative,
863
1074
  wspace,
864
1075
  hspace,
865
1076
  )
@@ -873,6 +1084,9 @@ class LayoutSpaces:
873
1084
  ncol = self.plot.facet.ncol
874
1085
  nrow = self.plot.facet.nrow
875
1086
 
1087
+ left, right = self.l.panel_left, self.r.panel_right
1088
+ top, bottom = self.t.panel_top, self.b.panel_bottom
1089
+
876
1090
  # Both spacings are specified as fractions of the figure width
877
1091
  # Multiply the vertical by (W/H) so that the gullies along both
878
1092
  # directions are equally spaced.
@@ -880,8 +1094,8 @@ class LayoutSpaces:
880
1094
  self.sh = theme.getp("panel_spacing_y") * self.W / self.H
881
1095
 
882
1096
  # width and height of axes as fraction of figure width & height
883
- self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
884
- self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
1097
+ self.w = ((right - left) - self.sw * (ncol - 1)) / ncol
1098
+ self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow
885
1099
 
886
1100
  # Spacing as fraction of axes width & height
887
1101
  wspace = self.sw / self.w
@@ -898,6 +1112,9 @@ class LayoutSpaces:
898
1112
  ncol = facet.ncol
899
1113
  nrow = facet.nrow
900
1114
 
1115
+ left, right = self.l.panel_left, self.r.panel_right
1116
+ top, bottom = self.t.panel_top, self.b.panel_bottom
1117
+
901
1118
  # Both spacings are specified as fractions of the figure width
902
1119
  self.sw = theme.getp("panel_spacing_x")
903
1120
  self.sh = theme.getp("panel_spacing_y") * self.W / self.H
@@ -914,7 +1131,7 @@ class LayoutSpaces:
914
1131
  # Only interested in the proportion of the strip that
915
1132
  # does not overlap with the panel
916
1133
  if strip_align_x > -1:
917
- self.sh += self.t.strip_text_x_height_top * (1 + strip_align_x)
1134
+ self.sh += self.t.strip_text_x_extra_height * (1 + strip_align_x)
918
1135
 
919
1136
  if facet.free["x"]:
920
1137
  self.sh += self.items.axis_text_x_max_height_at(
@@ -926,8 +1143,8 @@ class LayoutSpaces:
926
1143
  ) + self.items.axis_ticks_y_max_width_at("all")
927
1144
 
928
1145
  # width and height of axes as fraction of figure width & height
929
- self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
930
- self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
1146
+ self.w = ((right - left) - self.sw * (ncol - 1)) / ncol
1147
+ self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow
931
1148
 
932
1149
  # Spacing as fraction of axes width & height
933
1150
  wspace = self.sw / self.w
@@ -938,8 +1155,8 @@ class LayoutSpaces:
938
1155
  """
939
1156
  Calculate spacing parts for facet_null
940
1157
  """
941
- self.w = self.r.right - self.l.left
942
- self.h = self.t.top - self.b.bottom
1158
+ self.w = self.r.panel_right - self.l.panel_left
1159
+ self.h = self.t.panel_top - self.b.panel_bottom
943
1160
  self.sw = 0
944
1161
  self.sh = 0
945
1162
  return 0, 0