plotnine 0.15.0.dev1__py3-none-any.whl → 0.15.0.dev3__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.
- plotnine/_mpl/layout_manager/_layout_items.py +219 -118
- plotnine/_mpl/layout_manager/_layout_tree.py +168 -115
- plotnine/_mpl/layout_manager/_spaces.py +22 -9
- plotnine/_mpl/patches.py +1 -1
- plotnine/_mpl/text.py +59 -24
- plotnine/_utils/__init__.py +1 -1
- plotnine/facets/strips.py +5 -2
- plotnine/geoms/geom_bar.py +10 -2
- plotnine/geoms/geom_col.py +6 -0
- plotnine/geoms/geom_violin.py +24 -7
- plotnine/guides/guide.py +2 -2
- plotnine/guides/guide_legend.py +7 -8
- plotnine/iapi.py +4 -2
- plotnine/mapping/_eval_environment.py +85 -0
- plotnine/mapping/aes.py +10 -26
- plotnine/mapping/evaluation.py +7 -65
- plotnine/plot_composition/_compose.py +13 -4
- plotnine/stats/stat_sina.py +33 -0
- plotnine/themes/elements/element_text.py +1 -0
- plotnine/themes/targets.py +1 -1
- plotnine/themes/theme_gray.py +1 -0
- plotnine/themes/themeable.py +5 -5
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/METADATA +1 -1
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/RECORD +27 -26
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/WHEEL +1 -1
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from contextlib import suppress
|
|
5
4
|
from dataclasses import dataclass
|
|
6
5
|
from functools import cached_property
|
|
7
6
|
from typing import TYPE_CHECKING
|
|
@@ -81,6 +80,13 @@ class LayoutTree:
|
|
|
81
80
|
represents.
|
|
82
81
|
"""
|
|
83
82
|
|
|
83
|
+
@cached_property
|
|
84
|
+
def sub_compositions(self) -> list[LayoutTree]:
|
|
85
|
+
"""
|
|
86
|
+
LayoutTrees of the direct sub compositions of this one
|
|
87
|
+
"""
|
|
88
|
+
return [item for item in self.nodes if isinstance(item, LayoutTree)]
|
|
89
|
+
|
|
84
90
|
@cached_property
|
|
85
91
|
@abc.abstractmethod
|
|
86
92
|
def lefts(self) -> Sequence[float]:
|
|
@@ -141,89 +147,54 @@ class LayoutTree:
|
|
|
141
147
|
In figure dimenstions
|
|
142
148
|
"""
|
|
143
149
|
|
|
144
|
-
|
|
150
|
+
@abc.abstractmethod
|
|
151
|
+
def align(self):
|
|
145
152
|
"""
|
|
146
|
-
Align the
|
|
153
|
+
Align all the edges in this composition & contained compositions
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|# | | # |
|
|
151
|
-
|# | | # |
|
|
152
|
-
|-----------| -> |-----------|
|
|
153
|
-
| # | | # |
|
|
154
|
-
| # | | # |
|
|
155
|
-
| # | | # |
|
|
156
|
-
----------- -----------
|
|
155
|
+
This function mutates the layout spaces, specifically the
|
|
156
|
+
alignment_margins along the sides of the plot.
|
|
157
157
|
"""
|
|
158
158
|
|
|
159
|
-
def
|
|
159
|
+
def align_sub_compositions(self):
|
|
160
160
|
"""
|
|
161
|
-
Align the
|
|
162
|
-
|
|
163
|
-
----------- -----------
|
|
164
|
-
| | | | | |
|
|
165
|
-
| | | | | |
|
|
166
|
-
| | | -> | | |
|
|
167
|
-
| |#####| |#####|#####|
|
|
168
|
-
|#####| | | | |
|
|
169
|
-
----------- -----------
|
|
161
|
+
Align the compositions contained in this one
|
|
170
162
|
"""
|
|
163
|
+
# Recurse into the contained compositions
|
|
164
|
+
for tree in self.sub_compositions:
|
|
165
|
+
tree.align()
|
|
171
166
|
|
|
172
|
-
|
|
167
|
+
@property
|
|
168
|
+
def bottoms_align(self) -> bool:
|
|
173
169
|
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
----------- -----------
|
|
177
|
-
| # | | # |
|
|
178
|
-
| # | | # |
|
|
179
|
-
| # | | # |
|
|
180
|
-
|-----------| -> |-----------|
|
|
181
|
-
| #| | # |
|
|
182
|
-
| #| | # |
|
|
183
|
-
| #| | # |
|
|
184
|
-
----------- -----------
|
|
170
|
+
Return True if panel bottoms for the nodes are aligned
|
|
185
171
|
"""
|
|
172
|
+
arr = np.array(self.bottoms)
|
|
173
|
+
return all(arr == arr[0])
|
|
186
174
|
|
|
187
|
-
|
|
175
|
+
@property
|
|
176
|
+
def lefts_align(self) -> bool:
|
|
188
177
|
"""
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
----------- -----------
|
|
192
|
-
|#####| | | | |
|
|
193
|
-
| |#####| |#####|#####|
|
|
194
|
-
| | | -> | | |
|
|
195
|
-
| | | | | |
|
|
196
|
-
| | | | | |
|
|
197
|
-
----------- -----------
|
|
178
|
+
Return True if panel lefts for the nodes are aligned
|
|
198
179
|
"""
|
|
180
|
+
arr = np.array(self.lefts)
|
|
181
|
+
return all(arr == arr[0])
|
|
199
182
|
|
|
200
|
-
|
|
183
|
+
@property
|
|
184
|
+
def tops_align(self) -> bool:
|
|
201
185
|
"""
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
This function mutates the layout spaces, specifically the
|
|
205
|
-
alignment_margins along the sides of the plot.
|
|
186
|
+
Return True if panel tops for the nodes are aligned
|
|
206
187
|
"""
|
|
207
|
-
self.
|
|
208
|
-
|
|
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
|
|
188
|
+
arr = np.array(self.tops)
|
|
189
|
+
return all(arr == arr[0])
|
|
221
190
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
191
|
+
@property
|
|
192
|
+
def rights_align(self) -> bool:
|
|
193
|
+
"""
|
|
194
|
+
Return True if panel rights for the nodes are aligned
|
|
195
|
+
"""
|
|
196
|
+
arr = np.array(self.rights)
|
|
197
|
+
return all(arr == arr[0])
|
|
227
198
|
|
|
228
199
|
@property
|
|
229
200
|
@abc.abstractmethod
|
|
@@ -288,22 +259,13 @@ class LayoutTree:
|
|
|
288
259
|
This function mutates the composition gridspecs; specifically the
|
|
289
260
|
width_ratios and height_ratios.
|
|
290
261
|
"""
|
|
291
|
-
self.resize_widths()
|
|
292
|
-
self.resize_heights()
|
|
293
262
|
|
|
294
|
-
|
|
295
|
-
if isinstance(item, LayoutTree):
|
|
296
|
-
item.resize()
|
|
297
|
-
|
|
298
|
-
def resize_widths(self):
|
|
263
|
+
def resize_sub_compositions(self):
|
|
299
264
|
"""
|
|
300
|
-
Resize
|
|
301
|
-
"""
|
|
302
|
-
|
|
303
|
-
def resize_heights(self):
|
|
304
|
-
"""
|
|
305
|
-
Resize the heights of gridspec so that panels have equal heights
|
|
265
|
+
Resize panels in the compositions contained in this one
|
|
306
266
|
"""
|
|
267
|
+
for tree in self.sub_compositions:
|
|
268
|
+
tree.resize()
|
|
307
269
|
|
|
308
270
|
@staticmethod
|
|
309
271
|
def create(
|
|
@@ -368,6 +330,11 @@ class ColumnsTree(LayoutTree):
|
|
|
368
330
|
-------------------
|
|
369
331
|
"""
|
|
370
332
|
|
|
333
|
+
def align(self):
|
|
334
|
+
self.align_tops()
|
|
335
|
+
self.align_bottoms()
|
|
336
|
+
self.align_sub_compositions()
|
|
337
|
+
|
|
371
338
|
@cached_property
|
|
372
339
|
def lefts(self):
|
|
373
340
|
left_item = self.nodes[0]
|
|
@@ -383,14 +350,6 @@ class ColumnsTree(LayoutTree):
|
|
|
383
350
|
else:
|
|
384
351
|
left_item.set_left_alignment_margin(value)
|
|
385
352
|
|
|
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
353
|
@cached_property
|
|
395
354
|
def bottoms(self):
|
|
396
355
|
values = []
|
|
@@ -401,6 +360,33 @@ class ColumnsTree(LayoutTree):
|
|
|
401
360
|
values.append(max(item.bottoms))
|
|
402
361
|
return values
|
|
403
362
|
|
|
363
|
+
def align_bottoms(self):
|
|
364
|
+
"""
|
|
365
|
+
Align the immediate bottom edges this composition
|
|
366
|
+
|
|
367
|
+
----------- -----------
|
|
368
|
+
| | | | | |
|
|
369
|
+
| | | | | |
|
|
370
|
+
| | | -> | | |
|
|
371
|
+
| |#####| |#####|#####|
|
|
372
|
+
|#####| | | | |
|
|
373
|
+
----------- -----------
|
|
374
|
+
"""
|
|
375
|
+
# If panels are aligned and have a non-zero alignment_margin,
|
|
376
|
+
# aligning them again will set that value to zero and undoes
|
|
377
|
+
# the alignment.
|
|
378
|
+
if self.bottoms_align:
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
values = max(self.bottoms) - np.array(self.bottoms)
|
|
382
|
+
for item, value in zip(self.nodes, values):
|
|
383
|
+
if isinstance(item, LayoutSpaces):
|
|
384
|
+
item.b.alignment_margin = value
|
|
385
|
+
else:
|
|
386
|
+
item.set_bottom_alignment_margin(value)
|
|
387
|
+
|
|
388
|
+
del self.bottoms
|
|
389
|
+
|
|
404
390
|
def set_bottom_alignment_margin(self, value: float):
|
|
405
391
|
for item in self.nodes:
|
|
406
392
|
if isinstance(item, LayoutSpaces):
|
|
@@ -423,14 +409,6 @@ class ColumnsTree(LayoutTree):
|
|
|
423
409
|
else:
|
|
424
410
|
right_item.set_right_alignment_margin(value)
|
|
425
411
|
|
|
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
412
|
@cached_property
|
|
435
413
|
def tops(self):
|
|
436
414
|
values = []
|
|
@@ -441,6 +419,30 @@ class ColumnsTree(LayoutTree):
|
|
|
441
419
|
values.append(min(item.tops))
|
|
442
420
|
return values
|
|
443
421
|
|
|
422
|
+
def align_tops(self):
|
|
423
|
+
"""
|
|
424
|
+
Align the immediate top edges in this composition
|
|
425
|
+
|
|
426
|
+
----------- -----------
|
|
427
|
+
|#####| | | | |
|
|
428
|
+
| |#####| |#####|#####|
|
|
429
|
+
| | | -> | | |
|
|
430
|
+
| | | | | |
|
|
431
|
+
| | | | | |
|
|
432
|
+
----------- -----------
|
|
433
|
+
"""
|
|
434
|
+
if self.tops_align:
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
values = np.array(self.tops) - min(self.tops)
|
|
438
|
+
for item, value in zip(self.nodes, values):
|
|
439
|
+
if isinstance(item, LayoutSpaces):
|
|
440
|
+
item.t.alignment_margin = value
|
|
441
|
+
else:
|
|
442
|
+
item.set_top_alignment_margin(value)
|
|
443
|
+
|
|
444
|
+
del self.tops
|
|
445
|
+
|
|
444
446
|
def set_top_alignment_margin(self, value: float):
|
|
445
447
|
for item in self.nodes:
|
|
446
448
|
if isinstance(item, LayoutSpaces):
|
|
@@ -476,7 +478,10 @@ class ColumnsTree(LayoutTree):
|
|
|
476
478
|
"""
|
|
477
479
|
return max(self.plot_heights)
|
|
478
480
|
|
|
479
|
-
def
|
|
481
|
+
def resize(self):
|
|
482
|
+
"""
|
|
483
|
+
Resize the widths of gridspec so that panels have equal widths
|
|
484
|
+
"""
|
|
480
485
|
# The new width of each panel is the average width of all
|
|
481
486
|
# the panels plus all the space to the left and right
|
|
482
487
|
# of the panels.
|
|
@@ -486,6 +491,7 @@ class ColumnsTree(LayoutTree):
|
|
|
486
491
|
new_plot_widths = panel_widths.mean() + non_panel_space
|
|
487
492
|
width_ratios = new_plot_widths / new_plot_widths.min()
|
|
488
493
|
self.gridspec.set_width_ratios(width_ratios)
|
|
494
|
+
self.resize_sub_compositions()
|
|
489
495
|
|
|
490
496
|
|
|
491
497
|
@dataclass
|
|
@@ -506,13 +512,10 @@ class RowsTree(LayoutTree):
|
|
|
506
512
|
-------------------
|
|
507
513
|
"""
|
|
508
514
|
|
|
509
|
-
def
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
item.l.alignment_margin = value
|
|
514
|
-
else:
|
|
515
|
-
item.set_left_alignment_margin(value)
|
|
515
|
+
def align(self):
|
|
516
|
+
self.align_lefts()
|
|
517
|
+
self.align_rights()
|
|
518
|
+
self.align_sub_compositions()
|
|
516
519
|
|
|
517
520
|
@cached_property
|
|
518
521
|
def lefts(self):
|
|
@@ -524,6 +527,32 @@ class RowsTree(LayoutTree):
|
|
|
524
527
|
values.append(max(item.lefts))
|
|
525
528
|
return values
|
|
526
529
|
|
|
530
|
+
def align_lefts(self):
|
|
531
|
+
"""
|
|
532
|
+
Align the immediate left edges in this composition
|
|
533
|
+
|
|
534
|
+
----------- -----------
|
|
535
|
+
|# | | # |
|
|
536
|
+
|# | | # |
|
|
537
|
+
|# | | # |
|
|
538
|
+
|-----------| -> |-----------|
|
|
539
|
+
| # | | # |
|
|
540
|
+
| # | | # |
|
|
541
|
+
| # | | # |
|
|
542
|
+
----------- -----------
|
|
543
|
+
"""
|
|
544
|
+
if self.lefts_align:
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
values = max(self.lefts) - np.array(self.lefts)
|
|
548
|
+
for item, value in zip(self.nodes, values):
|
|
549
|
+
if isinstance(item, LayoutSpaces):
|
|
550
|
+
item.l.alignment_margin = value
|
|
551
|
+
else:
|
|
552
|
+
item.set_left_alignment_margin(value)
|
|
553
|
+
|
|
554
|
+
del self.lefts
|
|
555
|
+
|
|
527
556
|
def set_left_alignment_margin(self, value: float):
|
|
528
557
|
for item in self.nodes:
|
|
529
558
|
if isinstance(item, LayoutSpaces):
|
|
@@ -546,14 +575,6 @@ class RowsTree(LayoutTree):
|
|
|
546
575
|
else:
|
|
547
576
|
bottom_item.set_bottom_alignment_margin(value)
|
|
548
577
|
|
|
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
578
|
@cached_property
|
|
558
579
|
def rights(self):
|
|
559
580
|
values = []
|
|
@@ -564,6 +585,32 @@ class RowsTree(LayoutTree):
|
|
|
564
585
|
values.append(min(item.rights))
|
|
565
586
|
return values
|
|
566
587
|
|
|
588
|
+
def align_rights(self):
|
|
589
|
+
"""
|
|
590
|
+
Align the immediate right edges in this composition
|
|
591
|
+
|
|
592
|
+
----------- -----------
|
|
593
|
+
| # | | # |
|
|
594
|
+
| # | | # |
|
|
595
|
+
| # | | # |
|
|
596
|
+
|-----------| -> |-----------|
|
|
597
|
+
| #| | # |
|
|
598
|
+
| #| | # |
|
|
599
|
+
| #| | # |
|
|
600
|
+
----------- -----------
|
|
601
|
+
"""
|
|
602
|
+
if self.rights_align:
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
values = np.array(self.rights) - min(self.rights)
|
|
606
|
+
for item, value in zip(self.nodes, values):
|
|
607
|
+
if isinstance(item, LayoutSpaces):
|
|
608
|
+
item.r.alignment_margin = value
|
|
609
|
+
else:
|
|
610
|
+
item.set_right_alignment_margin(value)
|
|
611
|
+
|
|
612
|
+
del self.rights
|
|
613
|
+
|
|
567
614
|
def set_right_alignment_margin(self, value: float):
|
|
568
615
|
for item in self.nodes:
|
|
569
616
|
if isinstance(item, LayoutSpaces):
|
|
@@ -614,8 +661,13 @@ class RowsTree(LayoutTree):
|
|
|
614
661
|
"""
|
|
615
662
|
return sum(self.plot_heights)
|
|
616
663
|
|
|
617
|
-
def
|
|
618
|
-
|
|
664
|
+
def resize(self):
|
|
665
|
+
"""
|
|
666
|
+
Resize the heights of gridspec so that panels have equal heights
|
|
667
|
+
|
|
668
|
+
This method resizes (recursively) the contained compositions
|
|
669
|
+
"""
|
|
670
|
+
# The new height of each panel is the average width of all
|
|
619
671
|
# the panels plus all the space above and below the panels.
|
|
620
672
|
plot_heights = np.array(self.plot_heights)
|
|
621
673
|
panel_heights = np.array(self.panel_heights)
|
|
@@ -623,3 +675,4 @@ class RowsTree(LayoutTree):
|
|
|
623
675
|
new_plot_heights = panel_heights.mean() + non_panel_space
|
|
624
676
|
height_ratios = new_plot_heights / new_plot_heights.max()
|
|
625
677
|
self.gridspec.set_height_ratios(height_ratios)
|
|
678
|
+
self.resize_sub_compositions()
|
|
@@ -222,7 +222,9 @@ class left_spaces(_side_spaces):
|
|
|
222
222
|
axis_title_y_margin_left: float = 0
|
|
223
223
|
axis_title_y: float = 0
|
|
224
224
|
axis_title_y_margin_right: float = 0
|
|
225
|
+
axis_text_y_margin_left: float = 0
|
|
225
226
|
axis_text_y: float = 0
|
|
227
|
+
axis_text_y_margin_right: float = 0
|
|
226
228
|
axis_ticks_y: float = 0
|
|
227
229
|
|
|
228
230
|
def _calculate(self):
|
|
@@ -255,8 +257,13 @@ class left_spaces(_side_spaces):
|
|
|
255
257
|
self.axis_title_y_margin_right = m.r
|
|
256
258
|
|
|
257
259
|
# Account for the space consumed by the axis
|
|
258
|
-
self.axis_text_y = items.
|
|
259
|
-
self.
|
|
260
|
+
self.axis_text_y = items.axis_text_y_max_width_at("first_col")
|
|
261
|
+
if self.axis_text_y:
|
|
262
|
+
m = theme.get_margin("axis_text_y").fig
|
|
263
|
+
self.axis_text_y_margin_left = m.l
|
|
264
|
+
self.axis_text_y_margin_right = m.r
|
|
265
|
+
|
|
266
|
+
self.axis_ticks_y = items.axis_ticks_y_max_width_at("first_col")
|
|
260
267
|
|
|
261
268
|
# Adjust plot_margin to make room for ylabels that protude well
|
|
262
269
|
# beyond the axes
|
|
@@ -576,7 +583,9 @@ class bottom_spaces(_side_spaces):
|
|
|
576
583
|
axis_title_x_margin_bottom: float = 0
|
|
577
584
|
axis_title_x: float = 0
|
|
578
585
|
axis_title_x_margin_top: float = 0
|
|
586
|
+
axis_text_x_margin_bottom: float = 0
|
|
579
587
|
axis_text_x: float = 0
|
|
588
|
+
axis_text_x_margin_top: float = 0
|
|
580
589
|
axis_ticks_x: float = 0
|
|
581
590
|
|
|
582
591
|
def _calculate(self):
|
|
@@ -616,8 +625,12 @@ class bottom_spaces(_side_spaces):
|
|
|
616
625
|
self.axis_title_x_margin_top = m.t * F
|
|
617
626
|
|
|
618
627
|
# Account for the space consumed by the axis
|
|
619
|
-
self.
|
|
620
|
-
self.axis_text_x
|
|
628
|
+
self.axis_text_x = items.axis_text_x_max_height_at("last_row")
|
|
629
|
+
if self.axis_text_x:
|
|
630
|
+
m = theme.get_margin("axis_text_x").fig
|
|
631
|
+
self.axis_text_x_margin_bottom = m.b
|
|
632
|
+
self.axis_text_x_margin_top = m.t
|
|
633
|
+
self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row")
|
|
621
634
|
|
|
622
635
|
# Adjust plot_margin to make room for ylabels that protude well
|
|
623
636
|
# beyond the axes
|
|
@@ -736,7 +749,7 @@ class LayoutSpaces:
|
|
|
736
749
|
sw: float = field(init=False, default=0)
|
|
737
750
|
"""vertical spacing btn panels w.r.t figure"""
|
|
738
751
|
|
|
739
|
-
gsparams: GridSpecParams = field(init=False)
|
|
752
|
+
gsparams: GridSpecParams = field(init=False, repr=False)
|
|
740
753
|
"""Grid spacing btn panels w.r.t figure"""
|
|
741
754
|
|
|
742
755
|
def __post_init__(self):
|
|
@@ -917,13 +930,13 @@ class LayoutSpaces:
|
|
|
917
930
|
self.sh += self.t.strip_text_x_height_top * (1 + strip_align_x)
|
|
918
931
|
|
|
919
932
|
if facet.free["x"]:
|
|
920
|
-
self.sh += self.items.
|
|
933
|
+
self.sh += self.items.axis_text_x_max_height_at(
|
|
921
934
|
"all"
|
|
922
|
-
) + self.items.
|
|
935
|
+
) + self.items.axis_ticks_x_max_height_at("all")
|
|
923
936
|
if facet.free["y"]:
|
|
924
|
-
self.sw += self.items.
|
|
937
|
+
self.sw += self.items.axis_text_y_max_width_at(
|
|
925
938
|
"all"
|
|
926
|
-
) + self.items.
|
|
939
|
+
) + self.items.axis_ticks_y_max_width_at("all")
|
|
927
940
|
|
|
928
941
|
# width and height of axes as fraction of figure width & height
|
|
929
942
|
self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
|
plotnine/_mpl/patches.py
CHANGED
|
@@ -49,7 +49,7 @@ class StripTextPatch(FancyBboxPatch):
|
|
|
49
49
|
return
|
|
50
50
|
|
|
51
51
|
text = self.text
|
|
52
|
-
posx, posy = text.get_transform().transform(
|
|
52
|
+
posx, posy = text.get_transform().transform(text.get_position())
|
|
53
53
|
x, y, w, h = _get_textbox(text, renderer)
|
|
54
54
|
|
|
55
55
|
self.set_bounds(0.0, 0.0, w, h)
|
plotnine/_mpl/text.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from matplotlib.text import Text
|
|
6
6
|
|
|
7
7
|
from .patches import StripTextPatch
|
|
8
|
-
from .utils import bbox_in_axes_space
|
|
8
|
+
from .utils import bbox_in_axes_space, rel_position
|
|
9
9
|
|
|
10
|
-
if
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
11
|
from matplotlib.backend_bases import RendererBase
|
|
12
12
|
|
|
13
13
|
from plotnine.iapi import strip_draw_info
|
|
14
|
+
from plotnine.typing import HorizontalJustification, VerticalJustification
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class StripText(Text):
|
|
@@ -23,8 +24,6 @@ class StripText(Text):
|
|
|
23
24
|
|
|
24
25
|
def __init__(self, info: strip_draw_info):
|
|
25
26
|
kwargs = {
|
|
26
|
-
"ha": info.ha,
|
|
27
|
-
"va": info.va,
|
|
28
27
|
"rotation": info.rotation,
|
|
29
28
|
"transform": info.ax.transAxes,
|
|
30
29
|
"clip_on": False,
|
|
@@ -40,38 +39,74 @@ class StripText(Text):
|
|
|
40
39
|
self.draw_info = info
|
|
41
40
|
self.patch = StripTextPatch(self)
|
|
42
41
|
|
|
42
|
+
# TODO: Move these _justify methods to the layout manager
|
|
43
|
+
# We need to first make sure that the patch has the final size during
|
|
44
|
+
# layout computation. Right now, the final size is calculated during
|
|
45
|
+
# draw (in these justify methods)
|
|
46
|
+
def _justify_horizontally(self, renderer):
|
|
47
|
+
"""
|
|
48
|
+
Justify the text along the strip_background
|
|
49
|
+
"""
|
|
50
|
+
info = self.draw_info
|
|
51
|
+
lookup: dict[HorizontalJustification, float] = {
|
|
52
|
+
"left": 0.0,
|
|
53
|
+
"center": 0.5,
|
|
54
|
+
"right": 1.0,
|
|
55
|
+
}
|
|
56
|
+
rel = lookup.get(info.ha, 0.5) if isinstance(info.ha, str) else info.ha
|
|
57
|
+
patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
|
|
58
|
+
text_bbox = bbox_in_axes_space(self, info.ax, renderer)
|
|
59
|
+
l, b, w, h = info.x, info.y, info.box_width, patch_bbox.height
|
|
60
|
+
b = b + patch_bbox.height * info.strip_align
|
|
61
|
+
x = rel_position(rel, text_bbox.width, patch_bbox.x0, patch_bbox.x1)
|
|
62
|
+
y = b + h / 2
|
|
63
|
+
self.set_horizontalalignment("left")
|
|
64
|
+
self.patch.set_bounds(l, b, w, h)
|
|
65
|
+
self.set_position((x, y))
|
|
66
|
+
|
|
67
|
+
def _justify_vertically(self, renderer):
|
|
68
|
+
"""
|
|
69
|
+
Justify the text along the strip_background
|
|
70
|
+
"""
|
|
71
|
+
# Note that the strip text & background and horizontal but
|
|
72
|
+
# rotated to appear vertical. So we really are still justifying
|
|
73
|
+
# horizontally.
|
|
74
|
+
info = self.draw_info
|
|
75
|
+
lookup: dict[VerticalJustification, float] = {
|
|
76
|
+
"bottom": 0.0,
|
|
77
|
+
"center": 0.5,
|
|
78
|
+
"top": 1.0,
|
|
79
|
+
}
|
|
80
|
+
rel = lookup.get(info.va, 0.5) if isinstance(info.va, str) else info.va
|
|
81
|
+
patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
|
|
82
|
+
text_bbox = bbox_in_axes_space(self, info.ax, renderer)
|
|
83
|
+
l, b, w, h = info.x, info.y, patch_bbox.width, info.box_height
|
|
84
|
+
l = l + patch_bbox.width * info.strip_align
|
|
85
|
+
x = l + w / 2
|
|
86
|
+
y = rel_position(rel, text_bbox.height, patch_bbox.y0, patch_bbox.y1)
|
|
87
|
+
self.set_horizontalalignment("right") # 90CW right means bottom
|
|
88
|
+
self.patch.set_bounds(l, b, w, h)
|
|
89
|
+
self.set_position((x, y))
|
|
90
|
+
|
|
43
91
|
def draw(self, renderer: RendererBase):
|
|
44
92
|
if not self.get_visible():
|
|
45
93
|
return
|
|
46
94
|
|
|
47
|
-
|
|
48
|
-
# "fill up" spatch to contain the text
|
|
95
|
+
# expand strip_text patch to contain the text
|
|
49
96
|
self.patch.update_position_size(renderer)
|
|
50
97
|
|
|
51
|
-
# Get bbox of spatch in transAxes space
|
|
52
|
-
patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
|
|
53
|
-
|
|
54
98
|
# Align patch across the edge of the panel
|
|
55
|
-
if
|
|
56
|
-
|
|
57
|
-
b = b + patch_bbox.height * info.strip_align
|
|
99
|
+
if self.draw_info.position == "top":
|
|
100
|
+
self._justify_horizontally(renderer)
|
|
58
101
|
else: # "right"
|
|
59
|
-
|
|
60
|
-
l = l + patch_bbox.width * info.strip_align
|
|
102
|
+
self._justify_vertically(renderer)
|
|
61
103
|
|
|
62
|
-
self.patch.
|
|
63
|
-
self.patch.set_transform(info.ax.transAxes)
|
|
104
|
+
self.patch.set_transform(self.draw_info.ax.transAxes)
|
|
64
105
|
self.patch.set_mutation_scale(0)
|
|
65
106
|
|
|
66
107
|
# Put text in center of patch
|
|
67
|
-
self._x = l + w / 2
|
|
68
|
-
self._y = b + h / 2
|
|
69
|
-
|
|
70
|
-
# "anchor" aligns before rotation so the right-strip get properly
|
|
71
|
-
# centered text
|
|
72
108
|
self.set_rotation_mode("anchor")
|
|
73
|
-
self.
|
|
74
|
-
self.set_verticalalignment("center_baseline") # top-strip
|
|
109
|
+
self.set_verticalalignment("center_baseline")
|
|
75
110
|
|
|
76
111
|
# Draw spatch
|
|
77
112
|
self.patch.draw(renderer)
|
plotnine/_utils/__init__.py
CHANGED
|
@@ -299,7 +299,7 @@ def ninteraction(df: pd.DataFrame, drop: bool = False) -> list[int]:
|
|
|
299
299
|
return _id_var(df[df.columns[0]], drop)
|
|
300
300
|
|
|
301
301
|
# Calculate individual ids
|
|
302
|
-
ids = df.apply(_id_var, axis=0)
|
|
302
|
+
ids = df.apply(_id_var, axis=0, drop=drop)
|
|
303
303
|
ids = ids.reindex(columns=list(reversed(ids.columns)))
|
|
304
304
|
|
|
305
305
|
# Calculate dimensions
|
plotnine/facets/strips.py
CHANGED
|
@@ -63,11 +63,13 @@ class strip:
|
|
|
63
63
|
"""
|
|
64
64
|
theme = self.theme
|
|
65
65
|
position = self.position
|
|
66
|
+
|
|
66
67
|
if position == "top":
|
|
67
68
|
# The x & y values are just starting locations
|
|
68
69
|
# The final location is determined by the layout manager.
|
|
69
70
|
y = 1
|
|
70
|
-
ha
|
|
71
|
+
ha = theme.getp(("strip_text_x", "ha"), "center")
|
|
72
|
+
va = theme.getp(("strip_text_x", "va"), "bottom")
|
|
71
73
|
rotation = theme.getp(("strip_text_x", "rotation"))
|
|
72
74
|
box_width = 1
|
|
73
75
|
box_height = 0 # Determined by the text size
|
|
@@ -88,7 +90,8 @@ class strip:
|
|
|
88
90
|
# The x & y values are just starting locations
|
|
89
91
|
# The final location is determined by the layout manager.
|
|
90
92
|
x = 1
|
|
91
|
-
ha
|
|
93
|
+
ha = theme.getp(("strip_text_y", "ha"), "left")
|
|
94
|
+
va = theme.getp(("strip_text_y", "va"), "center")
|
|
92
95
|
rotation = theme.getp(("strip_text_y", "rotation"))
|
|
93
96
|
box_width = 0 # Determine by the text height
|
|
94
97
|
# TODO: Allow two unique paddings for either side.
|