plotnine 0.14.5__py3-none-any.whl → 0.15.0.dev2__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 (64) 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 +916 -0
  6. plotnine/_mpl/layout_manager/_layout_tree.py +625 -0
  7. plotnine/_mpl/layout_manager/_spaces.py +1007 -0
  8. plotnine/_mpl/patches.py +1 -1
  9. plotnine/_mpl/text.py +59 -24
  10. plotnine/_mpl/utils.py +78 -10
  11. plotnine/_utils/__init__.py +5 -5
  12. plotnine/_utils/dev.py +45 -27
  13. plotnine/animation.py +1 -1
  14. plotnine/coords/coord_trans.py +1 -1
  15. plotnine/data/__init__.py +12 -8
  16. plotnine/doctools.py +1 -1
  17. plotnine/facets/facet.py +30 -39
  18. plotnine/facets/facet_grid.py +14 -6
  19. plotnine/facets/facet_wrap.py +3 -5
  20. plotnine/facets/strips.py +7 -9
  21. plotnine/geoms/geom_crossbar.py +2 -3
  22. plotnine/geoms/geom_path.py +1 -1
  23. plotnine/ggplot.py +94 -65
  24. plotnine/guides/guide.py +12 -10
  25. plotnine/guides/guide_colorbar.py +3 -3
  26. plotnine/guides/guide_legend.py +12 -13
  27. plotnine/guides/guides.py +3 -3
  28. plotnine/iapi.py +5 -2
  29. plotnine/labels.py +5 -0
  30. plotnine/mapping/aes.py +4 -3
  31. plotnine/options.py +14 -7
  32. plotnine/plot_composition/__init__.py +10 -0
  33. plotnine/plot_composition/_compose.py +436 -0
  34. plotnine/plot_composition/_plotspec.py +50 -0
  35. plotnine/plot_composition/_spacer.py +32 -0
  36. plotnine/positions/position_dodge.py +1 -1
  37. plotnine/positions/position_dodge2.py +1 -1
  38. plotnine/positions/position_stack.py +1 -2
  39. plotnine/qplot.py +1 -2
  40. plotnine/scales/__init__.py +0 -6
  41. plotnine/scales/scale.py +1 -1
  42. plotnine/stats/binning.py +1 -1
  43. plotnine/stats/smoothers.py +3 -5
  44. plotnine/stats/stat_density.py +1 -1
  45. plotnine/stats/stat_qq_line.py +1 -1
  46. plotnine/stats/stat_sina.py +1 -1
  47. plotnine/themes/elements/__init__.py +2 -0
  48. plotnine/themes/elements/element_text.py +35 -24
  49. plotnine/themes/elements/margin.py +73 -60
  50. plotnine/themes/targets.py +3 -1
  51. plotnine/themes/theme.py +13 -7
  52. plotnine/themes/theme_gray.py +28 -31
  53. plotnine/themes/theme_matplotlib.py +25 -28
  54. plotnine/themes/theme_seaborn.py +31 -34
  55. plotnine/themes/theme_void.py +17 -26
  56. plotnine/themes/themeable.py +290 -157
  57. {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info}/METADATA +4 -3
  58. {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info}/RECORD +61 -54
  59. {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info}/WHEEL +1 -1
  60. plotnine/_mpl/_plot_side_space.py +0 -888
  61. plotnine/_mpl/_plotnine_tight_layout.py +0 -293
  62. plotnine/_mpl/layout_engine.py +0 -110
  63. {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info/licenses}/LICENSE +0 -0
  64. {plotnine-0.14.5.dist-info → plotnine-0.15.0.dev2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,625 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ from contextlib import suppress
5
+ from dataclasses import dataclass
6
+ from functools import cached_property
7
+ from typing import TYPE_CHECKING
8
+
9
+ import numpy as np
10
+
11
+ from plotnine.plot_composition import OR
12
+
13
+ from ._spaces import LayoutSpaces
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Sequence
17
+
18
+ from plotnine import ggplot
19
+ from plotnine._mpl.gridspec import p9GridSpec
20
+ from plotnine.plot_composition import Compose
21
+
22
+
23
+ @dataclass
24
+ class LayoutTree:
25
+ """
26
+ A Tree representation of the composition
27
+
28
+ The purpose of this class (and its subclasses) is to align and
29
+ and resize plots in a composition.
30
+
31
+ For example, this composition;
32
+
33
+ (p1 | p2) | (p3 / p4)
34
+
35
+ where p1, p2, p3 & p4 are ggplot objects would look like this;
36
+
37
+ -----------------------------
38
+ | | | |
39
+ | | | |
40
+ | | | |
41
+ | | | |
42
+ | | |---------|
43
+ | | | |
44
+ | | | |
45
+ | | | |
46
+ | | | |
47
+ -----------------------------
48
+
49
+ and the tree would have this structure;
50
+
51
+ ColumnsTree
52
+ |
53
+ ----------------------------
54
+ | | |
55
+ LayoutSpaces LayoutSpaces RowsTree
56
+ |
57
+ -------------
58
+ | |
59
+ LayoutSpaces LayoutSpaces
60
+
61
+ Each composition is a tree or subtree
62
+ """
63
+
64
+ gridspec: p9GridSpec
65
+ """
66
+ Gridspec of the composition
67
+
68
+ Originally this gridspec occupies all the space available to it so the
69
+ subplots are of equal sizes. As each subplot contains full ggplot,
70
+ differences in texts and legend sizes may make the panels (panel area)
71
+ have unequal sizes. We can resize the panels, by changing the height
72
+ and width ratios of this (composition) gridspec.
73
+
74
+ The information about the size (width & height) of the panels is in the
75
+ LayoutSpaces.
76
+ """
77
+
78
+ nodes: list[LayoutSpaces | LayoutTree]
79
+ """
80
+ The spaces or tree of spaces in the composition that the tree
81
+ represents.
82
+ """
83
+
84
+ @cached_property
85
+ @abc.abstractmethod
86
+ def lefts(self) -> Sequence[float]:
87
+ """
88
+ Left values [figure space] of nodes in this tree
89
+ """
90
+
91
+ @abc.abstractmethod
92
+ def set_left_alignment_margin(self, value: float):
93
+ """
94
+ Set a margin to align the left of the panels in this composition
95
+
96
+ In figure dimenstions
97
+ """
98
+
99
+ @cached_property
100
+ @abc.abstractmethod
101
+ def bottoms(self) -> Sequence[float]:
102
+ """
103
+ Bottom values [figure space] of nodes in this tree
104
+ """
105
+
106
+ @abc.abstractmethod
107
+ def set_bottom_alignment_margin(self, value: float):
108
+ """
109
+ Set a margin to align the bottom of the panels in this composition
110
+
111
+ In figure dimenstions
112
+ """
113
+
114
+ @cached_property
115
+ @abc.abstractmethod
116
+ def tops(self) -> Sequence[float]:
117
+ """
118
+ Top values [figure space] of nodes in this tree
119
+ """
120
+
121
+ @abc.abstractmethod
122
+ def set_top_alignment_margin(self, value: float):
123
+ """
124
+ Set a margin to align the top of the panels in this composition
125
+
126
+ In figure dimenstions
127
+ """
128
+
129
+ @cached_property
130
+ @abc.abstractmethod
131
+ def rights(self) -> Sequence[float]:
132
+ """
133
+ Right values [figure space] of nodes in this tree
134
+ """
135
+
136
+ @abc.abstractmethod
137
+ def set_right_alignment_margin(self, value: float):
138
+ """
139
+ Set a margin to align the right of the panels in this composition
140
+
141
+ In figure dimenstions
142
+ """
143
+
144
+ def align_lefts(self):
145
+ """
146
+ Align the immediate left edges in this composition
147
+
148
+ ----------- -----------
149
+ |# | | # |
150
+ |# | | # |
151
+ |# | | # |
152
+ |-----------| -> |-----------|
153
+ | # | | # |
154
+ | # | | # |
155
+ | # | | # |
156
+ ----------- -----------
157
+ """
158
+
159
+ def align_bottoms(self):
160
+ """
161
+ Align the immediate bottom edges this composition
162
+
163
+ ----------- -----------
164
+ | | | | | |
165
+ | | | | | |
166
+ | | | -> | | |
167
+ | |#####| |#####|#####|
168
+ |#####| | | | |
169
+ ----------- -----------
170
+ """
171
+
172
+ def align_rights(self):
173
+ """
174
+ Align the immediate right edges in this composition
175
+
176
+ ----------- -----------
177
+ | # | | # |
178
+ | # | | # |
179
+ | # | | # |
180
+ |-----------| -> |-----------|
181
+ | #| | # |
182
+ | #| | # |
183
+ | #| | # |
184
+ ----------- -----------
185
+ """
186
+
187
+ def align_tops(self):
188
+ """
189
+ Align the immediate top edges in this composition
190
+
191
+ ----------- -----------
192
+ |#####| | | | |
193
+ | |#####| |#####|#####|
194
+ | | | -> | | |
195
+ | | | | | |
196
+ | | | | | |
197
+ ----------- -----------
198
+ """
199
+
200
+ def align(self):
201
+ """
202
+ Align all the edges in this composition & contained compositions
203
+
204
+ This function mutates the layout spaces, specifically the
205
+ alignment_margins along the sides of the plot.
206
+ """
207
+ self.align_lefts()
208
+ self.align_bottoms()
209
+ self.align_rights()
210
+ self.align_tops()
211
+
212
+ for item in self.nodes:
213
+ if isinstance(item, LayoutTree):
214
+ item.align()
215
+
216
+ with suppress(AttributeError):
217
+ del self.lefts
218
+
219
+ with suppress(AttributeError):
220
+ del self.bottoms
221
+
222
+ with suppress(AttributeError):
223
+ del self.rights
224
+
225
+ with suppress(AttributeError):
226
+ del self.tops
227
+
228
+ @property
229
+ @abc.abstractmethod
230
+ def panel_width(self) -> float:
231
+ """
232
+ A representative for width for panels of the nodes
233
+ """
234
+
235
+ @property
236
+ @abc.abstractmethod
237
+ def panel_height(self) -> float:
238
+ """
239
+ A representative for height for panels of the nodes
240
+ """
241
+
242
+ @property
243
+ @abc.abstractmethod
244
+ def plot_width(self) -> float:
245
+ """
246
+ A representative for width for plots of the nodes
247
+ """
248
+
249
+ @property
250
+ @abc.abstractmethod
251
+ def plot_height(self) -> float:
252
+ """
253
+ A representative for height for plots of the nodes
254
+ """
255
+
256
+ @property
257
+ def plot_widths(self) -> Sequence[float]:
258
+ """
259
+ Widths [figure space] of nodes in this tree
260
+ """
261
+ return [node.plot_width for node in self.nodes]
262
+
263
+ @property
264
+ def plot_heights(self) -> Sequence[float]:
265
+ """
266
+ Heights [figure space] of nodes in this tree
267
+ """
268
+ return [node.plot_height for node in self.nodes]
269
+
270
+ @property
271
+ def panel_widths(self) -> Sequence[float]:
272
+ """
273
+ Widths [figure space] of the panels in this tree
274
+ """
275
+ return [node.panel_width for node in self.nodes]
276
+
277
+ @property
278
+ def panel_heights(self) -> Sequence[float]:
279
+ """
280
+ Heights [figure space] of the panels in this tree
281
+ """
282
+ return [node.panel_height for node in self.nodes]
283
+
284
+ def resize(self):
285
+ """
286
+ Resize panels and the entire plots
287
+
288
+ This function mutates the composition gridspecs; specifically the
289
+ width_ratios and height_ratios.
290
+ """
291
+ self.resize_widths()
292
+ self.resize_heights()
293
+
294
+ for item in self.nodes:
295
+ if isinstance(item, LayoutTree):
296
+ item.resize()
297
+
298
+ def resize_widths(self):
299
+ """
300
+ Resize the widths of gridspec so that panels have equal widths
301
+ """
302
+
303
+ def resize_heights(self):
304
+ """
305
+ Resize the heights of gridspec so that panels have equal heights
306
+ """
307
+
308
+ @staticmethod
309
+ def create(
310
+ cmp: Compose,
311
+ lookup_spaces: dict[ggplot, LayoutSpaces],
312
+ ) -> LayoutTree:
313
+ """
314
+ Create a LayoutTree for this composition
315
+
316
+ Parameters
317
+ ----------
318
+ cmp :
319
+ Composition
320
+ lookup_spaces :
321
+ A table to lookup the LayoutSpaces for each plot.
322
+
323
+ Notes
324
+ -----
325
+ LayoutTree works by modifying the `.gridspec` of the compositions,
326
+ and the `LayoutSpaces` of the plots.
327
+ """
328
+ from plotnine import ggplot
329
+
330
+ nodes: list[LayoutSpaces | LayoutTree] = []
331
+ for item in cmp:
332
+ if isinstance(item, ggplot):
333
+ nodes.append(lookup_spaces[item])
334
+ else:
335
+ nodes.append(LayoutTree.create(item, lookup_spaces))
336
+
337
+ if isinstance(cmp, OR):
338
+ return ColumnsTree(cmp.gridspec, nodes)
339
+ else:
340
+ return RowsTree(cmp.gridspec, nodes)
341
+
342
+ def harmonise(self):
343
+ """
344
+ Align and resize plots in composition to look good
345
+ """
346
+ self.align()
347
+ self.resize()
348
+
349
+
350
+ @dataclass
351
+ class ColumnsTree(LayoutTree):
352
+ """
353
+ Tree with columns at the outermost level
354
+
355
+ e.g. p1 | (p2 / p3)
356
+
357
+
358
+ -------------------
359
+ | | |
360
+ | | |
361
+ | | |
362
+ | | |
363
+ | |---------|
364
+ | | |
365
+ | | |
366
+ | | |
367
+ | | |
368
+ -------------------
369
+ """
370
+
371
+ @cached_property
372
+ def lefts(self):
373
+ left_item = self.nodes[0]
374
+ if isinstance(left_item, LayoutSpaces):
375
+ return [left_item.l.left]
376
+ else:
377
+ return left_item.lefts
378
+
379
+ def set_left_alignment_margin(self, value: float):
380
+ left_item = self.nodes[0]
381
+ if isinstance(left_item, LayoutSpaces):
382
+ left_item.l.alignment_margin = value
383
+ else:
384
+ left_item.set_left_alignment_margin(value)
385
+
386
+ def align_bottoms(self):
387
+ values = max(self.bottoms) - np.array(self.bottoms)
388
+ for item, value in zip(self.nodes, values):
389
+ if isinstance(item, LayoutSpaces):
390
+ item.b.alignment_margin = value
391
+ else:
392
+ item.set_bottom_alignment_margin(value)
393
+
394
+ @cached_property
395
+ def bottoms(self):
396
+ values = []
397
+ for item in self.nodes:
398
+ if isinstance(item, LayoutSpaces):
399
+ values.append(item.b.bottom)
400
+ else:
401
+ values.append(max(item.bottoms))
402
+ return values
403
+
404
+ def set_bottom_alignment_margin(self, value: float):
405
+ for item in self.nodes:
406
+ if isinstance(item, LayoutSpaces):
407
+ item.b.alignment_margin = value
408
+ else:
409
+ item.set_bottom_alignment_margin(value)
410
+
411
+ @cached_property
412
+ def rights(self):
413
+ right_item = self.nodes[-1]
414
+ if isinstance(right_item, LayoutSpaces):
415
+ return [right_item.r.right]
416
+ else:
417
+ return right_item.rights
418
+
419
+ def set_right_alignment_margin(self, value: float):
420
+ right_item = self.nodes[-1]
421
+ if isinstance(right_item, LayoutSpaces):
422
+ right_item.r.alignment_margin = value
423
+ else:
424
+ right_item.set_right_alignment_margin(value)
425
+
426
+ def align_tops(self):
427
+ values = np.array(self.tops) - min(self.tops)
428
+ for item, value in zip(self.nodes, values):
429
+ if isinstance(item, LayoutSpaces):
430
+ item.t.alignment_margin = value
431
+ else:
432
+ item.set_top_alignment_margin(value)
433
+
434
+ @cached_property
435
+ def tops(self):
436
+ values = []
437
+ for item in self.nodes:
438
+ if isinstance(item, LayoutSpaces):
439
+ values.append(item.t.top)
440
+ else:
441
+ values.append(min(item.tops))
442
+ return values
443
+
444
+ def set_top_alignment_margin(self, value: float):
445
+ for item in self.nodes:
446
+ if isinstance(item, LayoutSpaces):
447
+ item.t.alignment_margin = value
448
+ else:
449
+ item.set_top_alignment_margin(value)
450
+
451
+ @property
452
+ def panel_width(self) -> float:
453
+ """
454
+ A representative for width for panels of the nodes
455
+ """
456
+ return sum(self.panel_widths)
457
+
458
+ @property
459
+ def panel_height(self) -> float:
460
+ """
461
+ A representative for height for panels of the nodes
462
+ """
463
+ return float(np.mean(self.panel_heights))
464
+
465
+ @property
466
+ def plot_width(self) -> float:
467
+ """
468
+ A representative for width for plots of the nodes
469
+ """
470
+ return sum(self.plot_widths)
471
+
472
+ @property
473
+ def plot_height(self) -> float:
474
+ """
475
+ A representative for height for plots of the nodes
476
+ """
477
+ return max(self.plot_heights)
478
+
479
+ def resize_widths(self):
480
+ # The new width of each panel is the average width of all
481
+ # the panels plus all the space to the left and right
482
+ # of the panels.
483
+ plot_widths = np.array(self.plot_widths)
484
+ panel_widths = np.array(self.panel_widths)
485
+ non_panel_space = plot_widths - panel_widths
486
+ new_plot_widths = panel_widths.mean() + non_panel_space
487
+ width_ratios = new_plot_widths / new_plot_widths.min()
488
+ self.gridspec.set_width_ratios(width_ratios)
489
+
490
+
491
+ @dataclass
492
+ class RowsTree(LayoutTree):
493
+ """
494
+ Tree with rows at the outermost level
495
+
496
+ e.g. p1 / (p2 | p3)
497
+
498
+ -------------------
499
+ | |
500
+ | |
501
+ | |
502
+ |-------------------|
503
+ | | |
504
+ | | |
505
+ | | |
506
+ -------------------
507
+ """
508
+
509
+ def align_lefts(self):
510
+ values = max(self.lefts) - np.array(self.lefts)
511
+ for item, value in zip(self.nodes, values):
512
+ if isinstance(item, LayoutSpaces):
513
+ item.l.alignment_margin = value
514
+ else:
515
+ item.set_left_alignment_margin(value)
516
+
517
+ @cached_property
518
+ def lefts(self):
519
+ values = []
520
+ for item in self.nodes:
521
+ if isinstance(item, LayoutSpaces):
522
+ values.append(item.l.left)
523
+ else:
524
+ values.append(max(item.lefts))
525
+ return values
526
+
527
+ def set_left_alignment_margin(self, value: float):
528
+ for item in self.nodes:
529
+ if isinstance(item, LayoutSpaces):
530
+ item.l.alignment_margin = value
531
+ else:
532
+ item.set_left_alignment_margin(value)
533
+
534
+ @cached_property
535
+ def bottoms(self):
536
+ bottom_item = self.nodes[-1]
537
+ if isinstance(bottom_item, LayoutSpaces):
538
+ return [bottom_item.b.bottom]
539
+ else:
540
+ return bottom_item.bottoms
541
+
542
+ def set_bottom_alignment_margin(self, value: float):
543
+ bottom_item = self.nodes[-1]
544
+ if isinstance(bottom_item, LayoutSpaces):
545
+ bottom_item.b.alignment_margin = value
546
+ else:
547
+ bottom_item.set_bottom_alignment_margin(value)
548
+
549
+ def align_rights(self):
550
+ values = np.array(self.rights) - min(self.rights)
551
+ for item, value in zip(self.nodes, values):
552
+ if isinstance(item, LayoutSpaces):
553
+ item.r.alignment_margin = value
554
+ else:
555
+ item.set_right_alignment_margin(value)
556
+
557
+ @cached_property
558
+ def rights(self):
559
+ values = []
560
+ for item in self.nodes:
561
+ if isinstance(item, LayoutSpaces):
562
+ values.append(item.r.right)
563
+ else:
564
+ values.append(min(item.rights))
565
+ return values
566
+
567
+ def set_right_alignment_margin(self, value: float):
568
+ for item in self.nodes:
569
+ if isinstance(item, LayoutSpaces):
570
+ item.r.alignment_margin = value
571
+ else:
572
+ item.set_right_alignment_margin(value)
573
+
574
+ @cached_property
575
+ def tops(self):
576
+ top_item = self.nodes[0]
577
+ if isinstance(top_item, LayoutSpaces):
578
+ return [top_item.t.top]
579
+ else:
580
+ return top_item.tops
581
+
582
+ def set_top_alignment_margin(self, value: float):
583
+ top_item = self.nodes[0]
584
+ if isinstance(top_item, LayoutSpaces):
585
+ top_item.t.alignment_margin = value
586
+ else:
587
+ top_item.set_top_alignment_margin(value)
588
+
589
+ @property
590
+ def panel_width(self) -> float:
591
+ """
592
+ A representative for width for panels of the nodes
593
+ """
594
+ return float(np.mean(self.panel_widths))
595
+
596
+ @property
597
+ def panel_height(self) -> float:
598
+ """
599
+ A representative for height for panels of the nodes
600
+ """
601
+ return sum(self.panel_heights)
602
+
603
+ @property
604
+ def plot_width(self) -> float:
605
+ """
606
+ A representative for width for plots of the nodes
607
+ """
608
+ return max(self.plot_widths)
609
+
610
+ @property
611
+ def plot_height(self) -> float:
612
+ """
613
+ A representative for height for plots of the nodes
614
+ """
615
+ return sum(self.plot_heights)
616
+
617
+ def resize_heights(self):
618
+ # The new width of each panel is the average width of all
619
+ # the panels plus all the space above and below the panels.
620
+ plot_heights = np.array(self.plot_heights)
621
+ panel_heights = np.array(self.panel_heights)
622
+ non_panel_space = plot_heights - panel_heights
623
+ new_plot_heights = panel_heights.mean() + non_panel_space
624
+ height_ratios = new_plot_heights / new_plot_heights.max()
625
+ self.gridspec.set_height_ratios(height_ratios)