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.
@@ -299,40 +299,6 @@ class LayoutItems:
299
299
 
300
300
  return chain(major, minor)
301
301
 
302
- def axis_text_x_margin(self, ax: Axes) -> Iterator[float]:
303
- """
304
- Return XTicks paddings
305
- """
306
- # In plotnine tick padding are specified as a margin to the
307
- # the axis_text.
308
- major, minor = [], []
309
- if not self._is_blank("axis_text_x"):
310
- h = self.plot.figure.bbox.height
311
- major = [
312
- (t.get_pad() or 0) / h for t in ax.xaxis.get_major_ticks()
313
- ]
314
- minor = [
315
- (t.get_pad() or 0) / h for t in ax.xaxis.get_minor_ticks()
316
- ]
317
- return chain(major, minor)
318
-
319
- def axis_text_y_margin(self, ax: Axes) -> Iterator[float]:
320
- """
321
- Return YTicks paddings
322
- """
323
- # In plotnine tick padding are specified as a margin to the
324
- # the axis_text.
325
- major, minor = [], []
326
- if not self._is_blank("axis_text_y"):
327
- w = self.plot.figure.bbox.width
328
- major = [
329
- (t.get_pad() or 0) / w for t in ax.yaxis.get_major_ticks()
330
- ]
331
- minor = [
332
- (t.get_pad() or 0) / w for t in ax.yaxis.get_minor_ticks()
333
- ]
334
- return chain(major, minor)
335
-
336
302
  def strip_text_x_height(self, position: StripPosition) -> float:
337
303
  """
338
304
  Height taken up by the top strips
@@ -361,9 +327,9 @@ class LayoutItems:
361
327
  ]
362
328
  return self.calc.max_width(artists)
363
329
 
364
- def axis_ticks_x_max_height(self, location: AxesLocation) -> float:
330
+ def axis_ticks_x_max_height_at(self, location: AxesLocation) -> float:
365
331
  """
366
- Return maximum height[inches] of x ticks
332
+ Return maximum height[figure space] of x ticks
367
333
  """
368
334
  heights = [
369
335
  self.calc.tight_height(tick.tick1line)
@@ -372,22 +338,28 @@ class LayoutItems:
372
338
  ]
373
339
  return max(heights) if len(heights) else 0
374
340
 
375
- def axis_text_x_max_height(self, location: AxesLocation) -> float:
341
+ def axis_text_x_max_height(self, ax: Axes) -> float:
376
342
  """
377
- Return maximum height[inches] of x tick labels
343
+ Return maximum height[figure space] of x tick labels
378
344
  """
379
345
  heights = [
380
- self.calc.tight_height(label) + pad
346
+ self.calc.tight_height(label) for label in self.axis_text_x(ax)
347
+ ]
348
+ return max(heights) if len(heights) else 0
349
+
350
+ def axis_text_x_max_height_at(self, location: AxesLocation) -> float:
351
+ """
352
+ Return maximum height[figure space] of x tick labels
353
+ """
354
+ heights = [
355
+ self.axis_text_x_max_height(ax)
381
356
  for ax in self._filter_axes(location)
382
- for label, pad in zip(
383
- self.axis_text_x(ax), self.axis_text_x_margin(ax)
384
- )
385
357
  ]
386
358
  return max(heights) if len(heights) else 0
387
359
 
388
- def axis_ticks_y_max_width(self, location: AxesLocation) -> float:
360
+ def axis_ticks_y_max_width_at(self, location: AxesLocation) -> float:
389
361
  """
390
- Return maximum width[inches] of y ticks
362
+ Return maximum width[figure space] of y ticks
391
363
  """
392
364
  widths = [
393
365
  self.calc.tight_width(tick.tick1line)
@@ -396,22 +368,28 @@ class LayoutItems:
396
368
  ]
397
369
  return max(widths) if len(widths) else 0
398
370
 
399
- def axis_text_y_max_width(self, location: AxesLocation) -> float:
371
+ def axis_text_y_max_width(self, ax: Axes) -> float:
372
+ """
373
+ Return maximum width[figure space] of y tick labels
374
+ """
375
+ widths = [
376
+ self.calc.tight_width(label) for label in self.axis_text_y(ax)
377
+ ]
378
+ return max(widths) if len(widths) else 0
379
+
380
+ def axis_text_y_max_width_at(self, location: AxesLocation) -> float:
400
381
  """
401
- Return maximum width[inches] of y tick labels
382
+ Return maximum width[figure space] of y tick labels
402
383
  """
403
384
  widths = [
404
- self.calc.tight_width(label) + pad
385
+ self.axis_text_y_max_width(ax)
405
386
  for ax in self._filter_axes(location)
406
- for label, pad in zip(
407
- self.axis_text_y(ax), self.axis_text_y_margin(ax)
408
- )
409
387
  ]
410
388
  return max(widths) if len(widths) else 0
411
389
 
412
390
  def axis_text_y_top_protrusion(self, location: AxesLocation) -> float:
413
391
  """
414
- Return maximum height[inches] above the axes of y tick labels
392
+ Return maximum height[figure space] above the axes of y tick labels
415
393
  """
416
394
  extras = []
417
395
  for ax in self._filter_axes(location):
@@ -424,7 +402,7 @@ class LayoutItems:
424
402
 
425
403
  def axis_text_y_bottom_protrusion(self, location: AxesLocation) -> float:
426
404
  """
427
- Return maximum height[inches] below the axes of y tick labels
405
+ Return maximum height[figure space] below the axes of y tick labels
428
406
  """
429
407
  extras = []
430
408
  for ax in self._filter_axes(location):
@@ -438,7 +416,7 @@ class LayoutItems:
438
416
 
439
417
  def axis_text_x_left_protrusion(self, location: AxesLocation) -> float:
440
418
  """
441
- Return maximum width[inches] of x tick labels to the left of the axes
419
+ Return maximum width[figure space] left of the axes of x tick labels
442
420
  """
443
421
  extras = []
444
422
  for ax in self._filter_axes(location):
@@ -452,7 +430,7 @@ class LayoutItems:
452
430
 
453
431
  def axis_text_x_right_protrusion(self, location: AxesLocation) -> float:
454
432
  """
455
- Return maximum width[inches] of x tick labels to the right of the axes
433
+ Return maximum width[figure space] right of the axes of y tick labels
456
434
  """
457
435
  extras = []
458
436
  for ax in self._filter_axes(location):
@@ -470,6 +448,7 @@ class LayoutItems:
470
448
  theme = self.plot.theme
471
449
  plot_title_position = theme.getp("plot_title_position", "panel")
472
450
  plot_caption_position = theme.getp("plot_caption_position", "panel")
451
+ justify = TextJustifier(spaces)
473
452
 
474
453
  if self.plot_tag:
475
454
  set_plot_tag_position(self.plot_tag, spaces)
@@ -477,37 +456,124 @@ class LayoutItems:
477
456
  if self.plot_title:
478
457
  ha = theme.getp(("plot_title", "ha"))
479
458
  self.plot_title.set_y(spaces.t.y2("plot_title"))
480
- horizontally_align_text(
481
- self.plot_title, ha, spaces, plot_title_position
459
+ justify.horizontally_about(
460
+ self.plot_title, ha, plot_title_position
482
461
  )
483
462
 
484
463
  if self.plot_subtitle:
485
464
  ha = theme.getp(("plot_subtitle", "ha"))
486
465
  self.plot_subtitle.set_y(spaces.t.y2("plot_subtitle"))
487
- horizontally_align_text(
488
- self.plot_subtitle, ha, spaces, plot_title_position
466
+ justify.horizontally_about(
467
+ self.plot_subtitle, ha, plot_title_position
489
468
  )
490
469
 
491
470
  if self.plot_caption:
492
471
  ha = theme.getp(("plot_caption", "ha"), "right")
493
472
  self.plot_caption.set_y(spaces.b.y1("plot_caption"))
494
- horizontally_align_text(
495
- self.plot_caption, ha, spaces, plot_caption_position
473
+ justify.horizontally_about(
474
+ self.plot_caption, ha, plot_caption_position
496
475
  )
497
476
 
498
477
  if self.axis_title_x:
499
478
  ha = theme.getp(("axis_title_x", "ha"), "center")
500
479
  self.axis_title_x.set_y(spaces.b.y1("axis_title_x"))
501
- horizontally_align_text(self.axis_title_x, ha, spaces)
480
+ justify.horizontally_about(self.axis_title_x, ha, "panel")
502
481
 
503
482
  if self.axis_title_y:
504
483
  va = theme.getp(("axis_title_y", "va"), "center")
505
484
  self.axis_title_y.set_x(spaces.l.x1("axis_title_y"))
506
- vertically_align_text(self.axis_title_y, va, spaces)
485
+ justify.vertically_about(self.axis_title_y, va, "panel")
507
486
 
508
487
  if self.legends:
509
488
  set_legends_position(self.legends, spaces)
510
489
 
490
+ self._adjust_axis_text_x(justify)
491
+ self._adjust_axis_text_y(justify)
492
+
493
+ def _adjust_axis_text_x(self, justify: TextJustifier):
494
+ """
495
+ Adjust x-axis text, justifying vertically as necessary
496
+ """
497
+
498
+ def to_vertical_axis_dimensions(value: float, ax: Axes) -> float:
499
+ """
500
+ Convert value in figure dimensions to axis dimensions
501
+ """
502
+ _, H = self.plot.figure.bbox.size
503
+ h = ax.get_window_extent().height
504
+ return value * H / h
505
+
506
+ if self._is_blank("axis_text_x"):
507
+ return
508
+
509
+ va = self.plot.theme.getp(("axis_text_x", "va"), "top")
510
+
511
+ for ax in self.plot.axs:
512
+ texts = list(self.axis_text_x(ax))
513
+ axis_text_row_height = to_vertical_axis_dimensions(
514
+ self.axis_text_x_max_height(ax), ax
515
+ )
516
+ for text in texts:
517
+ height = to_vertical_axis_dimensions(
518
+ self.calc.tight_height(text), ax
519
+ )
520
+ justify.vertically(
521
+ text, va, -axis_text_row_height, 0, height=height
522
+ )
523
+
524
+ def _adjust_axis_text_y(self, justify: TextJustifier):
525
+ """
526
+ Adjust x-axis text, justifying horizontally as necessary
527
+ """
528
+
529
+ def to_horizontal_axis_dimensions(value: float, ax: Axes) -> float:
530
+ """
531
+ Convert value in figure dimensions to axis dimensions
532
+
533
+ Matplotlib expects x position of y-axis text is in transAxes,
534
+ but all our layout measurements are in transFigure.
535
+
536
+ ---------------------
537
+ | |
538
+ | ----------- |
539
+ | X | | |
540
+ | X | | |
541
+ | X | | |
542
+ | X | | |
543
+ | X | | |
544
+ | X | | |
545
+ | 0-----------1 |
546
+ | axes |
547
+ | |
548
+ 0---------------------1
549
+ figure
550
+
551
+ We do not set the transform to transFigure because, then we need
552
+ to calculate the position in transFigure; accounting for all the
553
+ space wherever the panel may be.
554
+ """
555
+ W, _ = self.plot.figure.bbox.size
556
+ w = ax.get_window_extent().width
557
+ return value * W / w
558
+
559
+ if self._is_blank("axis_text_y"):
560
+ return
561
+
562
+ ha = self.plot.theme.getp(("axis_text_y", "ha"), "right")
563
+
564
+ for ax in self.plot.axs:
565
+ texts = list(self.axis_text_y(ax))
566
+ axis_text_col_width = to_horizontal_axis_dimensions(
567
+ self.axis_text_y_max_width(ax), ax
568
+ )
569
+ for text in texts:
570
+ width = to_horizontal_axis_dimensions(
571
+ self.calc.tight_width(text), ax
572
+ )
573
+ justify.horizontally(
574
+ text, ha, -axis_text_col_width, 0, width=width
575
+ )
576
+
511
577
 
512
578
  def _text_is_visible(text: Text) -> bool:
513
579
  """
@@ -516,54 +582,47 @@ def _text_is_visible(text: Text) -> bool:
516
582
  return text.get_visible() and text._text # type: ignore
517
583
 
518
584
 
519
- def horizontally_align_text(
520
- text: Text,
521
- ha: str | float,
522
- spaces: LayoutSpaces,
523
- how: Literal["panel", "plot"] = "panel",
524
- ):
585
+ @dataclass
586
+ class TextJustifier:
525
587
  """
526
- Horizontal justification
588
+ Justify Text
527
589
 
528
- Reinterpret horizontal alignment to be justification about the panels or
529
- the plot (depending on the how parameter)
590
+ The justification methods reinterpret alignment values to be justification
591
+ about a span.
530
592
  """
531
- if isinstance(ha, str):
532
- lookup = {
533
- "left": 0.0,
534
- "center": 0.5,
535
- "right": 1.0,
536
- }
537
- rel = lookup[ha]
538
- else:
539
- rel = ha
540
-
541
- if how == "panel":
542
- left = spaces.l.left
543
- right = spaces.r.right
544
- else:
545
- left = spaces.l.plot_left
546
- right = spaces.r.plot_right
547
-
548
- width = spaces.items.calc.width(text)
549
- x = rel_position(rel, width, left, right)
550
- text.set_x(x)
551
- text.set_horizontalalignment("left")
552
593
 
594
+ spaces: LayoutSpaces
553
595
 
554
- def vertically_align_text(
555
- text: Text,
556
- va: str | float,
557
- spaces: LayoutSpaces,
558
- how: Literal["panel", "plot"] = "panel",
559
- ):
560
- """
561
- Vertical justification
562
-
563
- Reinterpret vertical alignment to be justification about the panels or
564
- the plot (depending on the how parameter).
565
- """
566
- if isinstance(va, str):
596
+ def horizontally(
597
+ self,
598
+ text: Text,
599
+ ha: str | float,
600
+ left: float,
601
+ right: float,
602
+ width: float | None = None,
603
+ ):
604
+ """
605
+ Horizontally Justify text between left and right
606
+ """
607
+ lookup = {"left": 0.0, "center": 0.5, "right": 1.0}
608
+ rel = lookup.get(ha, ha) # pyright: ignore[reportCallIssue, reportArgumentType]
609
+ if width is None:
610
+ width = self.spaces.items.calc.width(text)
611
+ x = rel_position(rel, width, left, right)
612
+ text.set_x(x)
613
+ text.set_horizontalalignment("left")
614
+
615
+ def vertically(
616
+ self,
617
+ text: Text,
618
+ va: str | float,
619
+ bottom: float,
620
+ top: float,
621
+ height: float | None = None,
622
+ ):
623
+ """
624
+ Vertically Justify text between bottom and top
625
+ """
567
626
  lookup = {
568
627
  "top": 1.0,
569
628
  "center": 0.5,
@@ -571,21 +630,63 @@ def vertically_align_text(
571
630
  "center_baseline": 0.5,
572
631
  "bottom": 0.0,
573
632
  }
574
- rel = lookup[va]
575
- else:
576
- rel = va
633
+ rel = lookup.get(va, va) # pyright: ignore[reportCallIssue, reportArgumentType]
577
634
 
578
- if how == "panel":
579
- top = spaces.t.top
580
- bottom = spaces.b.bottom
581
- else:
582
- top = spaces.t.plot_top
583
- bottom = spaces.b.plot_bottom
635
+ if height is None:
636
+ height = self.spaces.items.calc.height(text)
637
+ y = rel_position(rel, height, bottom, top)
638
+ text.set_y(y)
639
+ text.set_verticalalignment("bottom")
640
+
641
+ def horizontally_across_panel(self, text: Text, ha: str | float):
642
+ """
643
+ Horizontally Justify text accross the panel(s) width
644
+ """
645
+ self.horizontally(text, ha, self.spaces.l.left, self.spaces.r.right)
646
+
647
+ def horizontally_across_plot(self, text: Text, ha: str | float):
648
+ """
649
+ Horizontally Justify text across the plot's width
650
+ """
651
+ self.horizontally(
652
+ text, ha, self.spaces.l.plot_left, self.spaces.r.plot_right
653
+ )
654
+
655
+ def vertically_along_panel(self, text: Text, va: str | float):
656
+ """
657
+ Horizontally Justify text along the panel(s) height
658
+ """
659
+ self.vertically(text, va, self.spaces.b.bottom, self.spaces.t.top)
660
+
661
+ def vertically_along_plot(self, text: Text, va: str | float):
662
+ """
663
+ Vertically Justify text along the plot's height
664
+ """
665
+ self.vertically(
666
+ text, va, self.spaces.b.plot_bottom, self.spaces.t.plot_top
667
+ )
584
668
 
585
- height = spaces.items.calc.height(text)
586
- y = rel_position(rel, height, bottom, top)
587
- text.set_y(y)
588
- text.set_verticalalignment("bottom")
669
+ def horizontally_about(
670
+ self, text: Text, ratio: float, how: Literal["panel", "plot"]
671
+ ):
672
+ """
673
+ Horizontally Justify text across the panel or plot
674
+ """
675
+ if how == "panel":
676
+ self.horizontally_across_panel(text, ratio)
677
+ else:
678
+ self.horizontally_across_plot(text, ratio)
679
+
680
+ def vertically_about(
681
+ self, text: Text, ratio: float, how: Literal["panel", "plot"]
682
+ ):
683
+ """
684
+ Vertically Justify text along the panel or plot
685
+ """
686
+ if how == "panel":
687
+ self.vertically_along_panel(text, ratio)
688
+ else:
689
+ self.vertically_along_plot(text, ratio)
589
690
 
590
691
 
591
692
  def set_legends_position(legends: legend_artists, spaces: LayoutSpaces):