plotnine 0.15.0a1__py3-none-any.whl → 0.15.0a3__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 (86) hide show
  1. plotnine/_mpl/layout_manager/_layout_items.py +85 -23
  2. plotnine/_mpl/layout_manager/_layout_tree.py +16 -6
  3. plotnine/_mpl/layout_manager/_spaces.py +5 -5
  4. plotnine/_mpl/patches.py +70 -34
  5. plotnine/_mpl/text.py +150 -63
  6. plotnine/_mpl/utils.py +1 -1
  7. plotnine/_utils/__init__.py +30 -2
  8. plotnine/doctools.py +1 -1
  9. plotnine/facets/strips.py +17 -28
  10. plotnine/geoms/annotation_logticks.py +7 -8
  11. plotnine/geoms/annotation_stripes.py +6 -6
  12. plotnine/geoms/geom.py +20 -8
  13. plotnine/geoms/geom_abline.py +3 -2
  14. plotnine/geoms/geom_blank.py +0 -3
  15. plotnine/geoms/geom_boxplot.py +4 -4
  16. plotnine/geoms/geom_crossbar.py +3 -3
  17. plotnine/geoms/geom_dotplot.py +1 -1
  18. plotnine/geoms/geom_errorbar.py +2 -2
  19. plotnine/geoms/geom_errorbarh.py +2 -2
  20. plotnine/geoms/geom_hline.py +3 -2
  21. plotnine/geoms/geom_linerange.py +2 -2
  22. plotnine/geoms/geom_map.py +3 -3
  23. plotnine/geoms/geom_path.py +10 -11
  24. plotnine/geoms/geom_point.py +4 -5
  25. plotnine/geoms/geom_pointrange.py +3 -5
  26. plotnine/geoms/geom_polygon.py +2 -3
  27. plotnine/geoms/geom_raster.py +4 -5
  28. plotnine/geoms/geom_rect.py +3 -4
  29. plotnine/geoms/geom_ribbon.py +7 -7
  30. plotnine/geoms/geom_rug.py +1 -1
  31. plotnine/geoms/geom_segment.py +2 -2
  32. plotnine/geoms/geom_smooth.py +3 -3
  33. plotnine/geoms/geom_step.py +2 -2
  34. plotnine/geoms/geom_text.py +2 -3
  35. plotnine/geoms/geom_violin.py +4 -5
  36. plotnine/geoms/geom_vline.py +3 -2
  37. plotnine/guides/guides.py +1 -1
  38. plotnine/helpers.py +49 -0
  39. plotnine/iapi.py +28 -5
  40. plotnine/layer.py +18 -12
  41. plotnine/mapping/_eval_environment.py +1 -1
  42. plotnine/scales/scale_color.py +46 -14
  43. plotnine/scales/scale_continuous.py +5 -4
  44. plotnine/scales/scale_datetime.py +28 -14
  45. plotnine/scales/scale_discrete.py +2 -2
  46. plotnine/scales/scale_identity.py +10 -2
  47. plotnine/scales/scale_xy.py +2 -2
  48. plotnine/stats/binning.py +4 -1
  49. plotnine/stats/smoothers.py +19 -19
  50. plotnine/stats/stat.py +15 -25
  51. plotnine/stats/stat_bin.py +2 -5
  52. plotnine/stats/stat_bin_2d.py +7 -9
  53. plotnine/stats/stat_bindot.py +6 -11
  54. plotnine/stats/stat_boxplot.py +5 -5
  55. plotnine/stats/stat_count.py +5 -9
  56. plotnine/stats/stat_density.py +6 -9
  57. plotnine/stats/stat_density_2d.py +12 -9
  58. plotnine/stats/stat_ecdf.py +6 -5
  59. plotnine/stats/stat_ellipse.py +5 -6
  60. plotnine/stats/stat_function.py +6 -8
  61. plotnine/stats/stat_hull.py +2 -3
  62. plotnine/stats/stat_identity.py +1 -2
  63. plotnine/stats/stat_pointdensity.py +4 -7
  64. plotnine/stats/stat_qq.py +45 -20
  65. plotnine/stats/stat_qq_line.py +15 -11
  66. plotnine/stats/stat_quantile.py +6 -7
  67. plotnine/stats/stat_sina.py +12 -14
  68. plotnine/stats/stat_smooth.py +7 -10
  69. plotnine/stats/stat_sum.py +1 -2
  70. plotnine/stats/stat_summary.py +6 -9
  71. plotnine/stats/stat_summary_bin.py +10 -13
  72. plotnine/stats/stat_unique.py +1 -2
  73. plotnine/stats/stat_ydensity.py +7 -10
  74. plotnine/themes/elements/__init__.py +2 -1
  75. plotnine/themes/elements/margin.py +64 -1
  76. plotnine/themes/theme_gray.py +5 -3
  77. plotnine/themes/theme_matplotlib.py +5 -4
  78. plotnine/themes/theme_seaborn.py +7 -4
  79. plotnine/themes/theme_void.py +11 -4
  80. plotnine/themes/themeable.py +2 -2
  81. plotnine/typing.py +2 -2
  82. {plotnine-0.15.0a1.dist-info → plotnine-0.15.0a3.dist-info}/METADATA +7 -4
  83. {plotnine-0.15.0a1.dist-info → plotnine-0.15.0a3.dist-info}/RECORD +86 -85
  84. {plotnine-0.15.0a1.dist-info → plotnine-0.15.0a3.dist-info}/WHEEL +1 -1
  85. {plotnine-0.15.0a1.dist-info → plotnine-0.15.0a3.dist-info}/licenses/LICENSE +0 -0
  86. {plotnine-0.15.0a1.dist-info → plotnine-0.15.0a3.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, cast
6
6
 
7
7
  from matplotlib.text import Text
8
8
 
9
+ from plotnine._mpl.patches import StripTextPatch
10
+ from plotnine._utils import ha_as_float, va_as_float
9
11
  from plotnine.exceptions import PlotnineError
10
12
 
11
13
  from ..utils import (
@@ -35,7 +37,11 @@ if TYPE_CHECKING:
35
37
  from plotnine._mpl.text import StripText
36
38
  from plotnine.iapi import legend_artists
37
39
  from plotnine.themes.elements import margin as Margin
38
- from plotnine.typing import StripPosition
40
+ from plotnine.typing import (
41
+ HorizontalJustification,
42
+ StripPosition,
43
+ VerticalJustification,
44
+ )
39
45
 
40
46
  from ._spaces import LayoutSpaces
41
47
 
@@ -299,9 +305,9 @@ class LayoutItems:
299
305
 
300
306
  return chain(major, minor)
301
307
 
302
- def strip_text_x_height(self, position: StripPosition) -> float:
308
+ def strip_text_x_extra_height(self, position: StripPosition) -> float:
303
309
  """
304
- Height taken up by the top strips
310
+ Height taken up by the top strips that is outside the panels
305
311
  """
306
312
  if not self.strip_text_x:
307
313
  return 0
@@ -311,11 +317,23 @@ class LayoutItems:
311
317
  for st in self.strip_text_x
312
318
  if st.patch.position == position
313
319
  ]
314
- return self.calc.max_height(artists)
315
320
 
316
- def strip_text_y_width(self, position: StripPosition) -> float:
321
+ heights = []
322
+
323
+ for a in artists:
324
+ info = (
325
+ a.text.draw_info
326
+ if isinstance(a, StripTextPatch)
327
+ else a.draw_info
328
+ )
329
+ h = self.calc.height(a)
330
+ heights.append(max(h + h * info.strip_align, 0))
331
+
332
+ return max(heights)
333
+
334
+ def strip_text_y_extra_width(self, position: StripPosition) -> float:
317
335
  """
318
- Width taken up by the right strips
336
+ Width taken up by the top strips that is outside the panels
319
337
  """
320
338
  if not self.strip_text_y:
321
339
  return 0
@@ -325,7 +343,19 @@ class LayoutItems:
325
343
  for st in self.strip_text_y
326
344
  if st.patch.position == position
327
345
  ]
328
- return self.calc.max_width(artists)
346
+
347
+ widths = []
348
+
349
+ for a in artists:
350
+ info = (
351
+ a.text.draw_info
352
+ if isinstance(a, StripTextPatch)
353
+ else a.draw_info
354
+ )
355
+ w = self.calc.width(a)
356
+ widths.append(max(w + w * info.strip_align, 0))
357
+
358
+ return max(widths)
329
359
 
330
360
  def axis_ticks_x_max_height_at(self, location: AxesLocation) -> float:
331
361
  """
@@ -489,6 +519,8 @@ class LayoutItems:
489
519
 
490
520
  self._adjust_axis_text_x(justify)
491
521
  self._adjust_axis_text_y(justify)
522
+ self._strip_text_x_background_equal_heights()
523
+ self._strip_text_y_background_equal_widths()
492
524
 
493
525
  def _adjust_axis_text_x(self, justify: TextJustifier):
494
526
  """
@@ -574,6 +606,36 @@ class LayoutItems:
574
606
  text, ha, -axis_text_col_width, 0, width=width
575
607
  )
576
608
 
609
+ def _strip_text_x_background_equal_heights(self):
610
+ """
611
+ Make the strip_text_x_backgrounds have equal heights
612
+
613
+ The smaller heights are expanded to match the largest height
614
+ """
615
+ if not self.strip_text_x:
616
+ return
617
+
618
+ heights = [self.calc.bbox(t.patch).height for t in self.strip_text_x]
619
+ max_height = max(heights)
620
+ relative_heights = [max_height / h for h in heights]
621
+ for text, scale in zip(self.strip_text_x, relative_heights):
622
+ text.patch.expand = scale
623
+
624
+ def _strip_text_y_background_equal_widths(self):
625
+ """
626
+ Make the strip_text_y_backgrounds have equal widths
627
+
628
+ The smaller widths are expanded to match the largest width
629
+ """
630
+ if not self.strip_text_y:
631
+ return
632
+
633
+ widths = [self.calc.bbox(t.patch).width for t in self.strip_text_y]
634
+ max_width = max(widths)
635
+ relative_widths = [max_width / w for w in widths]
636
+ for text, scale in zip(self.strip_text_y, relative_widths):
637
+ text.patch.expand = scale
638
+
577
639
 
578
640
  def _text_is_visible(text: Text) -> bool:
579
641
  """
@@ -596,7 +658,7 @@ class TextJustifier:
596
658
  def horizontally(
597
659
  self,
598
660
  text: Text,
599
- ha: str | float,
661
+ ha: HorizontalJustification | float,
600
662
  left: float,
601
663
  right: float,
602
664
  width: float | None = None,
@@ -604,8 +666,7 @@ class TextJustifier:
604
666
  """
605
667
  Horizontally Justify text between left and right
606
668
  """
607
- lookup = {"left": 0.0, "center": 0.5, "right": 1.0}
608
- rel = lookup.get(ha, ha) # pyright: ignore[reportCallIssue, reportArgumentType]
669
+ rel = ha_as_float(ha)
609
670
  if width is None:
610
671
  width = self.spaces.items.calc.width(text)
611
672
  x = rel_position(rel, width, left, right)
@@ -615,7 +676,7 @@ class TextJustifier:
615
676
  def vertically(
616
677
  self,
617
678
  text: Text,
618
- va: str | float,
679
+ va: VerticalJustification | float,
619
680
  bottom: float,
620
681
  top: float,
621
682
  height: float | None = None,
@@ -623,14 +684,7 @@ class TextJustifier:
623
684
  """
624
685
  Vertically Justify text between bottom and top
625
686
  """
626
- lookup = {
627
- "top": 1.0,
628
- "center": 0.5,
629
- "baseline": 0.5,
630
- "center_baseline": 0.5,
631
- "bottom": 0.0,
632
- }
633
- rel = lookup.get(va, va) # pyright: ignore[reportCallIssue, reportArgumentType]
687
+ rel = va_as_float(va)
634
688
 
635
689
  if height is None:
636
690
  height = self.spaces.items.calc.height(text)
@@ -638,13 +692,17 @@ class TextJustifier:
638
692
  text.set_y(y)
639
693
  text.set_verticalalignment("bottom")
640
694
 
641
- def horizontally_across_panel(self, text: Text, ha: str | float):
695
+ def horizontally_across_panel(
696
+ self, text: Text, ha: HorizontalJustification | float
697
+ ):
642
698
  """
643
699
  Horizontally Justify text accross the panel(s) width
644
700
  """
645
701
  self.horizontally(text, ha, self.spaces.l.left, self.spaces.r.right)
646
702
 
647
- def horizontally_across_plot(self, text: Text, ha: str | float):
703
+ def horizontally_across_plot(
704
+ self, text: Text, ha: HorizontalJustification | float
705
+ ):
648
706
  """
649
707
  Horizontally Justify text across the plot's width
650
708
  """
@@ -652,13 +710,17 @@ class TextJustifier:
652
710
  text, ha, self.spaces.l.plot_left, self.spaces.r.plot_right
653
711
  )
654
712
 
655
- def vertically_along_panel(self, text: Text, va: str | float):
713
+ def vertically_along_panel(
714
+ self, text: Text, va: VerticalJustification | float
715
+ ):
656
716
  """
657
717
  Horizontally Justify text along the panel(s) height
658
718
  """
659
719
  self.vertically(text, va, self.spaces.b.bottom, self.spaces.t.top)
660
720
 
661
- def vertically_along_plot(self, text: Text, va: str | float):
721
+ def vertically_along_plot(
722
+ self, text: Text, va: VerticalJustification | float
723
+ ):
662
724
  """
663
725
  Vertically Justify text along the plot's height
664
726
  """
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import abc
4
4
  from dataclasses import dataclass
5
5
  from functools import cached_property
6
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, cast
7
7
 
8
8
  import numpy as np
9
9
 
@@ -528,8 +528,9 @@ class ColumnsTree(LayoutTree):
528
528
  if self.bottom_tags_align:
529
529
  return
530
530
 
531
- values = max(self.bottom_tag_heights) - np.array(
532
- self.bottom_tag_heights
531
+ values = cast(
532
+ "Sequence[float]",
533
+ max(self.bottom_tag_heights) - np.array(self.bottom_tag_heights),
533
534
  )
534
535
  for item, value in zip(self.nodes, values):
535
536
  if isinstance(item, LayoutSpaces):
@@ -541,7 +542,10 @@ class ColumnsTree(LayoutTree):
541
542
  if self.top_tags_align:
542
543
  return
543
544
 
544
- values = max(self.top_tag_heights) - np.array(self.top_tag_heights)
545
+ values = cast(
546
+ "Sequence[float]",
547
+ max(self.top_tag_heights) - np.array(self.top_tag_heights),
548
+ )
545
549
  for item, value in zip(self.nodes, values):
546
550
  if isinstance(item, LayoutSpaces):
547
551
  item.t.tag_alignment = value
@@ -776,7 +780,10 @@ class RowsTree(LayoutTree):
776
780
  if self.left_tags_align:
777
781
  return
778
782
 
779
- values = max(self.left_tag_widths) - np.array(self.left_tag_widths)
783
+ values = cast(
784
+ "Sequence[float]",
785
+ max(self.left_tag_widths) - np.array(self.left_tag_widths),
786
+ )
780
787
  for item, value in zip(self.nodes, values):
781
788
  if isinstance(item, LayoutSpaces):
782
789
  item.l.tag_alignment = value
@@ -787,7 +794,10 @@ class RowsTree(LayoutTree):
787
794
  if self.right_tags_align:
788
795
  return
789
796
 
790
- values = max(self.right_tag_widths) - np.array(self.right_tag_widths)
797
+ values = cast(
798
+ "Sequence[float]",
799
+ max(self.right_tag_widths) - np.array(self.right_tag_widths),
800
+ )
791
801
  for item, value in zip(self.nodes, values):
792
802
  if isinstance(item, LayoutSpaces):
793
803
  item.r.tag_alignment = value
@@ -433,7 +433,7 @@ class right_spaces(_side_spaces):
433
433
  margin_alignment: float = 0
434
434
  legend: float = 0
435
435
  legend_box_spacing: float = 0
436
- strip_text_y_width_right: float = 0
436
+ strip_text_y_extra_width: float = 0
437
437
 
438
438
  def _calculate(self):
439
439
  items = self.items
@@ -452,7 +452,7 @@ class right_spaces(_side_spaces):
452
452
  self.legend = self.legend_width
453
453
  self.legend_box_spacing = theme.getp("legend_box_spacing")
454
454
 
455
- self.strip_text_y_width_right = items.strip_text_y_width("right")
455
+ self.strip_text_y_extra_width = items.strip_text_y_extra_width("right")
456
456
 
457
457
  # Adjust plot_margin to make room for ylabels that protude well
458
458
  # beyond the axes
@@ -545,7 +545,7 @@ class top_spaces(_side_spaces):
545
545
  plot_subtitle_margin_bottom: float = 0
546
546
  legend: float = 0
547
547
  legend_box_spacing: float = 0
548
- strip_text_x_height_top: float = 0
548
+ strip_text_x_extra_height: float = 0
549
549
 
550
550
  def _calculate(self):
551
551
  items = self.items
@@ -578,7 +578,7 @@ class top_spaces(_side_spaces):
578
578
  self.legend = self.legend_height
579
579
  self.legend_box_spacing = theme.getp("legend_box_spacing") * F
580
580
 
581
- self.strip_text_x_height_top = items.strip_text_x_height("top")
581
+ self.strip_text_x_extra_height = items.strip_text_x_extra_height("top")
582
582
 
583
583
  # Adjust plot_margin to make room for ylabels that protude well
584
584
  # beyond the axes
@@ -1061,7 +1061,7 @@ class LayoutSpaces:
1061
1061
  # Only interested in the proportion of the strip that
1062
1062
  # does not overlap with the panel
1063
1063
  if strip_align_x > -1:
1064
- self.sh += self.t.strip_text_x_height_top * (1 + strip_align_x)
1064
+ self.sh += self.t.strip_text_x_extra_height * (1 + strip_align_x)
1065
1065
 
1066
1066
  if facet.free["x"]:
1067
1067
  self.sh += self.items.axis_text_x_max_height_at(
plotnine/_mpl/patches.py CHANGED
@@ -4,12 +4,11 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  from matplotlib import artist
6
6
  from matplotlib.patches import FancyBboxPatch, Rectangle
7
- from matplotlib.text import _get_textbox # type: ignore
8
- from matplotlib.transforms import Affine2D
7
+ from matplotlib.transforms import Bbox
9
8
 
10
- if TYPE_CHECKING:
11
- from matplotlib.backend_bases import RendererBase
9
+ from plotnine._mpl.utils import rel_position
12
10
 
11
+ if TYPE_CHECKING:
13
12
  from plotnine.typing import StripPosition
14
13
 
15
14
  from .text import StripText
@@ -26,51 +25,88 @@ class StripTextPatch(FancyBboxPatch):
26
25
  Strip text background box
27
26
  """
28
27
 
29
- # The text artists that is wrapped by this box
30
28
  text: StripText
29
+ """
30
+ The text artists that is wrapped by this box
31
+ """
32
+
31
33
  position: StripPosition
32
- _update = False
34
+ """
35
+ The position of the strip_text associated with this patch
36
+ """
33
37
 
34
- def __init__(self, text: StripText):
35
- boxstyle = f"square, pad={text.draw_info.strip_text_margin}"
38
+ expand: float = 1
39
+ """
40
+ Factor by which to expand the thickness of this patch.
36
41
 
42
+ This value is used by the layout manager to increase the breadth
43
+ of the narrower strip_backgrounds.
44
+ """
45
+
46
+ def __init__(self, text: StripText):
37
47
  super().__init__(
38
- (0, 0), 1, 1, boxstyle=boxstyle, clip_on=False, zorder=2.2
48
+ # The position, width and height are determine in
49
+ # .get_window_extent.
50
+ (0, 0),
51
+ width=1,
52
+ height=1,
53
+ boxstyle="square, pad=0",
54
+ clip_on=False,
55
+ zorder=2.2,
39
56
  )
40
57
 
41
58
  self.text = text
42
59
  self.position = text.draw_info.position
43
60
 
44
- def update_position_size(self, renderer: RendererBase):
61
+ def get_window_extent(self, renderer=None):
45
62
  """
46
- Update the location and the size of the bbox.
63
+ Location & dimensions of the box in display coordinates
47
64
  """
48
- if self._update:
49
- return
50
-
51
- text = self.text
52
- posx, posy = text.get_transform().transform(text.get_position())
53
- x, y, w, h = _get_textbox(text, renderer)
54
-
55
- self.set_bounds(0.0, 0.0, w, h)
56
- self.set_transform(
57
- Affine2D()
58
- .rotate_deg(text.get_rotation())
59
- .translate(posx + x, posy + y)
60
- )
61
- fontsize_in_pixel = renderer.points_to_pixels(
62
- text.get_size() # type: ignore
63
- )
64
- self.set_mutation_scale(fontsize_in_pixel) # type: ignore
65
- self._update = True
65
+ info = self.text.draw_info
66
+ m = info.margin
67
+
68
+ # bboxes in display space
69
+ text_bbox = self.text.get_window_extent(renderer)
70
+ ax_bbox = info.ax.bbox.frozen()
71
+
72
+ # line height in display space
73
+ line_height = self.text._line_height(renderer)
74
+
75
+ # Convert the bottom left coordinates of the patch from
76
+ # transAxes to display space. We are not justifying the patch
77
+ # within the axes so we use 0 for the lengths, this gives us
78
+ # a patch that starts at the edge of the axes and not one that
79
+ # ends at the edge
80
+ x0 = rel_position(info.bg_x, 0, ax_bbox.x0, ax_bbox.x1)
81
+ y0 = rel_position(info.bg_y, 0, ax_bbox.y0, ax_bbox.y1)
82
+
83
+ # info.bg_width and info.bg_height are in axes space
84
+ # so they are a scaling factor
85
+ if info.position == "top":
86
+ width = ax_bbox.width * info.bg_width
87
+ height = text_bbox.height + ((m.b + m.t) * line_height)
88
+ height *= self.expand
89
+ y0 += height * info.strip_align
90
+ else:
91
+ height = ax_bbox.height * info.bg_height
92
+ width = text_bbox.width + ((m.l + m.r) * line_height)
93
+ width *= self.expand
94
+ x0 += width * info.strip_align
95
+
96
+ return Bbox.from_bounds(x0, y0, width, height)
66
97
 
67
- def get_window_extent(self, renderer=None):
98
+ @artist.allow_rasterization
99
+ def draw(self, renderer):
68
100
  """
69
- Location & dimensions of the box
101
+ Draw patch
70
102
  """
71
- if renderer:
72
- self.update_position_size(renderer)
73
- return super().get_window_extent(renderer)
103
+ # The geometry of the patch is determined by its rectangular bounds,
104
+ # this is also its "window_extent". As the extent value is in
105
+ # display units, we don't need a transform.
106
+ bbox = self.get_window_extent(renderer)
107
+ self.set_bounds(bbox.bounds)
108
+ self.set_transform(None)
109
+ return super().draw(renderer)
74
110
 
75
111
 
76
112
  class InsideStrokedRectangle(Rectangle):