plotnine 0.16.0a2__py3-none-any.whl → 0.16.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.
@@ -14,6 +14,9 @@ from plotnine._mpl.utils import (
14
14
  if TYPE_CHECKING:
15
15
  from typing import Any
16
16
 
17
+ from matplotlib.lines import Line2D
18
+ from matplotlib.patches import Rectangle
19
+
17
20
  from plotnine.composition._compose import Compose
18
21
 
19
22
  from ._composition_side_space import CompositionSideSpaces
@@ -44,6 +47,11 @@ class CompositionLayoutItems:
44
47
  self.plot_title: Text | None = get("plot_title")
45
48
  self.plot_subtitle: Text | None = get("plot_subtitle")
46
49
  self.plot_caption: Text | None = get("plot_caption")
50
+ self.plot_footer: Text | None = get("plot_footer")
51
+ self.plot_footer_background: Rectangle | None = get(
52
+ "plot_footer_background"
53
+ )
54
+ self.plot_footer_line: Line2D | None = get("plot_footer_line")
47
55
 
48
56
  def _is_blank(self, name: str) -> bool:
49
57
  return self.cmp.theme.T.is_blank(name)
@@ -55,6 +63,7 @@ class CompositionLayoutItems:
55
63
  theme = self.cmp.theme
56
64
  plot_title_position = theme.getp("plot_title_position", "panel")
57
65
  plot_caption_position = theme.getp("plot_caption_position", "panel")
66
+ plot_footer_position = theme.getp("plot_footer_position", "plot")
58
67
  justify = CompositionTextJustifier(spaces)
59
68
 
60
69
  if self.plot_title:
@@ -78,6 +87,40 @@ class CompositionLayoutItems:
78
87
  self.plot_caption, ha, plot_caption_position
79
88
  )
80
89
 
90
+ if self.plot_footer:
91
+ ha = theme.getp(("plot_footer", "ha"), "left")
92
+ self.plot_footer.set_y(spaces.b.y1("plot_footer"))
93
+ justify.horizontally_about(
94
+ self.plot_footer, ha, plot_footer_position
95
+ )
96
+ self._resize_plot_footer_background(spaces)
97
+ self._resize_plot_footer_line(spaces)
98
+
99
+ def _resize_plot_footer_background(self, spaces: CompositionSideSpaces):
100
+ """
101
+ Resize the plot footer to the size of the footer
102
+ """
103
+ if not self.plot_footer_background:
104
+ return
105
+
106
+ self.plot_footer_background.set_x(spaces.l.offset)
107
+ self.plot_footer_background.set_y(spaces.b.offset)
108
+ self.plot_footer_background.set_height(spaces.b.footer_height)
109
+ self.plot_footer_background.set_width(spaces.plot_width)
110
+
111
+ def _resize_plot_footer_line(self, spaces: CompositionSideSpaces):
112
+ """
113
+ Resize the footer line to be a border above the footer
114
+ """
115
+ if not self.plot_footer_line:
116
+ return
117
+
118
+ x1 = spaces.l.offset
119
+ x2 = x1 + spaces.plot_width
120
+ y1 = y2 = spaces.b.offset + spaces.b.footer_height
121
+ self.plot_footer_line.set_xdata([x1, x2])
122
+ self.plot_footer_line.set_ydata([y1, y2])
123
+
81
124
 
82
125
  class CompositionTextJustifier(TextJustifier):
83
126
  """
@@ -218,6 +218,9 @@ class composition_bottom_space(_composition_side_space):
218
218
  Ordered from the edge of the figure and going inwards
219
219
  """
220
220
 
221
+ plot_footer_margin_bottom: float = 0
222
+ plot_footer: float = 0
223
+ plot_footer_margin_top: float = 0
221
224
  plot_margin: float = 0
222
225
  plot_caption_margin_bottom: float = 0
223
226
  plot_caption: float = 0
@@ -231,6 +234,11 @@ class composition_bottom_space(_composition_side_space):
231
234
  F = W / H
232
235
 
233
236
  self.plot_margin = theme.getp("plot_margin_bottom") * F
237
+ if items.plot_footer:
238
+ m = theme.get_margin("plot_footer").fig
239
+ self.plot_footer_margin_bottom = m.b * F
240
+ self.plot_footer = geometry.height(items.plot_footer)
241
+ self.plot_footer_margin_top = m.t * F
234
242
 
235
243
  if items.plot_caption:
236
244
  m = theme.get_margin("plot_caption").fig
@@ -269,6 +277,17 @@ class composition_bottom_space(_composition_side_space):
269
277
  """
270
278
  return self.to_figure_space(self.sum_incl(item))
271
279
 
280
+ @property
281
+ def footer_height(self):
282
+ """
283
+ The height of the footer including the margins
284
+ """
285
+ return (
286
+ self.plot_footer_margin_bottom
287
+ + self.plot_footer
288
+ + self.plot_footer_margin_top
289
+ )
290
+
272
291
  @property
273
292
  def items_bottom_relative(self):
274
293
  """
@@ -361,6 +380,20 @@ class CompositionSideSpaces:
361
380
  0,
362
381
  )
363
382
 
383
+ @property
384
+ def plot_width(self) -> float:
385
+ """
386
+ Width [figure dimensions] of the whole plot composition
387
+ """
388
+ return float(self.gridspec.width)
389
+
390
+ @property
391
+ def plot_height(self) -> float:
392
+ """
393
+ Height [figure dimensions] of the whole plot composition
394
+ """
395
+ return float(self.gridspec.height)
396
+
364
397
  @property
365
398
  def horizontal_space(self) -> float:
366
399
  """
@@ -26,6 +26,8 @@ if TYPE_CHECKING:
26
26
 
27
27
  from matplotlib.axes import Axes
28
28
  from matplotlib.axis import Tick
29
+ from matplotlib.lines import Line2D
30
+ from matplotlib.patches import Rectangle
29
31
  from matplotlib.transforms import Transform
30
32
 
31
33
  from plotnine import ggplot
@@ -86,12 +88,18 @@ class PlotLayoutItems:
86
88
  # # AnchoredOffsetboxes (groups of legends)
87
89
  self.legends: legend_artists | None = get("legends")
88
90
  self.plot_caption: Text | None = get("plot_caption")
91
+ self.plot_footer: Text | None = get("plot_footer")
89
92
  self.plot_subtitle: Text | None = get("plot_subtitle")
90
93
  self.plot_title: Text | None = get("plot_title")
91
94
  self.plot_tag: Text | None = get("plot_tag")
92
95
  self.strip_text_x: list[StripText] | None = get("strip_text_x")
93
96
  self.strip_text_y: list[StripText] | None = get("strip_text_y")
94
97
 
98
+ self.plot_footer_background: Rectangle | None = get(
99
+ "plot_footer_background"
100
+ )
101
+ self.plot_footer_line: Line2D | None = get("plot_footer_line")
102
+
95
103
  def _is_blank(self, name: str) -> bool:
96
104
  return self.plot.theme.T.is_blank(name)
97
105
 
@@ -345,6 +353,7 @@ class PlotLayoutItems:
345
353
  theme = self.plot.theme
346
354
  plot_title_position = theme.getp("plot_title_position", "panel")
347
355
  plot_caption_position = theme.getp("plot_caption_position", "panel")
356
+ plot_footer_position = theme.getp("plot_footer_position", "plot")
348
357
  justify = PlotTextJustifier(spaces)
349
358
 
350
359
  if self.plot_tag:
@@ -371,6 +380,15 @@ class PlotLayoutItems:
371
380
  self.plot_caption, ha, plot_caption_position
372
381
  )
373
382
 
383
+ if self.plot_footer:
384
+ ha = theme.getp(("plot_footer", "ha"), "left")
385
+ self.plot_footer.set_y(spaces.b.y1("plot_footer"))
386
+ justify.horizontally_about(
387
+ self.plot_footer, ha, plot_footer_position
388
+ )
389
+ self._resize_plot_footer_background(spaces)
390
+ self._resize_plot_footer_line(spaces)
391
+
374
392
  if self.axis_title_x:
375
393
  ha = theme.getp(("axis_title_x", "ha"), "center")
376
394
  self.axis_title_x.set_y(spaces.b.y1("axis_title_x"))
@@ -505,6 +523,31 @@ class PlotLayoutItems:
505
523
  for text, scale in zip(self.strip_text_y, relative_widths):
506
524
  text.patch.expand = scale
507
525
 
526
+ def _resize_plot_footer_background(self, spaces: PlotSideSpaces):
527
+ """
528
+ Resize the plot footer to the size of the footer
529
+ """
530
+ if not self.plot_footer_background:
531
+ return
532
+
533
+ self.plot_footer_background.set_x(spaces.l.offset)
534
+ self.plot_footer_background.set_y(spaces.b.offset)
535
+ self.plot_footer_background.set_height(spaces.b.footer_height)
536
+ self.plot_footer_background.set_width(spaces.plot_width)
537
+
538
+ def _resize_plot_footer_line(self, spaces: PlotSideSpaces):
539
+ """
540
+ Resize the footer line to be a border above the footer
541
+ """
542
+ if not self.plot_footer_line:
543
+ return
544
+
545
+ x1 = spaces.l.offset
546
+ x2 = x1 + spaces.plot_width
547
+ y1 = y2 = spaces.b.offset + spaces.b.footer_height
548
+ self.plot_footer_line.set_xdata([x1, x2])
549
+ self.plot_footer_line.set_ydata([y1, y2])
550
+
508
551
 
509
552
  def _text_is_visible(text: Text) -> bool:
510
553
  """
@@ -553,6 +553,9 @@ class bottom_space(_plot_side_space):
553
553
  Ordered from the edge of the figure and going inwards
554
554
  """
555
555
 
556
+ plot_footer_margin_bottom: float = 0
557
+ plot_footer: float = 0
558
+ plot_footer_margin_top: float = 0
556
559
  plot_margin: float = 0
557
560
  tag_alignment: float = 0
558
561
  plot_tag_margin_bottom: float = 0
@@ -602,6 +605,12 @@ class bottom_space(_plot_side_space):
602
605
  self.plot_caption = geometry.height(items.plot_caption)
603
606
  self.plot_caption_margin_top = m.t * F
604
607
 
608
+ if items.plot_footer:
609
+ m = theme.get_margin("plot_footer").fig
610
+ self.plot_footer_margin_bottom = m.b * F
611
+ self.plot_footer = geometry.height(items.plot_footer)
612
+ self.plot_footer_margin_top = m.t * F
613
+
605
614
  if items.legends and items.legends.bottom:
606
615
  self.legend = self.legend_height
607
616
  self.legend_box_spacing = theme.getp("legend_box_spacing") * F
@@ -680,6 +689,17 @@ class bottom_space(_plot_side_space):
680
689
  """
681
690
  return self.y1("legend")
682
691
 
692
+ @property
693
+ def footer_height(self):
694
+ """
695
+ The height of the footer including the margins
696
+ """
697
+ return (
698
+ self.plot_footer_margin_bottom
699
+ + self.plot_footer
700
+ + self.plot_footer_margin_top
701
+ )
702
+
683
703
  @property
684
704
  def tag_height(self):
685
705
  """
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  import pandas as pd
7
+ from packaging.version import Version
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from typing_extensions import Self
@@ -11,6 +12,8 @@ if TYPE_CHECKING:
11
12
  from plotnine import ggplot
12
13
  from plotnine.composition import Compose
13
14
 
15
+ PANDAS_LT_3 = Version(pd.__version__) < Version("3.0")
16
+
14
17
 
15
18
  def reopen(fig):
16
19
  """
@@ -55,12 +58,11 @@ class plot_context:
55
58
 
56
59
  # Contexts
57
60
  self.rc_context = mpl.rc_context(plot.theme.rcParams)
58
- # TODO: Remove this context when copy-on-write is permanent, i.e.
59
- # pandas >= 3.0
60
- self.pd_option_context = pd.option_context(
61
- "mode.copy_on_write",
62
- True,
63
- )
61
+ if PANDAS_LT_3:
62
+ self.pd_option_context = pd.option_context(
63
+ "mode.copy_on_write",
64
+ True,
65
+ )
64
66
 
65
67
  def __enter__(self) -> Self:
66
68
  """
@@ -68,7 +70,8 @@ class plot_context:
68
70
  """
69
71
 
70
72
  self.rc_context.__enter__()
71
- self.pd_option_context.__enter__()
73
+ if PANDAS_LT_3:
74
+ self.pd_option_context.__enter__()
72
75
 
73
76
  return self
74
77
 
@@ -89,7 +92,8 @@ class plot_context:
89
92
  plt.close(self.plot.figure)
90
93
 
91
94
  self.rc_context.__exit__(exc_type, exc_value, exc_traceback)
92
- self.pd_option_context.__exit__(exc_type, exc_value, exc_traceback)
95
+ if PANDAS_LT_3:
96
+ self.pd_option_context.__exit__(exc_type, exc_value, exc_traceback)
93
97
 
94
98
 
95
99
  @dataclass
@@ -123,6 +123,8 @@ class Compose:
123
123
  | ------------- |
124
124
  | |
125
125
  | caption |
126
+ |-------------------|
127
+ | footer |
126
128
  -------------------
127
129
  """
128
130
  _sidespaces: CompositionSideSpaces
@@ -573,13 +575,28 @@ class Compose:
573
575
  """
574
576
  Draw the background rectangle of the composition
575
577
  """
578
+ from matplotlib.lines import Line2D
576
579
  from matplotlib.patches import Rectangle
577
580
 
578
- rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=-1000)
581
+ zorder = -1000
582
+ rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=zorder)
579
583
  self.figure.add_artist(rect)
580
584
  self._gridspec.patch = rect
581
585
  self.theme.targets.plot_background = rect
582
586
 
587
+ if self.annotation.footer:
588
+ rect = Rectangle(
589
+ (0, 0), 0, 0, facecolor="none", linewidth=0, zorder=zorder + 1
590
+ )
591
+ self.figure.add_artist(rect)
592
+ self.theme.targets.plot_footer_background = rect
593
+
594
+ line = Line2D(
595
+ [0, 0], [0, 0], color="none", linewidth=0, zorder=zorder + 2
596
+ )
597
+ self.figure.add_artist(line)
598
+ self.theme.targets.plot_footer_line = line
599
+
583
600
  def _draw_annotation(self):
584
601
  """
585
602
  Draw the items in the annotation
@@ -602,6 +619,9 @@ class Compose:
602
619
  if caption := self.annotation.caption:
603
620
  targets.plot_caption = figure.text(0, 0, caption)
604
621
 
622
+ if footer := self.annotation.footer:
623
+ targets.plot_footer = figure.text(0, 0, footer)
624
+
605
625
  def save(
606
626
  self,
607
627
  filename: str | Path | BytesIO,
@@ -36,9 +36,14 @@ class plot_annotation(ComposeAddable):
36
36
  The caption of the composition
37
37
  """
38
38
 
39
+ footer: str | None = None
40
+ """
41
+ The footer of the composition
42
+ """
43
+
39
44
  theme: theme = field(default_factory=theme) # pyright: ignore[reportUnboundVariable]
40
45
  """
41
- Theme to use for the plot title, subtitle, caption, margin and background
46
+ Theme for the plot title, subtitle, caption, footer, margin and background
42
47
 
43
48
  It also controls the [](`~plotnine.themes.themeables.figure_size`) of the
44
49
  composition. The default theme is the same as the default one used for the
plotnine/ggplot.py CHANGED
@@ -127,6 +127,8 @@ class ggplot:
127
127
  | axis_text |
128
128
  | axis_title |
129
129
  | caption |
130
+ |-------------------------|
131
+ | footer |
130
132
  -------------------------
131
133
  """
132
134
 
@@ -553,6 +555,7 @@ class ggplot:
553
555
  subtitle = self.labels.get("subtitle", "")
554
556
  caption = self.labels.get("caption", "")
555
557
  tag = self.labels.get("tag", "")
558
+ footer = self.labels.get("footer", "")
556
559
 
557
560
  # Get the axis labels (default or specified by user)
558
561
  # and let the coordinate modify them e.g. flip
@@ -570,6 +573,9 @@ class ggplot:
570
573
  if caption:
571
574
  targets.plot_caption = figure.text(0, 0, caption)
572
575
 
576
+ if footer:
577
+ targets.plot_footer = figure.text(0, 0, footer)
578
+
573
579
  if tag:
574
580
  targets.plot_tag = figure.text(0, 0, tag)
575
581
 
@@ -587,13 +593,30 @@ class ggplot:
587
593
  wm.draw(self.figure)
588
594
 
589
595
  def _draw_plot_background(self):
596
+ from matplotlib.lines import Line2D
590
597
  from matplotlib.patches import Rectangle
591
598
 
592
- rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=-1000)
599
+ zorder = -1000
600
+ rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=zorder)
593
601
  self.figure.add_artist(rect)
594
602
  self._gridspec.patch = rect
595
603
  self.theme.targets.plot_background = rect
596
604
 
605
+ # Footer background and line only if there is a footer, and put
606
+ # it on top of the plot background
607
+ if self.labels.get("footer", ""):
608
+ rect = Rectangle(
609
+ (0, 0), 0, 0, facecolor="none", linewidth=0, zorder=zorder + 1
610
+ )
611
+ self.figure.add_artist(rect)
612
+ self.theme.targets.plot_footer_background = rect
613
+
614
+ line = Line2D(
615
+ [0, 0], [0, 0], color="none", linewidth=0, zorder=zorder + 2
616
+ )
617
+ self.figure.add_artist(line)
618
+ self.theme.targets.plot_footer_line = line
619
+
597
620
  def _save_filename(self, ext: str) -> Path:
598
621
  """
599
622
  Make a filename for use by the save method
@@ -272,9 +272,12 @@ class guide_legend(guide):
272
272
  # Drawings
273
273
  drawings: list[ColoredDrawingArea] = []
274
274
  for i in range(nbreak):
275
- da = ColoredDrawingArea(
276
- elements.key_widths[i], elements.key_heights[i], 0, 0
277
- )
275
+ try:
276
+ w, h = elements.key_widths[i], elements.key_heights[i]
277
+ except IndexError:
278
+ w, h = elements.empty_key_size
279
+
280
+ da = ColoredDrawingArea(w, h, 0, 0)
278
281
 
279
282
  # overlay geoms
280
283
  for params in self._layer_parameters:
@@ -482,3 +485,14 @@ class GuideElementsLegend(GuideElements):
482
485
  if self.is_horizontal:
483
486
  return [max(hs)] * len(hs)
484
487
  return hs
488
+
489
+ @cached_property
490
+ def empty_key_size(self) -> tuple[float, float]:
491
+ """
492
+ Size of an empty key
493
+ """
494
+ return (
495
+ (max(self.key_widths), min(self.key_heights))
496
+ if self.is_vertical
497
+ else (min(self.key_widths), max(self.key_heights))
498
+ )
plotnine/iapi.py CHANGED
@@ -66,7 +66,7 @@ class range_view:
66
66
  @dataclass
67
67
  class labels_view:
68
68
  """
69
- Scale labels (incl. caption & title) for the plot
69
+ Scale labels (incl. caption, title, footer) for the plot
70
70
  """
71
71
 
72
72
  x: Optional[str] = None
@@ -80,8 +80,9 @@ class labels_view:
80
80
  size: Optional[str] = None
81
81
  stroke: Optional[str] = None
82
82
  title: Optional[str] = None
83
- caption: Optional[str] = None
84
83
  subtitle: Optional[str] = None
84
+ caption: Optional[str] = None
85
+ footer: Optional[str] = None
85
86
  tag: Optional[str] = None
86
87
 
87
88
  def update(self, other: labels_view):
plotnine/labels.py CHANGED
@@ -90,6 +90,13 @@ class labs:
90
90
  The caption at the bottom of the plot.
91
91
  """
92
92
 
93
+ footer: str | None = None
94
+ """
95
+ The footer at the bottom of the plot
96
+
97
+ The footer when added is below the caption
98
+ """
99
+
93
100
  tag: str | None = None
94
101
  """
95
102
  A plot tag
@@ -7,6 +7,7 @@ if TYPE_CHECKING:
7
7
  from typing import Optional
8
8
 
9
9
  from matplotlib.collections import LineCollection
10
+ from matplotlib.lines import Line2D
10
11
  from matplotlib.patches import Rectangle
11
12
  from matplotlib.text import Text
12
13
 
@@ -37,9 +38,12 @@ class ThemeTargets:
37
38
  panel_border: list[Rectangle] = field(default_factory=list)
38
39
  plot_caption: Optional[Text] = None
39
40
  plot_subtitle: Optional[Text] = None
41
+ plot_footer: Optional[Text] = None
40
42
  plot_tag: Optional[Text] = None
41
43
  plot_title: Optional[Text] = None
42
44
  plot_background: Optional[Rectangle] = None
45
+ plot_footer_background: Optional[Rectangle] = None
46
+ plot_footer_line: Optional[Line2D] = None
43
47
  strip_background_x: list[StripTextPatch] = field(default_factory=list)
44
48
  strip_background_y: list[StripTextPatch] = field(default_factory=list)
45
49
  strip_text_x: list[StripText] = field(default_factory=list)
plotnine/themes/theme.py CHANGED
@@ -124,9 +124,11 @@ class theme:
124
124
  plot_title=None,
125
125
  plot_subtitle=None,
126
126
  plot_caption=None,
127
+ plot_footer=None,
127
128
  plot_tag=None,
128
129
  plot_title_position=None,
129
130
  plot_caption_position=None,
131
+ plot_footer_position=None,
130
132
  plot_tag_location=None,
131
133
  plot_tag_position=None,
132
134
  strip_text_x=None,
@@ -157,6 +159,7 @@ class theme:
157
159
  panel_grid_major=None,
158
160
  panel_grid_minor=None,
159
161
  panel_grid=None,
162
+ plot_footer_line=None,
160
163
  line=None,
161
164
  legend_key=None,
162
165
  legend_frame=None,
@@ -165,6 +168,7 @@ class theme:
165
168
  panel_background=None,
166
169
  panel_border=None,
167
170
  plot_background=None,
171
+ plot_footer_background=None,
168
172
  strip_background_x=None,
169
173
  strip_background_y=None,
170
174
  strip_background=None,
@@ -109,6 +109,15 @@ class theme_gray(theme):
109
109
  ma="left",
110
110
  margin=margin(t=m, unit="fig"),
111
111
  ),
112
+ plot_footer=element_text(
113
+ size=base_size * 0.8,
114
+ ha="left",
115
+ va="bottom",
116
+ ma="left",
117
+ margin=margin(t=1 / 3, b=1 / 3, unit="lines"),
118
+ ),
119
+ plot_footer_background=element_blank(),
120
+ plot_footer_line=element_blank(),
112
121
  plot_margin=m,
113
122
  plot_subtitle=element_text(
114
123
  va="top",
@@ -128,6 +137,7 @@ class theme_gray(theme):
128
137
  ),
129
138
  plot_title_position="panel",
130
139
  plot_caption_position="panel",
140
+ plot_footer_position="plot",
131
141
  plot_tag_location="margin",
132
142
  plot_tag_position="topleft",
133
143
  strip_align=0,
@@ -85,6 +85,12 @@ class theme_matplotlib(theme):
85
85
  ma="left",
86
86
  margin=margin(t=m, unit="fig"),
87
87
  ),
88
+ plot_footer=element_text(
89
+ ha="left",
90
+ va="bottom",
91
+ ma="left",
92
+ margin=margin(t=1 / 3, b=1 / 3, unit="lines"),
93
+ ),
88
94
  plot_margin=m,
89
95
  plot_subtitle=element_text(
90
96
  size=base_size * 0.9,
@@ -92,6 +98,8 @@ class theme_matplotlib(theme):
92
98
  ma="left",
93
99
  margin=margin(b=m, unit="fig"),
94
100
  ),
101
+ plot_footer_background=element_blank(),
102
+ plot_footer_line=element_blank(),
95
103
  plot_title=element_text(
96
104
  va="top",
97
105
  ma="left",
@@ -104,6 +112,7 @@ class theme_matplotlib(theme):
104
112
  ),
105
113
  plot_title_position="panel",
106
114
  plot_caption_position="panel",
115
+ plot_footer_position="plot",
107
116
  plot_tag_location="margin",
108
117
  plot_tag_position="topleft",
109
118
  strip_align=0,
@@ -100,6 +100,15 @@ class theme_seaborn(theme):
100
100
  ma="left",
101
101
  margin=margin(t=m, unit="fig"),
102
102
  ),
103
+ plot_footer=element_text(
104
+ size=base_size * 0.8,
105
+ ha="left",
106
+ va="bottom",
107
+ ma="left",
108
+ margin=margin(t=1 / 3, b=1 / 3, unit="lines"),
109
+ ),
110
+ plot_footer_background=element_blank(),
111
+ plot_footer_line=element_blank(),
103
112
  plot_margin=m,
104
113
  plot_subtitle=element_text(
105
114
  size=base_size * 1,
@@ -120,6 +129,7 @@ class theme_seaborn(theme):
120
129
  ),
121
130
  plot_title_position="panel",
122
131
  plot_caption_position="panel",
132
+ plot_footer_position="plot",
123
133
  plot_tag_location="margin",
124
134
  plot_tag_position="topleft",
125
135
  strip_align=0,
@@ -74,6 +74,13 @@ class theme_void(theme):
74
74
  ma="left",
75
75
  margin=margin(t=m, unit="fig"),
76
76
  ),
77
+ plot_footer=element_text(
78
+ size=base_size * 0.8,
79
+ ha="left",
80
+ va="bottom",
81
+ ma="left",
82
+ margin=margin(t=1 / 3, b=1 / 3, unit="lines"),
83
+ ),
77
84
  plot_margin=0,
78
85
  plot_subtitle=element_text(
79
86
  size=base_size * 1,
@@ -94,6 +101,7 @@ class theme_void(theme):
94
101
  ),
95
102
  plot_title_position="panel",
96
103
  plot_caption_position="panel",
104
+ plot_footer_position="plot",
97
105
  plot_tag_location="margin",
98
106
  plot_tag_position="topleft",
99
107
  strip_align=0,
@@ -768,6 +768,28 @@ class plot_caption(themeable):
768
768
  text.set_visible(False)
769
769
 
770
770
 
771
+ class plot_footer(themeable):
772
+ """
773
+ Plot footer
774
+
775
+ Parameters
776
+ ----------
777
+ theme_element : element_text
778
+ """
779
+
780
+ _omit = ["margin"]
781
+
782
+ def apply_figure(self, figure: Figure, targets: ThemeTargets):
783
+ super().apply_figure(figure, targets)
784
+ if text := targets.plot_footer:
785
+ text.set(**self.properties)
786
+
787
+ def blank_figure(self, figure: Figure, targets: ThemeTargets):
788
+ super().blank_figure(figure, targets)
789
+ if text := targets.plot_footer:
790
+ text.set_visible(False)
791
+
792
+
771
793
  class plot_tag(themeable):
772
794
  """
773
795
  Plot tag
@@ -834,6 +856,19 @@ class plot_caption_position(themeable):
834
856
  """
835
857
 
836
858
 
859
+ class plot_footer_position(themeable):
860
+ """
861
+ How to align the plot footer
862
+
863
+ Parameters
864
+ ----------
865
+ theme_element : Literal["panel", "plot"], default = "plot"
866
+ If "panel", the footer is aligned with respect to the
867
+ panels. If "plot", it is aligned with the plot, excluding
868
+ the margin space.
869
+ """
870
+
871
+
837
872
  class plot_tag_location(themeable):
838
873
  """
839
874
  The area where the tag will be positioned
@@ -919,7 +954,13 @@ class strip_text(strip_text_x, strip_text_y):
919
954
 
920
955
 
921
956
  class title(
922
- axis_title, legend_title, plot_title, plot_subtitle, plot_caption, plot_tag
957
+ axis_title,
958
+ legend_title,
959
+ plot_title,
960
+ plot_subtitle,
961
+ plot_caption,
962
+ plot_footer,
963
+ plot_tag,
923
964
  ):
924
965
  """
925
966
  All titles on the plot
@@ -1476,7 +1517,27 @@ class panel_grid(panel_grid_major, panel_grid_minor):
1476
1517
  """
1477
1518
 
1478
1519
 
1479
- class line(axis_line, axis_ticks, panel_grid, legend_ticks):
1520
+ class plot_footer_line(themeable):
1521
+ """
1522
+ Line above the footer
1523
+
1524
+ Parameters
1525
+ ----------
1526
+ theme_element : element_line
1527
+ """
1528
+
1529
+ def apply_figure(self, figure: Figure, targets: ThemeTargets):
1530
+ super().apply_figure(figure, targets)
1531
+ if targets.plot_footer_line:
1532
+ targets.plot_footer_line.set(**self.properties)
1533
+
1534
+ def blank_figure(self, figure: Figure, targets: ThemeTargets):
1535
+ super().blank_figure(figure, targets)
1536
+ if targets.plot_footer_line:
1537
+ targets.plot_footer_line.set_visible(False)
1538
+
1539
+
1540
+ class line(axis_line, axis_ticks, panel_grid, legend_ticks, plot_footer_line):
1480
1541
  """
1481
1542
  All line elements
1482
1543
 
@@ -1691,6 +1752,33 @@ class plot_background(themeable):
1691
1752
  targets.plot_background.set_visible(False)
1692
1753
 
1693
1754
 
1755
+ class plot_footer_background(themeable):
1756
+ """
1757
+ Footer background
1758
+
1759
+ The background is placed across the entire with of the plot,
1760
+ or the composition. And the height is determined by the height
1761
+ of the footer including the top and bottom margin.
1762
+
1763
+ Parameters
1764
+ ----------
1765
+ theme_element : element_rect
1766
+ """
1767
+
1768
+ def apply_figure(self, figure: Figure, targets: ThemeTargets):
1769
+ super().apply_figure(figure, targets)
1770
+ if targets.plot_footer_background:
1771
+ props = self.properties
1772
+ props["linewidth"] = 0
1773
+ props["edgecolor"] = "none"
1774
+ targets.plot_footer_background.set(**props)
1775
+
1776
+ def blank_figure(self, figure: Figure, targets: ThemeTargets):
1777
+ super().blank_figure(figure, targets)
1778
+ if targets.plot_footer_background:
1779
+ targets.plot_footer_background.set_visible(False)
1780
+
1781
+
1694
1782
  class strip_background_x(MixinSequenceOfValues):
1695
1783
  """
1696
1784
  Horizontal facet label background
@@ -1747,6 +1835,7 @@ class rect(
1747
1835
  panel_background,
1748
1836
  panel_border,
1749
1837
  plot_background,
1838
+ plot_footer_background,
1750
1839
  strip_background,
1751
1840
  ):
1752
1841
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotnine
3
- Version: 0.16.0a2
3
+ Version: 0.16.0a3
4
4
  Summary: A Grammar of Graphics for Python
5
5
  Author-email: Hassan Kibirige <has2k1@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -49,7 +49,7 @@ Requires-Dist: pandas>=2.2.0
49
49
  Requires-Dist: mizani~=0.14.0
50
50
  Requires-Dist: numpy>=1.23.5
51
51
  Requires-Dist: scipy>=1.8.0
52
- Requires-Dist: statsmodels>=0.14.5
52
+ Requires-Dist: statsmodels>=0.14.6
53
53
  Provides-Extra: all
54
54
  Requires-Dist: plotnine[extra]; extra == "all"
55
55
  Requires-Dist: plotnine[doc]; extra == "all"
@@ -2,10 +2,10 @@ plotnine/__init__.py,sha256=_Q18ZKrippC4L27KdYUhuJon95AE8JPZXEYeXZyutms,10372
2
2
  plotnine/animation.py,sha256=5UK3Y5IHHRtf0sRU5x288XCldn1OhK1NGdMQoTi_Tlg,7683
3
3
  plotnine/doctools.py,sha256=JBF55q1MX2fXYQcGDpVrGPdlKf5OiQ5gyTdWhnM_IzU,14558
4
4
  plotnine/exceptions.py,sha256=SgTxBHkV65HjGI3aFy2q1_lHP9HAdiuxVLN3U-PJWSQ,1616
5
- plotnine/ggplot.py,sha256=wUoSUcYcseRv6pq9kqEh23L3vvfNOTPcb_c37Xxy_CI,25771
5
+ plotnine/ggplot.py,sha256=nPa16xmivwCHsNLWer2kJ418sp0RIArcpzAOaziUwhI,26628
6
6
  plotnine/helpers.py,sha256=4R3KZmtGH46-kRNSGOA0JxZaLKBo0ge8Vnx1cDQ8_gI,966
7
- plotnine/iapi.py,sha256=nA49ur2AksGUnu8P5DGBVzPOk9YHYaNLSWzZetV2qoc,9429
8
- plotnine/labels.py,sha256=3pOXth2Xma_qCqB_xXAGIkPQ9gcaUaaFEAsa5if1iR0,2830
7
+ plotnine/iapi.py,sha256=YGOM4U2z3RB87o2S3p4M3VJkfFHcTHEko_yJ1EyMMI0,9469
8
+ plotnine/labels.py,sha256=5uZdYLXutzife_a7xJIqJ31L_bNCThiIbIqh84PLwVU,2966
9
9
  plotnine/layer.py,sha256=l9ehqvWoDpaZdVFLGoiA5USL79NYWWaiZM_ZuDeGzpo,17294
10
10
  plotnine/options.py,sha256=j3zXv4wc3J4nOI_TqJ5s_abuifodt_UN8MR8M4i8UVA,3108
11
11
  plotnine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -21,16 +21,16 @@ plotnine/_mpl/ticker.py,sha256=RY_7AdTggc7QBq9_t0KBJXg36oxKfB-Vtc9FzLnaGnQ,393
21
21
  plotnine/_mpl/transforms.py,sha256=DNaOlNq76xlT696sN8ot1bmYyp4mmrjXQHk3kTi4HIg,76
22
22
  plotnine/_mpl/utils.py,sha256=CHRhcOnB7O3Fj953COiwBi6ccF2TQG8puvUVLWO3e_k,11051
23
23
  plotnine/_mpl/layout_manager/__init__.py,sha256=w3OZfdMJAFIDrQYVoYG6pvRPqwTNoQ4KJ0zXdJq8L7U,79
24
- plotnine/_mpl/layout_manager/_composition_layout_items.py,sha256=6Byw7I15Ax-htkb22ekbSQYG-DoGSxOKWoErWd-druo,3018
25
- plotnine/_mpl/layout_manager/_composition_side_space.py,sha256=PH9YlkxUp5MGAEFST-lhrgCPTg05jszH32QnPzrj1es,13552
24
+ plotnine/_mpl/layout_manager/_composition_layout_items.py,sha256=1GQwH1ZvzbenRyi3zmDnLkLck5ndC8UV3u0WdMp0qpw,4705
25
+ plotnine/_mpl/layout_manager/_composition_side_space.py,sha256=VZ56K0agpcQ0XSMgMyCluFkPI1NKS0YmzYD0-LF5H9Q,14538
26
26
  plotnine/_mpl/layout_manager/_engine.py,sha256=Fn5Xsk2oPywlG5qj6PTqjgTxxR9tN8npGw7XEaZyXL4,1511
27
27
  plotnine/_mpl/layout_manager/_grid.py,sha256=dLzLv5BFVErVoI0DsDeca3zn4JMMcg5tv-BajSV9Ag0,2382
28
28
  plotnine/_mpl/layout_manager/_layout_tree.py,sha256=m9QVRa3N6pBVMaoZPTzhXzh_RUVdF54wUgKoxlMpfgw,20794
29
- plotnine/_mpl/layout_manager/_plot_layout_items.py,sha256=EHzThpxg-XAK1DD2sGVXGcbAfAR1bW4bF8NhXhKou8M,23867
30
- plotnine/_mpl/layout_manager/_plot_side_space.py,sha256=bUoDHXUrw_1v54oGuq8JLlCoBDSj01DBsDEUkJoGDGQ,33026
29
+ plotnine/_mpl/layout_manager/_plot_layout_items.py,sha256=XyRDmqEbZSCKeowfx2_OVfZNnhNDR5RS6uoGTHIAyEM,25540
30
+ plotnine/_mpl/layout_manager/_plot_side_space.py,sha256=NqhYZeQBXoqQNpSag26nEh-dnyr8uyhiZF53quAWg0Q,33650
31
31
  plotnine/_mpl/layout_manager/_side_space.py,sha256=RjsVctYLzDJyoGpPVEEyGEfJSfEetVhRRiBVz-eQXkI,5303
32
32
  plotnine/_utils/__init__.py,sha256=wefs_rvVdrWwhyWtDLarI6Bm7CDNUSSBWYlvbdWDzLQ,31528
33
- plotnine/_utils/context.py,sha256=e2W9PM5OyGuyAEOxyu5YE0HXmlsNgbMpdjqdOABQA3s,3896
33
+ plotnine/_utils/context.py,sha256=VrM2SP91xSwYZAflJYgp4WaglxFBn222NyrMM8ux-sk,3988
34
34
  plotnine/_utils/dataclasses.py,sha256=ahEoC7WeMsRB4_M9YBAuMpR-xm6LkpryAueLwJMXkNo,609
35
35
  plotnine/_utils/dev.py,sha256=0qgRbMhcd4dfuLuYxx0skocKAtfwHF02ntyILRBogbg,1629
36
36
  plotnine/_utils/ipython.py,sha256=mGD83wi3q0Sk2XfAbr9zyBxYNXVwXv-lmgnA3QXH0bI,1768
@@ -39,8 +39,8 @@ plotnine/_utils/registry.py,sha256=HoiB2NnbEHufXjYnVJyrJKflk2RwKtTYk2L3n7tH4XA,3
39
39
  plotnine/_utils/yippie.py,sha256=DbmxvVrd34P24CCmOZrAulyGQ35rXNaSr8BuutS2Uio,2392
40
40
  plotnine/composition/__init__.py,sha256=OoJG-K08x_c7abAGRfdWIBnGcBfRlC_WekjMd2ktQ6g,360
41
41
  plotnine/composition/_beside.py,sha256=TxrmQCFnmMnqoMawDhTLj5l-J4_VWL1XRDnjoQChFBQ,1436
42
- plotnine/composition/_compose.py,sha256=Tu7hwFX97VLNNg2i_ZnQFHtpvkPhYNWGxgkTdYjPotw,17763
43
- plotnine/composition/_plot_annotation.py,sha256=zulwsFJ-dlEEoZIsYh14b02eSlvp4MsW_PgqY6guYp0,2034
42
+ plotnine/composition/_compose.py,sha256=4wPU3P-2WNBGnLFhPLaQZSyy_vTujovJhpe3cBOtpiw,18465
43
+ plotnine/composition/_plot_annotation.py,sha256=RMA_vdcHFT9KVUbhwS-NZdPTbqb-XgAYRflWs8J3bVE,2116
44
44
  plotnine/composition/_plot_layout.py,sha256=JPYPtRutHtILOLU1xf6x0c1G_263Q_62GtN0kI0LqxE,3732
45
45
  plotnine/composition/_plot_spacer.py,sha256=RSzH4yRdcK0iairZZ88CE8onpr6NzNaDgtNnSW6ePzY,1859
46
46
  plotnine/composition/_stack.py,sha256=djjFrqO3-lOvKMYw87OSrF4BbHU9deFrk0l1wpUYSS8,1454
@@ -131,7 +131,7 @@ plotnine/guides/__init__.py,sha256=ulI-mDhtq3jAQEAqPv8clrn3UHGFOu3xRuO7jXlq-LY,2
131
131
  plotnine/guides/guide.py,sha256=09Zojde0stGEZbVNyx6m5As2PCaLRxzQvYNid-Y0YBU,8596
132
132
  plotnine/guides/guide_axis.py,sha256=zG_5Ot1kTuHOeuQspL5V1A1-7c7X8cNeMDoF01Ghh2w,296
133
133
  plotnine/guides/guide_colorbar.py,sha256=1RbF2WjqrouoELBdZNpy-4olsnJoSTOX--RYgbCx-Gw,18322
134
- plotnine/guides/guide_legend.py,sha256=gJ8AKr_YRL66Xxp4_nyWzhnQk5a1Bmns5PJhLZDwVBU,15084
134
+ plotnine/guides/guide_legend.py,sha256=EshNFzbYUiR31J0ZFLGrJmdZk9AwRawIcY5INpr8vcw,15486
135
135
  plotnine/guides/guides.py,sha256=SzdmK-XX5cR6YgysjNjdoynCATThX1pDDX2a6ghvseU,15485
136
136
  plotnine/mapping/__init__.py,sha256=DLu9E0kwwuHxzTUenoVjCNTTdkWMwIDtkExLleBq1MI,205
137
137
  plotnine/mapping/_atomic.py,sha256=TbobHVJlHRoSHibi6OOWMVM2J1r_kKQJMS6G5zvEhrg,4029
@@ -199,22 +199,22 @@ plotnine/stats/stat_unique.py,sha256=1SdLNm2CjhjONAwo714uB3tc5KX7vSDHAi1geJdWtDY
199
199
  plotnine/stats/stat_ydensity.py,sha256=_OVYc-QELPKMc24BvQaA9cPkfEoC_hW2teeo8yL8iao,5661
200
200
  plotnine/themes/__init__.py,sha256=tEKIF4gYmOF2Z1-_UDdK8zh41dLl-61HUGIhOQvki6I,916
201
201
  plotnine/themes/seaborn_rcmod.py,sha256=Pi-UX5LyH9teSuteYpqPOEnfLgKUz01LnKDyDA7Aois,15502
202
- plotnine/themes/targets.py,sha256=MjBRWWRgsLXXM_PJffPsV4DttQJB_m11jdD837BteuU,1686
203
- plotnine/themes/theme.py,sha256=UAgV-xx0eSQzIO0_fmZcupXmc7vCS3mQgxY9eyxjegA,16089
202
+ plotnine/themes/targets.py,sha256=yEwkaDhL1UoOBdlx9SWCQsSRg9oOjnFZgtJrtY_LyFA,1866
203
+ plotnine/themes/theme.py,sha256=zucHnZ4pJC_iLxBEy-ZNRLa6sRuKEeMq6SHW-sywoiE,16218
204
204
  plotnine/themes/theme_538.py,sha256=hr0FaGAffZYU3m8o90WuUWntaPgI2O_eGiaBz259MiM,1088
205
205
  plotnine/themes/theme_bw.py,sha256=XXUt9KXEIkROiU0ZVIZicypneb-zL5K1cQFPqYd5WAI,1010
206
206
  plotnine/themes/theme_classic.py,sha256=B6QkU6blGnEY5iaiPtu4VsvFzC0peWSAhlKiC2SJSkM,923
207
207
  plotnine/themes/theme_dark.py,sha256=tF6BJ2A4jE6C-Mhl95DQiAbu-mK0Hr1MaymhbJzaD2Q,1255
208
- plotnine/themes/theme_gray.py,sha256=VRPCWvorGLnqNRVgtvACYDPEvHMQigtp-qMrgOcyar8,5265
208
+ plotnine/themes/theme_gray.py,sha256=yKbjIowSSFiP5lBDk30dBSuUwtRyv8L8b5XZi6TEfWQ,5641
209
209
  plotnine/themes/theme_light.py,sha256=1vDa6QlLz2vJaa1gYfyttVuXxde2MwDnMnJAqToJMGs,1415
210
210
  plotnine/themes/theme_linedraw.py,sha256=woMr18xoEJmsD8ZCiUpdyn-CE-P1AGFHRNapwCGWUjg,1350
211
- plotnine/themes/theme_matplotlib.py,sha256=3JPOGhFz6Nul2s03Djd254RPtHtcQ0fsi3Hc9OIi0kc,4248
211
+ plotnine/themes/theme_matplotlib.py,sha256=8isOcIJlq8c3RbZnPLj6aUIAMHbTzLSF7gFHGdiO0Wk,4586
212
212
  plotnine/themes/theme_minimal.py,sha256=YhkKNA48jP2OLa8-kHMfXFEFRjcsfWBLMID2orITrmo,884
213
- plotnine/themes/theme_seaborn.py,sha256=8giJh8QvkMJ8pr_XXHPDMfkysoI2_yr0vJhz48_sQGI,4319
213
+ plotnine/themes/theme_seaborn.py,sha256=iJM3HR0BlElhIj0PvqR1bGyJGaWPKQnz6K7AQGkGmps,4695
214
214
  plotnine/themes/theme_tufte.py,sha256=qUOrZhQyfJgc0fmy8Es7tT7aYqUSzCjvkP7-dBilwHE,1926
215
- plotnine/themes/theme_void.py,sha256=itsmb9pWCZAwgccw_zHDjQRfhRRbAiMNo4zZ1zeBgIU,3223
215
+ plotnine/themes/theme_void.py,sha256=e7OgZPR0pmAewwKxKYtRR2X6zGSySGjdXKPHikZK6S0,3501
216
216
  plotnine/themes/theme_xkcd.py,sha256=5MOdj_H23Kr_jbDnepmq1DQDbn8-TfUmtzYBZTb4RB4,2207
217
- plotnine/themes/themeable.py,sha256=nH6bluQ87wpVpQDB5qzE5ZfoxgaNEML6NK0FcTFMdPI,72387
217
+ plotnine/themes/themeable.py,sha256=mCqZzo7gm0J1AqpSfeUOsfKijwuI8o1dOcU33he6kT4,74782
218
218
  plotnine/themes/elements/__init__.py,sha256=xV1la_mTv1BQ3zQp7ZGVGPdV6KohvEVhKAi1uPTeAs0,327
219
219
  plotnine/themes/elements/element_base.py,sha256=D7cfEglzsSuhW91KpZVAZ2MAHWZp64r9Aajoh8uMGZ4,832
220
220
  plotnine/themes/elements/element_blank.py,sha256=4r7-6HeR1494oWNIGQh0ASrFQ4SLvYa6aQHA85eH-Ds,187
@@ -222,8 +222,8 @@ plotnine/themes/elements/element_line.py,sha256=zJ6KL2_LcQ0aQjT4fzNA1f9c1QZ3xEeT
222
222
  plotnine/themes/elements/element_rect.py,sha256=w5cLH-Sr4cTRXVdkRiu8kBqFt3TXHhIb1MUITfi89gE,1767
223
223
  plotnine/themes/elements/element_text.py,sha256=NrHlGKB0cO0v_C7hkarTMPb2QVsTOoxVuCMS6hmv62U,6552
224
224
  plotnine/themes/elements/margin.py,sha256=jMHe-UKHHer_VYwAVDC-Tz2-AP_4YDuXPTWAuacoqgU,4080
225
- plotnine-0.16.0a2.dist-info/licenses/LICENSE,sha256=GY4tQiUd17Tq3wWR42Zs9MRTFOTf6ahIXhZTcwAdOeU,1082
226
- plotnine-0.16.0a2.dist-info/METADATA,sha256=xj-INX2wadP6ZX7rAP0LBZBbLWwoZMMp3At6ZeusAY8,9498
227
- plotnine-0.16.0a2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
228
- plotnine-0.16.0a2.dist-info/top_level.txt,sha256=t340Mbko1ZbmvYPkQ81dIiPHcaQdTUszYz-bWUpr8ys,9
229
- plotnine-0.16.0a2.dist-info/RECORD,,
225
+ plotnine-0.16.0a3.dist-info/licenses/LICENSE,sha256=GY4tQiUd17Tq3wWR42Zs9MRTFOTf6ahIXhZTcwAdOeU,1082
226
+ plotnine-0.16.0a3.dist-info/METADATA,sha256=O9d0UTthOHQ1pUScUwJSfK9v02LSQlJASwALXCa3vNs,9498
227
+ plotnine-0.16.0a3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
228
+ plotnine-0.16.0a3.dist-info/top_level.txt,sha256=t340Mbko1ZbmvYPkQ81dIiPHcaQdTUszYz-bWUpr8ys,9
229
+ plotnine-0.16.0a3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5