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
@@ -0,0 +1,1154 @@
1
+ """
2
+ Routines to adjust subplot params so that subplots are
3
+ nicely fit in the figure. In doing so, only axis labels, tick labels, axes
4
+ titles and offsetboxes that are anchored to axes are currently considered.
5
+
6
+ Internally, this module assumes that the margins (left margin, etc.) which are
7
+ differences between `Axes.get_tightbbox` and `Axes.bbox` are independent of
8
+ Axes position. This may fail if `Axes.adjustable` is `datalim` as well as
9
+ such cases as when left or right margin are affected by xlabel.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC
15
+ from dataclasses import dataclass, field, fields
16
+ from functools import cached_property
17
+ from typing import TYPE_CHECKING, cast
18
+
19
+ from plotnine.facets import facet_grid, facet_null, facet_wrap
20
+
21
+ from ._layout_items import LayoutItems
22
+
23
+ if TYPE_CHECKING:
24
+ from dataclasses import Field
25
+ from typing import Generator
26
+
27
+ from plotnine import ggplot
28
+ from plotnine._mpl.gridspec import p9GridSpec
29
+ from plotnine.iapi import outside_legend
30
+ from plotnine.typing import Side
31
+
32
+ # Note
33
+ # Margins around the plot are specified in figure coordinates
34
+ # We interpret that value to be a fraction of the width. So along
35
+ # the vertical direction we multiply by W/H to get equal space
36
+ # in both directions
37
+
38
+
39
+ @dataclass
40
+ class GridSpecParams:
41
+ """
42
+ Gridspec Parameters
43
+ """
44
+
45
+ left: float
46
+ right: float
47
+ top: float
48
+ bottom: float
49
+ wspace: float
50
+ hspace: float
51
+
52
+ @property
53
+ def valid(self) -> bool:
54
+ """
55
+ Return True if the params will create a non-empty area
56
+ """
57
+ return self.top - self.bottom > 0 and self.right - self.left > 0
58
+
59
+
60
+ @dataclass
61
+ class _side_spaces(ABC):
62
+ """
63
+ Base class to for spaces
64
+
65
+ A *_space class does the book keeping for all the artists that may
66
+ fall on that side of the panels. The same name may appear in multiple
67
+ side classes (e.g. legend).
68
+
69
+ The amount of space for each artist is computed in figure coordinates.
70
+ """
71
+
72
+ items: LayoutItems
73
+
74
+ def __post_init__(self):
75
+ self.side: Side = cast("Side", self.__class__.__name__[:-7])
76
+ """
77
+ Side of the panel(s) that this class applies to
78
+ """
79
+ self._calculate()
80
+
81
+ def _calculate(self):
82
+ """
83
+ Calculate the space taken up by each artist
84
+ """
85
+
86
+ @property
87
+ def total(self) -> float:
88
+ """
89
+ Total space
90
+ """
91
+ return sum(getattr(self, f.name) for f in fields(self)[1:])
92
+
93
+ def sum_upto(self, item: str) -> float:
94
+ """
95
+ Sum of space upto but not including item
96
+
97
+ Sums from the edge of the figure i.e. the "plot_margin".
98
+ """
99
+
100
+ def _fields_upto(item: str) -> Generator[Field, None, None]:
101
+ for f in fields(self)[1:]:
102
+ if f.name == item:
103
+ break
104
+ yield f
105
+
106
+ return sum(getattr(self, f.name) for f in _fields_upto(item))
107
+
108
+ def sum_incl(self, item: str) -> float:
109
+ """
110
+ Sum of space upto and including the item
111
+
112
+ Sums from the edge of the figure i.e. the "plot_margin".
113
+ """
114
+
115
+ def _fields_upto(item: str) -> Generator[Field, None, None]:
116
+ for f in fields(self)[1:]:
117
+ yield f
118
+ if f.name == item:
119
+ break
120
+
121
+ return sum(getattr(self, f.name) for f in _fields_upto(item))
122
+
123
+ @cached_property
124
+ def _legend_size(self) -> tuple[float, float]:
125
+ """
126
+ Return size of legend in figure coordinates
127
+
128
+ We need this to accurately justify the legend by proportional
129
+ values e.g. 0.2, instead of just left, right, top, bottom &
130
+ center.
131
+ """
132
+ if not self.has_legend:
133
+ return (0, 0)
134
+
135
+ ol: outside_legend = getattr(self.items.legends, self.side)
136
+ return self.items.calc.size(ol.box)
137
+
138
+ @cached_property
139
+ def legend_width(self) -> float:
140
+ """
141
+ Return width of legend in figure coordinates
142
+ """
143
+ return self._legend_size[0]
144
+
145
+ @cached_property
146
+ def legend_height(self) -> float:
147
+ """
148
+ Return height of legend in figure coordinates
149
+ """
150
+ return self._legend_size[1]
151
+
152
+ @cached_property
153
+ def gs(self) -> p9GridSpec:
154
+ """
155
+ The gridspec of the plot
156
+ """
157
+ return self.items.plot._gridspec
158
+
159
+ @property
160
+ def offset(self) -> float:
161
+ """
162
+ Distance in figure dimensions from the edge of the figure
163
+
164
+ Derived classes should override this method
165
+
166
+ The space/margin and size consumed by artists is in figure dimensions
167
+ but the exact position is relative to the position of the GridSpec
168
+ within the figure. The offset accounts for the position of the
169
+ GridSpec and allows us to accurately place artists using figure
170
+ coordinates.
171
+
172
+ Example of an offset
173
+
174
+ Figure
175
+ ----------------------------------------
176
+ | |
177
+ | Plot GridSpec |
178
+ | -------------------------- |
179
+ | offset | | |
180
+ |<------->| X | |
181
+ | | Panels GridSpec | |
182
+ | | -------------------- | |
183
+ | | | | | |
184
+ | | | | | |
185
+ | | | | | |
186
+ | | | | | |
187
+ | | -------------------- | |
188
+ | | | |
189
+ | -------------------------- |
190
+ | |
191
+ ----------------------------------------
192
+ """
193
+ return 0
194
+
195
+ def to_figure_space(self, rel_value: float) -> float:
196
+ """
197
+ Convert value relative to the gridspec to one in figure space
198
+
199
+ The result is meant to be used with transFigure transforms.
200
+
201
+ Parameters
202
+ ----------
203
+ rel_value :
204
+ Position relative to the position of the gridspec
205
+ """
206
+ return self.offset + rel_value
207
+
208
+ @property
209
+ def has_tag(self) -> bool:
210
+ """
211
+ Return True if the space/margin to this side of the panel has a tag
212
+
213
+ If it does, then it will be included in the layout
214
+ """
215
+ getp = self.items.plot.theme.getp
216
+ return getp("plot_tag_location") == "margin" and self.side in getp(
217
+ "plot_tag_position"
218
+ )
219
+
220
+ @property
221
+ def has_legend(self) -> bool:
222
+ """
223
+ Return True if the space/margin to this side of the panel has a legend
224
+
225
+ If it does, then it will be included in the layout
226
+ """
227
+ if not self.items.legends:
228
+ return False
229
+ return hasattr(self.items.legends, self.side)
230
+
231
+ @property
232
+ def tag_width(self) -> float:
233
+ """
234
+ The width of the tag including the margins
235
+
236
+ The value is zero expect if all these are true:
237
+ - The tag is in the margin `theme(plot_tag_position = "margin")`
238
+ - The tag at one one of the the following locations;
239
+ left, right, topleft, topright, bottomleft or bottomright
240
+ """
241
+ return 0
242
+
243
+ @property
244
+ def tag_height(self) -> float:
245
+ """
246
+ The height of the tag including the margins
247
+
248
+ The value is zero expect if all these are true:
249
+ - The tag is in the margin `theme(plot_tag_position = "margin")`
250
+ - The tag at one one of the the following locations;
251
+ top, bottom, topleft, topright, bottomleft or bottomright
252
+ """
253
+ return 0
254
+
255
+
256
+ @dataclass
257
+ class left_spaces(_side_spaces):
258
+ """
259
+ Space in the figure for artists on the left of the panel area
260
+
261
+ Ordered from the edge of the figure and going inwards
262
+ """
263
+
264
+ plot_margin: float = 0
265
+ tag_alignment: float = 0
266
+ """
267
+ Space added to align the tag in this plot with others in a composition
268
+
269
+ This value is calculated during the layout process, and it ensures that
270
+ all tags on this side of the plot take up the same amount of space in
271
+ the margin. e.g. from
272
+
273
+ ------------------------------------
274
+ | plot_margin | tag | artists |
275
+ |------------------------------------|
276
+ | plot_margin | A long tag | artists |
277
+ ------------------------------------
278
+
279
+ to
280
+
281
+ ------------------------------------
282
+ | plot_margin | tag | artists |
283
+ |------------------------------------|
284
+ | plot_margin | A long tag | artists |
285
+ ------------------------------------
286
+
287
+ And the tag is justified within that space e.g if ha="left" we get
288
+
289
+ ------------------------------------
290
+ | plot_margin | tag | artists |
291
+ |------------------------------------|
292
+ | plot_margin | A long tag | artists |
293
+ ------------------------------------
294
+
295
+ So, contrary to the order in which the space items are laid out, the
296
+ tag_alignment does not necessarily come before the plot_tag.
297
+ """
298
+ plot_tag_margin_left: float = 0
299
+ plot_tag: float = 0
300
+ plot_tag_margin_right: float = 0
301
+ margin_alignment: float = 0
302
+ """
303
+ Space added to align this plot with others in a composition
304
+
305
+ This value is calculated during the layout process in a tree structure
306
+ that has convenient access to the sides/edges of the panels in the
307
+ composition.
308
+ """
309
+ legend: float = 0
310
+ legend_box_spacing: float = 0
311
+ axis_title_y_margin_left: float = 0
312
+ axis_title_y: float = 0
313
+ axis_title_y_margin_right: float = 0
314
+ axis_text_y_margin_left: float = 0
315
+ axis_text_y: float = 0
316
+ axis_text_y_margin_right: float = 0
317
+ axis_ticks_y: float = 0
318
+
319
+ def _calculate(self):
320
+ theme = self.items.plot.theme
321
+ calc = self.items.calc
322
+ items = self.items
323
+
324
+ self.plot_margin = theme.getp("plot_margin_left")
325
+
326
+ if self.has_tag and items.plot_tag:
327
+ m = theme.get_margin("plot_tag").fig
328
+ self.plot_tag_margin_left = m.l
329
+ self.plot_tag = calc.width(items.plot_tag)
330
+ self.plot_tag_margin_right = m.r
331
+
332
+ if items.legends and items.legends.left:
333
+ self.legend = self.legend_width
334
+ self.legend_box_spacing = theme.getp("legend_box_spacing")
335
+
336
+ if items.axis_title_y:
337
+ m = theme.get_margin("axis_title_y").fig
338
+ self.axis_title_y_margin_left = m.l
339
+ self.axis_title_y = calc.width(items.axis_title_y)
340
+ self.axis_title_y_margin_right = m.r
341
+
342
+ # Account for the space consumed by the axis
343
+ self.axis_text_y = items.axis_text_y_max_width_at("first_col")
344
+ if self.axis_text_y:
345
+ m = theme.get_margin("axis_text_y").fig
346
+ self.axis_text_y_margin_left = m.l
347
+ self.axis_text_y_margin_right = m.r
348
+
349
+ self.axis_ticks_y = items.axis_ticks_y_max_width_at("first_col")
350
+
351
+ # Adjust plot_margin to make room for ylabels that protude well
352
+ # beyond the axes
353
+ # NOTE: This adjustment breaks down when the protrusion is large
354
+ protrusion = items.axis_text_x_left_protrusion("all")
355
+ adjustment = protrusion - (self.total - self.plot_margin)
356
+ if adjustment > 0:
357
+ self.plot_margin += adjustment
358
+
359
+ @property
360
+ def offset(self) -> float:
361
+ """
362
+ Distance from left of the figure to the left of the plot gridspec
363
+
364
+ ----------------(1, 1)
365
+ | ---- |
366
+ | dx | | |
367
+ |<--->| | |
368
+ | | | |
369
+ | ---- |
370
+ (0, 0)----------------
371
+
372
+ """
373
+ return self.gs.bbox_relative.x0
374
+
375
+ def x1(self, item: str) -> float:
376
+ """
377
+ Lower x-coordinate in figure space of the item
378
+ """
379
+ return self.to_figure_space(self.sum_upto(item))
380
+
381
+ def x2(self, item: str) -> float:
382
+ """
383
+ Higher x-coordinate in figure space of the item
384
+ """
385
+ return self.to_figure_space(self.sum_incl(item))
386
+
387
+ @property
388
+ def left_relative(self):
389
+ """
390
+ Left (relative to the gridspec) of the panels in figure dimensions
391
+ """
392
+ return self.total
393
+
394
+ @property
395
+ def left(self):
396
+ """
397
+ Left of the panels in figure space
398
+ """
399
+ return self.to_figure_space(self.left_relative)
400
+
401
+ @property
402
+ def plot_left(self):
403
+ """
404
+ Distance up to the left-most artist in figure space
405
+ """
406
+ return self.x1("legend")
407
+
408
+ @property
409
+ def tag_width(self):
410
+ """
411
+ The width of the tag including the margins
412
+ """
413
+ return (
414
+ self.plot_tag_margin_left
415
+ + self.plot_tag
416
+ + self.plot_tag_margin_right
417
+ )
418
+
419
+
420
+ @dataclass
421
+ class right_spaces(_side_spaces):
422
+ """
423
+ Space in the figure for artists on the right of the panel area
424
+
425
+ Ordered from the edge of the figure and going inwards
426
+ """
427
+
428
+ plot_margin: float = 0
429
+ tag_alignment: float = 0
430
+ plot_tag_margin_right: float = 0
431
+ plot_tag: float = 0
432
+ plot_tag_margin_left: float = 0
433
+ margin_alignment: float = 0
434
+ legend: float = 0
435
+ legend_box_spacing: float = 0
436
+ strip_text_y_extra_width: float = 0
437
+
438
+ def _calculate(self):
439
+ items = self.items
440
+ theme = self.items.plot.theme
441
+ calc = self.items.calc
442
+
443
+ self.plot_margin = theme.getp("plot_margin_right")
444
+
445
+ if self.has_tag and items.plot_tag:
446
+ m = theme.get_margin("plot_tag").fig
447
+ self.plot_tag_margin_right = m.r
448
+ self.plot_tag = calc.width(items.plot_tag)
449
+ self.plot_tag_margin_left = m.l
450
+
451
+ if items.legends and items.legends.right:
452
+ self.legend = self.legend_width
453
+ self.legend_box_spacing = theme.getp("legend_box_spacing")
454
+
455
+ self.strip_text_y_extra_width = items.strip_text_y_extra_width("right")
456
+
457
+ # Adjust plot_margin to make room for ylabels that protude well
458
+ # beyond the axes
459
+ # NOTE: This adjustment breaks down when the protrusion is large
460
+ protrusion = items.axis_text_x_right_protrusion("all")
461
+ adjustment = protrusion - (self.total - self.plot_margin)
462
+ if adjustment > 0:
463
+ self.plot_margin += adjustment
464
+
465
+ @property
466
+ def offset(self):
467
+ """
468
+ Distance from right of the figure to the right of the plot gridspec
469
+
470
+ ---------------(1, 1)
471
+ | ---- |
472
+ | | | -dx |
473
+ | | |<--->|
474
+ | | | |
475
+ | ---- |
476
+ (0, 0)---------------
477
+
478
+ """
479
+ return self.gs.bbox_relative.x1 - 1
480
+
481
+ def x1(self, item: str) -> float:
482
+ """
483
+ Lower x-coordinate in figure space of the item
484
+ """
485
+ return self.to_figure_space(1 - self.sum_incl(item))
486
+
487
+ def x2(self, item: str) -> float:
488
+ """
489
+ Higher x-coordinate in figure space of the item
490
+ """
491
+ return self.to_figure_space(1 - self.sum_upto(item))
492
+
493
+ @property
494
+ def right_relative(self):
495
+ """
496
+ Right (relative to the gridspec) of the panels in figure dimensions
497
+ """
498
+ return 1 - self.total
499
+
500
+ @property
501
+ def right(self):
502
+ """
503
+ Right of the panels in figure space
504
+ """
505
+ return self.to_figure_space(self.right_relative)
506
+
507
+ @property
508
+ def plot_right(self):
509
+ """
510
+ Distance up to the right-most artist in figure space
511
+ """
512
+ return self.x2("legend")
513
+
514
+ @property
515
+ def tag_width(self):
516
+ """
517
+ The width of the tag including the margins
518
+ """
519
+ return (
520
+ self.plot_tag_margin_right
521
+ + self.plot_tag
522
+ + self.plot_tag_margin_left
523
+ )
524
+
525
+
526
+ @dataclass
527
+ class top_spaces(_side_spaces):
528
+ """
529
+ Space in the figure for artists above the panel area
530
+
531
+ Ordered from the edge of the figure and going inwards
532
+ """
533
+
534
+ plot_margin: float = 0
535
+ tag_alignment: float = 0
536
+ plot_tag_margin_top: float = 0
537
+ plot_tag: float = 0
538
+ plot_tag_margin_bottom: float = 0
539
+ margin_alignment: float = 0
540
+ plot_title_margin_top: float = 0
541
+ plot_title: float = 0
542
+ plot_title_margin_bottom: float = 0
543
+ plot_subtitle_margin_top: float = 0
544
+ plot_subtitle: float = 0
545
+ plot_subtitle_margin_bottom: float = 0
546
+ legend: float = 0
547
+ legend_box_spacing: float = 0
548
+ strip_text_x_extra_height: float = 0
549
+
550
+ def _calculate(self):
551
+ items = self.items
552
+ theme = self.items.plot.theme
553
+ calc = self.items.calc
554
+ W, H = theme.getp("figure_size")
555
+ F = W / H
556
+
557
+ self.plot_margin = theme.getp("plot_margin_top") * F
558
+
559
+ if self.has_tag and items.plot_tag:
560
+ m = theme.get_margin("plot_tag").fig
561
+ self.plot_tag_margin_top = m.t
562
+ self.plot_tag = calc.height(items.plot_tag)
563
+ self.plot_tag_margin_bottom = m.b
564
+
565
+ if items.plot_title:
566
+ m = theme.get_margin("plot_title").fig
567
+ self.plot_title_margin_top = m.t * F
568
+ self.plot_title = calc.height(items.plot_title)
569
+ self.plot_title_margin_bottom = m.b * F
570
+
571
+ if items.plot_subtitle:
572
+ m = theme.get_margin("plot_subtitle").fig
573
+ self.plot_subtitle_margin_top = m.t * F
574
+ self.plot_subtitle = calc.height(items.plot_subtitle)
575
+ self.plot_subtitle_margin_bottom = m.b * F
576
+
577
+ if items.legends and items.legends.top:
578
+ self.legend = self.legend_height
579
+ self.legend_box_spacing = theme.getp("legend_box_spacing") * F
580
+
581
+ self.strip_text_x_extra_height = items.strip_text_x_extra_height("top")
582
+
583
+ # Adjust plot_margin to make room for ylabels that protude well
584
+ # beyond the axes
585
+ # NOTE: This adjustment breaks down when the protrusion is large
586
+ protrusion = items.axis_text_y_top_protrusion("all")
587
+ adjustment = protrusion - (self.total - self.plot_margin)
588
+ if adjustment > 0:
589
+ self.plot_margin += adjustment
590
+
591
+ @property
592
+ def offset(self) -> float:
593
+ """
594
+ Distance from top of the figure to the top of the plot gridspec
595
+
596
+ ----------------(1, 1)
597
+ | ^ |
598
+ | |-dy |
599
+ | v |
600
+ | ---- |
601
+ | | | |
602
+ | | | |
603
+ | | | |
604
+ | ---- |
605
+ | |
606
+ (0, 0)----------------
607
+ """
608
+ return self.gs.bbox_relative.y1 - 1
609
+
610
+ def y1(self, item: str) -> float:
611
+ """
612
+ Lower y-coordinate in figure space of the item
613
+ """
614
+ return self.to_figure_space(1 - self.sum_incl(item))
615
+
616
+ def y2(self, item: str) -> float:
617
+ """
618
+ Higher y-coordinate in figure space of the item
619
+ """
620
+ return self.to_figure_space(1 - self.sum_upto(item))
621
+
622
+ @property
623
+ def top_relative(self):
624
+ """
625
+ Top (relative to the gridspec) of the panels in figure dimensions
626
+ """
627
+ return 1 - self.total
628
+
629
+ @property
630
+ def top(self):
631
+ """
632
+ Top of the panels in figure space
633
+ """
634
+ return self.to_figure_space(self.top_relative)
635
+
636
+ @property
637
+ def plot_top(self):
638
+ """
639
+ Distance up to the top-most artist in figure space
640
+ """
641
+ return self.y2("legend")
642
+
643
+ @property
644
+ def tag_height(self):
645
+ """
646
+ The height of the tag including the margins
647
+ """
648
+ return (
649
+ self.plot_tag_margin_top
650
+ + self.plot_tag
651
+ + self.plot_tag_margin_bottom
652
+ )
653
+
654
+
655
+ @dataclass
656
+ class bottom_spaces(_side_spaces):
657
+ """
658
+ Space in the figure for artists below the panel area
659
+
660
+ Ordered from the edge of the figure and going inwards
661
+ """
662
+
663
+ plot_margin: float = 0
664
+ tag_alignment: float = 0
665
+ plot_tag_margin_bottom: float = 0
666
+ plot_tag: float = 0
667
+ plot_tag_margin_top: float = 0
668
+ margin_alignment: float = 0
669
+ plot_caption_margin_bottom: float = 0
670
+ plot_caption: float = 0
671
+ plot_caption_margin_top: float = 0
672
+ legend: float = 0
673
+ legend_box_spacing: float = 0
674
+ axis_title_x_margin_bottom: float = 0
675
+ axis_title_x: float = 0
676
+ axis_title_x_margin_top: float = 0
677
+ axis_text_x_margin_bottom: float = 0
678
+ axis_text_x: float = 0
679
+ axis_text_x_margin_top: float = 0
680
+ axis_ticks_x: float = 0
681
+
682
+ def _calculate(self):
683
+ items = self.items
684
+ theme = self.items.plot.theme
685
+ calc = self.items.calc
686
+ W, H = theme.getp("figure_size")
687
+ F = W / H
688
+
689
+ self.plot_margin = theme.getp("plot_margin_bottom") * F
690
+
691
+ if self.has_tag and items.plot_tag:
692
+ m = theme.get_margin("plot_tag").fig
693
+ self.plot_tag_margin_bottom = m.b
694
+ self.plot_tag = calc.height(items.plot_tag)
695
+ self.plot_tag_margin_top = m.t
696
+
697
+ if items.plot_caption:
698
+ m = theme.get_margin("plot_caption").fig
699
+ self.plot_caption_margin_bottom = m.b * F
700
+ self.plot_caption = calc.height(items.plot_caption)
701
+ self.plot_caption_margin_top = m.t * F
702
+
703
+ if items.legends and items.legends.bottom:
704
+ self.legend = self.legend_height
705
+ self.legend_box_spacing = theme.getp("legend_box_spacing") * F
706
+
707
+ if items.axis_title_x:
708
+ m = theme.get_margin("axis_title_x").fig
709
+ self.axis_title_x_margin_bottom = m.b * F
710
+ self.axis_title_x = calc.height(items.axis_title_x)
711
+ self.axis_title_x_margin_top = m.t * F
712
+
713
+ # Account for the space consumed by the axis
714
+ self.axis_text_x = items.axis_text_x_max_height_at("last_row")
715
+ if self.axis_text_x:
716
+ m = theme.get_margin("axis_text_x").fig
717
+ self.axis_text_x_margin_bottom = m.b
718
+ self.axis_text_x_margin_top = m.t
719
+ self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row")
720
+
721
+ # Adjust plot_margin to make room for ylabels that protude well
722
+ # beyond the axes
723
+ # NOTE: This adjustment breaks down when the protrusion is large
724
+ protrusion = items.axis_text_y_bottom_protrusion("all")
725
+ adjustment = protrusion - (self.total - self.plot_margin)
726
+ if adjustment > 0:
727
+ self.plot_margin += adjustment
728
+
729
+ @property
730
+ def offset(self) -> float:
731
+ """
732
+ Distance from bottom of the figure to the bottom of the plot gridspec
733
+
734
+ ----------------(1, 1)
735
+ | |
736
+ | ---- |
737
+ | | | |
738
+ | | | |
739
+ | | | |
740
+ | ---- |
741
+ | ^ |
742
+ | |dy |
743
+ | v |
744
+ (0, 0)----------------
745
+ """
746
+ return self.gs.bbox_relative.y0
747
+
748
+ def y1(self, item: str) -> float:
749
+ """
750
+ Lower y-coordinate in figure space of the item
751
+ """
752
+ return self.to_figure_space(self.sum_upto(item))
753
+
754
+ def y2(self, item: str) -> float:
755
+ """
756
+ Higher y-coordinate in figure space of the item
757
+ """
758
+ return self.to_figure_space(self.sum_incl(item))
759
+
760
+ @property
761
+ def bottom_relative(self):
762
+ """
763
+ Bottom (relative to the gridspec) of the panels in figure dimensions
764
+ """
765
+ return self.total
766
+
767
+ @property
768
+ def bottom(self):
769
+ """
770
+ Bottom of the panels in figure space
771
+ """
772
+ return self.to_figure_space(self.bottom_relative)
773
+
774
+ @property
775
+ def plot_bottom(self):
776
+ """
777
+ Distance up to the bottom-most artist in figure space
778
+ """
779
+ return self.y1("legend")
780
+
781
+ @property
782
+ def tag_height(self):
783
+ """
784
+ The height of the tag including the margins
785
+ """
786
+ return (
787
+ self.plot_tag_margin_bottom
788
+ + self.plot_tag
789
+ + self.plot_tag_margin_top
790
+ )
791
+
792
+
793
+ @dataclass
794
+ class LayoutSpaces:
795
+ """
796
+ Compute the all the spaces required in the layout
797
+
798
+ These are:
799
+
800
+ 1. The space of each artist between the panel and the edge of the
801
+ figure.
802
+ 2. The space in-between the panels
803
+
804
+ From these values, we put together the grid-spec parameters required
805
+ by matplotblib to position the axes. We also use the values to adjust
806
+ the coordinates of all the artists that occupy these spaces, placing
807
+ them in their final positions.
808
+ """
809
+
810
+ plot: ggplot
811
+
812
+ l: left_spaces = field(init=False)
813
+ """All subspaces to the left of the panels"""
814
+
815
+ r: right_spaces = field(init=False)
816
+ """All subspaces to the right of the panels"""
817
+
818
+ t: top_spaces = field(init=False)
819
+ """All subspaces above the top of the panels"""
820
+
821
+ b: bottom_spaces = field(init=False)
822
+ """All subspaces below the bottom of the panels"""
823
+
824
+ W: float = field(init=False, default=0)
825
+ """Figure Width [inches]"""
826
+
827
+ H: float = field(init=False, default=0)
828
+ """Figure Height [inches]"""
829
+
830
+ w: float = field(init=False, default=0)
831
+ """Axes width w.r.t figure in [0, 1]"""
832
+
833
+ h: float = field(init=False, default=0)
834
+ """Axes height w.r.t figure in [0, 1]"""
835
+
836
+ sh: float = field(init=False, default=0)
837
+ """horizontal spacing btn panels w.r.t figure"""
838
+
839
+ sw: float = field(init=False, default=0)
840
+ """vertical spacing btn panels w.r.t figure"""
841
+
842
+ gsparams: GridSpecParams = field(init=False, repr=False)
843
+ """Grid spacing btn panels w.r.t figure"""
844
+
845
+ def __post_init__(self):
846
+ self.items = LayoutItems(self.plot)
847
+ self.W, self.H = self.plot.theme.getp("figure_size")
848
+
849
+ # Calculate the spacing along the edges of the panel area
850
+ # (spacing required by plotnine)
851
+ self.l = left_spaces(self.items)
852
+ self.r = right_spaces(self.items)
853
+ self.t = top_spaces(self.items)
854
+ self.b = bottom_spaces(self.items)
855
+
856
+ def get_gridspec_params(self) -> GridSpecParams:
857
+ # Calculate the gridspec params
858
+ # (spacing required by mpl)
859
+ self.gsparams = self._calculate_panel_spacing()
860
+
861
+ # Adjust the spacing parameters for the desired aspect ratio
862
+ # It is simpler to adjust for the aspect ratio than to calculate
863
+ # the final parameters that are true to the aspect ratio in
864
+ # one-short
865
+ if (ratio := self.plot.facet._aspect_ratio()) is not None:
866
+ current_ratio = self.aspect_ratio
867
+ if ratio > current_ratio:
868
+ # Increase aspect ratio, taller panels
869
+ self._reduce_width(ratio)
870
+ elif ratio < current_ratio:
871
+ # Increase aspect ratio, wider panels
872
+ self._reduce_height(ratio)
873
+
874
+ return self.gsparams
875
+
876
+ @property
877
+ def plot_width(self) -> float:
878
+ """
879
+ Width [figure dimensions] of the whole plot
880
+ """
881
+ return self.plot._gridspec.bbox_relative.width
882
+
883
+ @property
884
+ def plot_height(self) -> float:
885
+ """
886
+ Height [figure dimensions] of the whole plot
887
+ """
888
+ return self.plot._gridspec.bbox_relative.height
889
+
890
+ @property
891
+ def panel_width(self) -> float:
892
+ """
893
+ Width [figure dimensions] of panels
894
+ """
895
+ return self.r.right - self.l.left
896
+
897
+ @property
898
+ def panel_height(self) -> float:
899
+ """
900
+ Height [figure dimensions] of panels
901
+ """
902
+ return self.t.top - self.b.bottom
903
+
904
+ @property
905
+ def tag_width(self) -> float:
906
+ """
907
+ Width [figure dimensions] of space taken up by the tag
908
+ """
909
+ # Atleast one of these is zero
910
+ return max(self.l.tag_width, self.r.tag_width)
911
+
912
+ @property
913
+ def tag_height(self) -> float:
914
+ """
915
+ Height [figure dimensions] of space taken up by the tag
916
+ """
917
+ # Atleast one of these is zero
918
+ return max(self.t.tag_height, self.b.tag_height)
919
+
920
+ @property
921
+ def left_tag_width(self) -> float:
922
+ """
923
+ Width [figure dimensions] of space taken up by a left tag
924
+ """
925
+ return self.l.tag_width
926
+
927
+ @property
928
+ def right_tag_width(self) -> float:
929
+ """
930
+ Width [figure dimensions] of space taken up by a right tag
931
+ """
932
+ return self.r.tag_width
933
+
934
+ @property
935
+ def top_tag_height(self) -> float:
936
+ """
937
+ Width [figure dimensions] of space taken up by a top tag
938
+ """
939
+ return self.t.tag_height
940
+
941
+ @property
942
+ def bottom_tag_height(self) -> float:
943
+ """
944
+ Height [figure dimensions] of space taken up by a bottom tag
945
+ """
946
+ return self.b.tag_height
947
+
948
+ def increase_horizontal_plot_margin(self, dw: float):
949
+ """
950
+ Increase the plot_margin to the right & left of the panels
951
+ """
952
+ self.l.plot_margin += dw
953
+ self.r.plot_margin += dw
954
+
955
+ def increase_vertical_plot_margin(self, dh: float):
956
+ """
957
+ Increase the plot_margin to the above & below of the panels
958
+ """
959
+ self.t.plot_margin += dh
960
+ self.b.plot_margin += dh
961
+
962
+ @property
963
+ def plot_area_coordinates(
964
+ self,
965
+ ) -> tuple[tuple[float, float], tuple[float, float]]:
966
+ """
967
+ Lower-left and upper-right coordinates of the plot area
968
+
969
+ This is the area surrounded by the plot_margin.
970
+ """
971
+ x1, x2 = self.l.x2("plot_margin"), self.r.x1("plot_margin")
972
+ y1, y2 = self.b.y2("plot_margin"), self.t.y1("plot_margin")
973
+ return ((x1, y1), (x2, y2))
974
+
975
+ @property
976
+ def panel_area_coordinates(
977
+ self,
978
+ ) -> tuple[tuple[float, float], tuple[float, float]]:
979
+ """
980
+ Lower-left and upper-right coordinates of the panel area
981
+
982
+ This is the area in which the panels are drawn.
983
+ """
984
+ x1, x2 = self.l.left, self.r.right
985
+ y1, y2 = self.b.bottom, self.t.top
986
+ return ((x1, y1), (x2, y2))
987
+
988
+ def _calculate_panel_spacing(self) -> GridSpecParams:
989
+ """
990
+ Spacing between the panels (wspace & hspace)
991
+
992
+ Both spaces are calculated from a fraction of the width.
993
+ This ensures that the same fraction gives equals space
994
+ in both directions.
995
+ """
996
+ if isinstance(self.plot.facet, facet_wrap):
997
+ wspace, hspace = self._calculate_panel_spacing_facet_wrap()
998
+ elif isinstance(self.plot.facet, facet_grid):
999
+ wspace, hspace = self._calculate_panel_spacing_facet_grid()
1000
+ elif isinstance(self.plot.facet, facet_null):
1001
+ wspace, hspace = self._calculate_panel_spacing_facet_null()
1002
+ else:
1003
+ raise TypeError(f"Unknown type of facet: {type(self.plot.facet)}")
1004
+
1005
+ return GridSpecParams(
1006
+ self.l.left_relative,
1007
+ self.r.right_relative,
1008
+ self.t.top_relative,
1009
+ self.b.bottom_relative,
1010
+ wspace,
1011
+ hspace,
1012
+ )
1013
+
1014
+ def _calculate_panel_spacing_facet_grid(self) -> tuple[float, float]:
1015
+ """
1016
+ Calculate spacing parts for facet_grid
1017
+ """
1018
+ theme = self.plot.theme
1019
+
1020
+ ncol = self.plot.facet.ncol
1021
+ nrow = self.plot.facet.nrow
1022
+
1023
+ # Both spacings are specified as fractions of the figure width
1024
+ # Multiply the vertical by (W/H) so that the gullies along both
1025
+ # directions are equally spaced.
1026
+ self.sw = theme.getp("panel_spacing_x")
1027
+ self.sh = theme.getp("panel_spacing_y") * self.W / self.H
1028
+
1029
+ # width and height of axes as fraction of figure width & height
1030
+ self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
1031
+ self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
1032
+
1033
+ # Spacing as fraction of axes width & height
1034
+ wspace = self.sw / self.w
1035
+ hspace = self.sh / self.h
1036
+ return (wspace, hspace)
1037
+
1038
+ def _calculate_panel_spacing_facet_wrap(self) -> tuple[float, float]:
1039
+ """
1040
+ Calculate spacing parts for facet_wrap
1041
+ """
1042
+ facet = self.plot.facet
1043
+ theme = self.plot.theme
1044
+
1045
+ ncol = facet.ncol
1046
+ nrow = facet.nrow
1047
+
1048
+ # Both spacings are specified as fractions of the figure width
1049
+ self.sw = theme.getp("panel_spacing_x")
1050
+ self.sh = theme.getp("panel_spacing_y") * self.W / self.H
1051
+
1052
+ # A fraction of the strip height
1053
+ # Effectively slides the strip
1054
+ # +ve: Away from the panel
1055
+ # 0: Top of the panel
1056
+ # -ve: Into the panel
1057
+ # Where values <= -1, put the strip completely into
1058
+ # the panel. We do not worry about larger -ves.
1059
+ strip_align_x = theme.getp("strip_align_x")
1060
+
1061
+ # Only interested in the proportion of the strip that
1062
+ # does not overlap with the panel
1063
+ if strip_align_x > -1:
1064
+ self.sh += self.t.strip_text_x_extra_height * (1 + strip_align_x)
1065
+
1066
+ if facet.free["x"]:
1067
+ self.sh += self.items.axis_text_x_max_height_at(
1068
+ "all"
1069
+ ) + self.items.axis_ticks_x_max_height_at("all")
1070
+ if facet.free["y"]:
1071
+ self.sw += self.items.axis_text_y_max_width_at(
1072
+ "all"
1073
+ ) + self.items.axis_ticks_y_max_width_at("all")
1074
+
1075
+ # width and height of axes as fraction of figure width & height
1076
+ self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
1077
+ self.h = ((self.t.top - self.b.bottom) - self.sh * (nrow - 1)) / nrow
1078
+
1079
+ # Spacing as fraction of axes width & height
1080
+ wspace = self.sw / self.w
1081
+ hspace = self.sh / self.h
1082
+ return (wspace, hspace)
1083
+
1084
+ def _calculate_panel_spacing_facet_null(self) -> tuple[float, float]:
1085
+ """
1086
+ Calculate spacing parts for facet_null
1087
+ """
1088
+ self.w = self.r.right - self.l.left
1089
+ self.h = self.t.top - self.b.bottom
1090
+ self.sw = 0
1091
+ self.sh = 0
1092
+ return 0, 0
1093
+
1094
+ def _reduce_height(self, ratio: float):
1095
+ """
1096
+ Reduce the height of axes to get the aspect ratio
1097
+ """
1098
+ # New height w.r.t figure height
1099
+ h1 = ratio * self.w * (self.W / self.H)
1100
+
1101
+ # Half of the total vertical reduction w.r.t figure height
1102
+ dh = (self.h - h1) * self.plot.facet.nrow / 2
1103
+
1104
+ # Reduce plot area height
1105
+ self.gsparams.top -= dh
1106
+ self.gsparams.bottom += dh
1107
+ self.gsparams.hspace = self.sh / h1
1108
+
1109
+ # Add more vertical plot margin
1110
+ self.increase_vertical_plot_margin(dh)
1111
+
1112
+ def _reduce_width(self, ratio: float):
1113
+ """
1114
+ Reduce the width of axes to get the aspect ratio
1115
+ """
1116
+ # New width w.r.t figure width
1117
+ w1 = (self.h * self.H) / (ratio * self.W)
1118
+
1119
+ # Half of the total horizontal reduction w.r.t figure width
1120
+ dw = (self.w - w1) * self.plot.facet.ncol / 2
1121
+
1122
+ # Reduce width
1123
+ self.gsparams.left += dw
1124
+ self.gsparams.right -= dw
1125
+ self.gsparams.wspace = self.sw / w1
1126
+
1127
+ # Add more horizontal margin
1128
+ self.increase_horizontal_plot_margin(dw)
1129
+
1130
+ @property
1131
+ def aspect_ratio(self) -> float:
1132
+ """
1133
+ Default aspect ratio of the panels
1134
+ """
1135
+ return (self.h * self.H) / (self.w * self.W)
1136
+
1137
+ @cached_property
1138
+ def gs(self) -> p9GridSpec:
1139
+ """
1140
+ The gridspec
1141
+ """
1142
+ return self.plot._gridspec
1143
+
1144
+ def to_figure_space(
1145
+ self,
1146
+ position: tuple[float, float],
1147
+ ) -> tuple[float, float]:
1148
+ """
1149
+ Convert position from gridspec space to figure space
1150
+ """
1151
+ _x, _y = position
1152
+ x = self.l.plot_left + (self.r.plot_right - self.l.plot_left) * _x
1153
+ y = self.b.plot_bottom + (self.t.plot_top - self.b.plot_bottom) * _y
1154
+ return (x, y)