plotnine 0.15.0.dev3__py3-none-any.whl → 0.15.2__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 (139) 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 +126 -41
  4. plotnine/_mpl/layout_manager/_layout_tree.py +712 -314
  5. plotnine/_mpl/layout_manager/_spaces.py +305 -101
  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 +26 -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 +1 -0
  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 +2 -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 +8 -5
  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 +1 -1
  77. plotnine/mapping/aes.py +85 -49
  78. plotnine/scales/__init__.py +2 -0
  79. plotnine/scales/limits.py +7 -7
  80. plotnine/scales/scale.py +3 -3
  81. plotnine/scales/scale_color.py +82 -18
  82. plotnine/scales/scale_continuous.py +6 -4
  83. plotnine/scales/scale_datetime.py +28 -14
  84. plotnine/scales/scale_discrete.py +1 -1
  85. plotnine/scales/scale_identity.py +21 -2
  86. plotnine/scales/scale_manual.py +8 -2
  87. plotnine/scales/scale_xy.py +2 -2
  88. plotnine/stats/binning.py +4 -1
  89. plotnine/stats/smoothers.py +23 -36
  90. plotnine/stats/stat.py +20 -32
  91. plotnine/stats/stat_bin.py +6 -5
  92. plotnine/stats/stat_bin_2d.py +11 -9
  93. plotnine/stats/stat_bindot.py +13 -16
  94. plotnine/stats/stat_boxplot.py +6 -6
  95. plotnine/stats/stat_count.py +6 -9
  96. plotnine/stats/stat_density.py +7 -10
  97. plotnine/stats/stat_density_2d.py +12 -8
  98. plotnine/stats/stat_ecdf.py +7 -6
  99. plotnine/stats/stat_ellipse.py +9 -6
  100. plotnine/stats/stat_function.py +10 -8
  101. plotnine/stats/stat_hull.py +6 -3
  102. plotnine/stats/stat_identity.py +5 -2
  103. plotnine/stats/stat_pointdensity.py +5 -7
  104. plotnine/stats/stat_qq.py +46 -20
  105. plotnine/stats/stat_qq_line.py +16 -11
  106. plotnine/stats/stat_quantile.py +15 -9
  107. plotnine/stats/stat_sina.py +13 -15
  108. plotnine/stats/stat_smooth.py +8 -10
  109. plotnine/stats/stat_sum.py +5 -2
  110. plotnine/stats/stat_summary.py +7 -10
  111. plotnine/stats/stat_summary_bin.py +11 -14
  112. plotnine/stats/stat_unique.py +5 -2
  113. plotnine/stats/stat_ydensity.py +8 -11
  114. plotnine/themes/elements/__init__.py +2 -1
  115. plotnine/themes/elements/element_line.py +17 -9
  116. plotnine/themes/elements/margin.py +64 -1
  117. plotnine/themes/theme.py +9 -1
  118. plotnine/themes/theme_538.py +0 -1
  119. plotnine/themes/theme_bw.py +0 -1
  120. plotnine/themes/theme_dark.py +0 -1
  121. plotnine/themes/theme_gray.py +6 -5
  122. plotnine/themes/theme_light.py +1 -1
  123. plotnine/themes/theme_matplotlib.py +5 -5
  124. plotnine/themes/theme_seaborn.py +7 -4
  125. plotnine/themes/theme_void.py +9 -8
  126. plotnine/themes/theme_xkcd.py +0 -1
  127. plotnine/themes/themeable.py +109 -31
  128. plotnine/typing.py +17 -6
  129. plotnine/watermark.py +3 -3
  130. {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/METADATA +13 -6
  131. plotnine-0.15.2.dist-info/RECORD +221 -0
  132. {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/WHEEL +1 -1
  133. plotnine/plot_composition/__init__.py +0 -10
  134. plotnine/plot_composition/_compose.py +0 -436
  135. plotnine/plot_composition/_spacer.py +0 -32
  136. plotnine-0.15.0.dev3.dist-info/RECORD +0 -215
  137. /plotnine/{plot_composition → composition}/_plotspec.py +0 -0
  138. {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/licenses/LICENSE +0 -0
  139. {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.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,14 +291,63 @@ 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
+ """
225
351
  axis_text_y_margin_left: float = 0
226
352
  axis_text_y: float = 0
227
353
  axis_text_y_margin_right: float = 0
@@ -232,22 +358,16 @@ class left_spaces(_side_spaces):
232
358
  calc = self.items.calc
233
359
  items = self.items
234
360
 
235
- # If the plot_tag is in the margin, it is included in the layout.
236
- # So we make space for it, including any margins it may have.
237
- plot_tag_in_layout = theme.getp(
238
- "plot_tag_location"
239
- ) == "margin" and "left" in theme.getp("plot_tag_position")
240
-
241
361
  self.plot_margin = theme.getp("plot_margin_left")
242
362
 
243
- if items.plot_tag and plot_tag_in_layout:
363
+ if self.has_tag and items.plot_tag:
244
364
  m = theme.get_margin("plot_tag").fig
245
365
  self.plot_tag_margin_left = m.l
246
366
  self.plot_tag = calc.width(items.plot_tag)
247
367
  self.plot_tag_margin_right = m.r
248
368
 
249
369
  if items.legends and items.legends.left:
250
- self.legend = self._legend_width
370
+ self.legend = self.legend_width
251
371
  self.legend_box_spacing = theme.getp("legend_box_spacing")
252
372
 
253
373
  if items.axis_title_y:
@@ -273,13 +393,6 @@ class left_spaces(_side_spaces):
273
393
  if adjustment > 0:
274
394
  self.plot_margin += adjustment
275
395
 
276
- @cached_property
277
- def _legend_size(self) -> tuple[float, float]:
278
- if not (self.items.legends and self.items.legends.left):
279
- return (0, 0)
280
-
281
- return self.items.calc.size(self.items.legends.left.box)
282
-
283
396
  @property
284
397
  def offset(self) -> float:
285
398
  """
@@ -309,18 +422,18 @@ class left_spaces(_side_spaces):
309
422
  return self.to_figure_space(self.sum_incl(item))
310
423
 
311
424
  @property
312
- def left_relative(self):
425
+ def panel_left_relative(self):
313
426
  """
314
427
  Left (relative to the gridspec) of the panels in figure dimensions
315
428
  """
316
429
  return self.total
317
430
 
318
431
  @property
319
- def left(self):
432
+ def panel_left(self):
320
433
  """
321
434
  Left of the panels in figure space
322
435
  """
323
- return self.to_figure_space(self.left_relative)
436
+ return self.to_figure_space(self.panel_left_relative)
324
437
 
325
438
  @property
326
439
  def plot_left(self):
@@ -329,6 +442,17 @@ class left_spaces(_side_spaces):
329
442
  """
330
443
  return self.x1("legend")
331
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
+
332
456
 
333
457
  @dataclass
334
458
  class right_spaces(_side_spaces):
@@ -339,36 +463,33 @@ class right_spaces(_side_spaces):
339
463
  """
340
464
 
341
465
  plot_margin: float = 0
466
+ tag_alignment: float = 0
342
467
  plot_tag_margin_right: float = 0
343
468
  plot_tag: float = 0
344
469
  plot_tag_margin_left: float = 0
470
+ margin_alignment: float = 0
345
471
  legend: float = 0
346
472
  legend_box_spacing: float = 0
347
- strip_text_y_width_right: float = 0
473
+ strip_text_y_extra_width: float = 0
348
474
 
349
475
  def _calculate(self):
350
476
  items = self.items
351
477
  theme = self.items.plot.theme
352
478
  calc = self.items.calc
353
- # If the plot_tag is in the margin, it is included in the layout.
354
- # So we make space for it, including any margins it may have.
355
- plot_tag_in_layout = theme.getp(
356
- "plot_tag_location"
357
- ) == "margin" and "right" in theme.getp("plot_tag_position")
358
479
 
359
480
  self.plot_margin = theme.getp("plot_margin_right")
360
481
 
361
- if items.plot_tag and plot_tag_in_layout:
482
+ if self.has_tag and items.plot_tag:
362
483
  m = theme.get_margin("plot_tag").fig
363
484
  self.plot_tag_margin_right = m.r
364
485
  self.plot_tag = calc.width(items.plot_tag)
365
486
  self.plot_tag_margin_left = m.l
366
487
 
367
488
  if items.legends and items.legends.right:
368
- self.legend = self._legend_width
489
+ self.legend = self.legend_width
369
490
  self.legend_box_spacing = theme.getp("legend_box_spacing")
370
491
 
371
- 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")
372
493
 
373
494
  # Adjust plot_margin to make room for ylabels that protude well
374
495
  # beyond the axes
@@ -378,13 +499,6 @@ class right_spaces(_side_spaces):
378
499
  if adjustment > 0:
379
500
  self.plot_margin += adjustment
380
501
 
381
- @cached_property
382
- def _legend_size(self) -> tuple[float, float]:
383
- if not (self.items.legends and self.items.legends.right):
384
- return (0, 0)
385
-
386
- return self.items.calc.size(self.items.legends.right.box)
387
-
388
502
  @property
389
503
  def offset(self):
390
504
  """
@@ -414,18 +528,18 @@ class right_spaces(_side_spaces):
414
528
  return self.to_figure_space(1 - self.sum_upto(item))
415
529
 
416
530
  @property
417
- def right_relative(self):
531
+ def panel_right_relative(self):
418
532
  """
419
533
  Right (relative to the gridspec) of the panels in figure dimensions
420
534
  """
421
535
  return 1 - self.total
422
536
 
423
537
  @property
424
- def right(self):
538
+ def panel_right(self):
425
539
  """
426
540
  Right of the panels in figure space
427
541
  """
428
- return self.to_figure_space(self.right_relative)
542
+ return self.to_figure_space(self.panel_right_relative)
429
543
 
430
544
  @property
431
545
  def plot_right(self):
@@ -434,6 +548,17 @@ class right_spaces(_side_spaces):
434
548
  """
435
549
  return self.x2("legend")
436
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
+
437
562
 
438
563
  @dataclass
439
564
  class top_spaces(_side_spaces):
@@ -444,9 +569,11 @@ class top_spaces(_side_spaces):
444
569
  """
445
570
 
446
571
  plot_margin: float = 0
572
+ tag_alignment: float = 0
447
573
  plot_tag_margin_top: float = 0
448
574
  plot_tag: float = 0
449
575
  plot_tag_margin_bottom: float = 0
576
+ margin_alignment: float = 0
450
577
  plot_title_margin_top: float = 0
451
578
  plot_title: float = 0
452
579
  plot_title_margin_bottom: float = 0
@@ -455,7 +582,7 @@ class top_spaces(_side_spaces):
455
582
  plot_subtitle_margin_bottom: float = 0
456
583
  legend: float = 0
457
584
  legend_box_spacing: float = 0
458
- strip_text_x_height_top: float = 0
585
+ strip_text_x_extra_height: float = 0
459
586
 
460
587
  def _calculate(self):
461
588
  items = self.items
@@ -463,15 +590,10 @@ class top_spaces(_side_spaces):
463
590
  calc = self.items.calc
464
591
  W, H = theme.getp("figure_size")
465
592
  F = W / H
466
- # If the plot_tag is in the margin, it is included in the layout.
467
- # So we make space for it, including any margins it may have.
468
- plot_tag_in_layout = theme.getp(
469
- "plot_tag_location"
470
- ) == "margin" and "top" in theme.getp("plot_tag_position")
471
593
 
472
594
  self.plot_margin = theme.getp("plot_margin_top") * F
473
595
 
474
- if items.plot_tag and plot_tag_in_layout:
596
+ if self.has_tag and items.plot_tag:
475
597
  m = theme.get_margin("plot_tag").fig
476
598
  self.plot_tag_margin_top = m.t
477
599
  self.plot_tag = calc.height(items.plot_tag)
@@ -490,10 +612,10 @@ class top_spaces(_side_spaces):
490
612
  self.plot_subtitle_margin_bottom = m.b * F
491
613
 
492
614
  if items.legends and items.legends.top:
493
- self.legend = self._legend_height
615
+ self.legend = self.legend_height
494
616
  self.legend_box_spacing = theme.getp("legend_box_spacing") * F
495
617
 
496
- 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")
497
619
 
498
620
  # Adjust plot_margin to make room for ylabels that protude well
499
621
  # beyond the axes
@@ -503,13 +625,6 @@ class top_spaces(_side_spaces):
503
625
  if adjustment > 0:
504
626
  self.plot_margin += adjustment
505
627
 
506
- @cached_property
507
- def _legend_size(self) -> tuple[float, float]:
508
- if not (self.items.legends and self.items.legends.top):
509
- return (0, 0)
510
-
511
- return self.items.calc.size(self.items.legends.top.box)
512
-
513
628
  @property
514
629
  def offset(self) -> float:
515
630
  """
@@ -542,18 +657,18 @@ class top_spaces(_side_spaces):
542
657
  return self.to_figure_space(1 - self.sum_upto(item))
543
658
 
544
659
  @property
545
- def top_relative(self):
660
+ def panel_top_relative(self):
546
661
  """
547
662
  Top (relative to the gridspec) of the panels in figure dimensions
548
663
  """
549
664
  return 1 - self.total
550
665
 
551
666
  @property
552
- def top(self):
667
+ def panel_top(self):
553
668
  """
554
669
  Top of the panels in figure space
555
670
  """
556
- return self.to_figure_space(self.top_relative)
671
+ return self.to_figure_space(self.panel_top_relative)
557
672
 
558
673
  @property
559
674
  def plot_top(self):
@@ -562,6 +677,17 @@ class top_spaces(_side_spaces):
562
677
  """
563
678
  return self.y2("legend")
564
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
+
565
691
 
566
692
  @dataclass
567
693
  class bottom_spaces(_side_spaces):
@@ -572,9 +698,11 @@ class bottom_spaces(_side_spaces):
572
698
  """
573
699
 
574
700
  plot_margin: float = 0
701
+ tag_alignment: float = 0
575
702
  plot_tag_margin_bottom: float = 0
576
703
  plot_tag: float = 0
577
704
  plot_tag_margin_top: float = 0
705
+ margin_alignment: float = 0
578
706
  plot_caption_margin_bottom: float = 0
579
707
  plot_caption: float = 0
580
708
  plot_caption_margin_top: float = 0
@@ -583,6 +711,15 @@ class bottom_spaces(_side_spaces):
583
711
  axis_title_x_margin_bottom: float = 0
584
712
  axis_title_x: float = 0
585
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
+ """
586
723
  axis_text_x_margin_bottom: float = 0
587
724
  axis_text_x: float = 0
588
725
  axis_text_x_margin_top: float = 0
@@ -594,15 +731,10 @@ class bottom_spaces(_side_spaces):
594
731
  calc = self.items.calc
595
732
  W, H = theme.getp("figure_size")
596
733
  F = W / H
597
- # If the plot_tag is in the margin, it is included in the layout.
598
- # So we make space for it, including any margins it may have.
599
- plot_tag_in_layout = theme.getp(
600
- "plot_tag_location"
601
- ) == "margin" and "bottom" in theme.getp("plot_tag_position")
602
734
 
603
735
  self.plot_margin = theme.getp("plot_margin_bottom") * F
604
736
 
605
- if items.plot_tag and plot_tag_in_layout:
737
+ if self.has_tag and items.plot_tag:
606
738
  m = theme.get_margin("plot_tag").fig
607
739
  self.plot_tag_margin_bottom = m.b
608
740
  self.plot_tag = calc.height(items.plot_tag)
@@ -615,7 +747,7 @@ class bottom_spaces(_side_spaces):
615
747
  self.plot_caption_margin_top = m.t * F
616
748
 
617
749
  if items.legends and items.legends.bottom:
618
- self.legend = self._legend_height
750
+ self.legend = self.legend_height
619
751
  self.legend_box_spacing = theme.getp("legend_box_spacing") * F
620
752
 
621
753
  if items.axis_title_x:
@@ -640,13 +772,6 @@ class bottom_spaces(_side_spaces):
640
772
  if adjustment > 0:
641
773
  self.plot_margin += adjustment
642
774
 
643
- @cached_property
644
- def _legend_size(self) -> tuple[float, float]:
645
- if not (self.items.legends and self.items.legends.bottom):
646
- return (0, 0)
647
-
648
- return self.items.calc.size(self.items.legends.bottom.box)
649
-
650
775
  @property
651
776
  def offset(self) -> float:
652
777
  """
@@ -679,18 +804,18 @@ class bottom_spaces(_side_spaces):
679
804
  return self.to_figure_space(self.sum_incl(item))
680
805
 
681
806
  @property
682
- def bottom_relative(self):
807
+ def panel_bottom_relative(self):
683
808
  """
684
809
  Bottom (relative to the gridspec) of the panels in figure dimensions
685
810
  """
686
811
  return self.total
687
812
 
688
813
  @property
689
- def bottom(self):
814
+ def panel_bottom(self):
690
815
  """
691
816
  Bottom of the panels in figure space
692
817
  """
693
- return self.to_figure_space(self.bottom_relative)
818
+ return self.to_figure_space(self.panel_bottom_relative)
694
819
 
695
820
  @property
696
821
  def plot_bottom(self):
@@ -699,6 +824,17 @@ class bottom_spaces(_side_spaces):
699
824
  """
700
825
  return self.y1("legend")
701
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
+
702
838
 
703
839
  @dataclass
704
840
  class LayoutSpaces:
@@ -802,14 +938,76 @@ class LayoutSpaces:
802
938
  """
803
939
  Width [figure dimensions] of panels
804
940
  """
805
- return self.r.right - self.l.left
941
+ return self.r.panel_right - self.l.panel_left
806
942
 
807
943
  @property
808
944
  def panel_height(self) -> float:
809
945
  """
810
946
  Height [figure dimensions] of panels
811
947
  """
812
- 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
813
1011
 
814
1012
  def increase_horizontal_plot_margin(self, dw: float):
815
1013
  """
@@ -847,8 +1045,8 @@ class LayoutSpaces:
847
1045
 
848
1046
  This is the area in which the panels are drawn.
849
1047
  """
850
- x1, x2 = self.l.left, self.r.right
851
- 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
852
1050
  return ((x1, y1), (x2, y2))
853
1051
 
854
1052
  def _calculate_panel_spacing(self) -> GridSpecParams:
@@ -869,10 +1067,10 @@ class LayoutSpaces:
869
1067
  raise TypeError(f"Unknown type of facet: {type(self.plot.facet)}")
870
1068
 
871
1069
  return GridSpecParams(
872
- self.l.left_relative,
873
- self.r.right_relative,
874
- self.t.top_relative,
875
- 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,
876
1074
  wspace,
877
1075
  hspace,
878
1076
  )
@@ -886,6 +1084,9 @@ class LayoutSpaces:
886
1084
  ncol = self.plot.facet.ncol
887
1085
  nrow = self.plot.facet.nrow
888
1086
 
1087
+ left, right = self.l.panel_left, self.r.panel_right
1088
+ top, bottom = self.t.panel_top, self.b.panel_bottom
1089
+
889
1090
  # Both spacings are specified as fractions of the figure width
890
1091
  # Multiply the vertical by (W/H) so that the gullies along both
891
1092
  # directions are equally spaced.
@@ -893,8 +1094,8 @@ class LayoutSpaces:
893
1094
  self.sh = theme.getp("panel_spacing_y") * self.W / self.H
894
1095
 
895
1096
  # width and height of axes as fraction of figure width & height
896
- self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
897
- 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
898
1099
 
899
1100
  # Spacing as fraction of axes width & height
900
1101
  wspace = self.sw / self.w
@@ -911,6 +1112,9 @@ class LayoutSpaces:
911
1112
  ncol = facet.ncol
912
1113
  nrow = facet.nrow
913
1114
 
1115
+ left, right = self.l.panel_left, self.r.panel_right
1116
+ top, bottom = self.t.panel_top, self.b.panel_bottom
1117
+
914
1118
  # Both spacings are specified as fractions of the figure width
915
1119
  self.sw = theme.getp("panel_spacing_x")
916
1120
  self.sh = theme.getp("panel_spacing_y") * self.W / self.H
@@ -927,7 +1131,7 @@ class LayoutSpaces:
927
1131
  # Only interested in the proportion of the strip that
928
1132
  # does not overlap with the panel
929
1133
  if strip_align_x > -1:
930
- 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)
931
1135
 
932
1136
  if facet.free["x"]:
933
1137
  self.sh += self.items.axis_text_x_max_height_at(
@@ -939,8 +1143,8 @@ class LayoutSpaces:
939
1143
  ) + self.items.axis_ticks_y_max_width_at("all")
940
1144
 
941
1145
  # width and height of axes as fraction of figure width & height
942
- self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
943
- 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
944
1148
 
945
1149
  # Spacing as fraction of axes width & height
946
1150
  wspace = self.sw / self.w
@@ -951,8 +1155,8 @@ class LayoutSpaces:
951
1155
  """
952
1156
  Calculate spacing parts for facet_null
953
1157
  """
954
- self.w = self.r.right - self.l.left
955
- 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
956
1160
  self.sw = 0
957
1161
  self.sh = 0
958
1162
  return 0, 0