plotnine 0.14.5__py3-none-any.whl → 0.15.0a2__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 (92) hide show
  1. plotnine/__init__.py +31 -37
  2. plotnine/_mpl/gridspec.py +265 -0
  3. plotnine/_mpl/layout_manager/__init__.py +6 -0
  4. plotnine/_mpl/layout_manager/_engine.py +87 -0
  5. plotnine/_mpl/layout_manager/_layout_items.py +957 -0
  6. plotnine/_mpl/layout_manager/_layout_tree.py +905 -0
  7. plotnine/_mpl/layout_manager/_spaces.py +1154 -0
  8. plotnine/_mpl/patches.py +70 -34
  9. plotnine/_mpl/text.py +159 -37
  10. plotnine/_mpl/utils.py +78 -10
  11. plotnine/_utils/__init__.py +35 -9
  12. plotnine/_utils/dev.py +45 -27
  13. plotnine/_utils/yippie.py +115 -0
  14. plotnine/animation.py +1 -1
  15. plotnine/coords/coord.py +3 -3
  16. plotnine/coords/coord_trans.py +1 -1
  17. plotnine/data/__init__.py +43 -8
  18. plotnine/data/anscombe-quartet.csv +45 -0
  19. plotnine/doctools.py +2 -2
  20. plotnine/facets/facet.py +34 -43
  21. plotnine/facets/facet_grid.py +14 -6
  22. plotnine/facets/facet_wrap.py +3 -5
  23. plotnine/facets/strips.py +20 -33
  24. plotnine/geoms/annotate.py +3 -3
  25. plotnine/geoms/annotation_logticks.py +2 -0
  26. plotnine/geoms/annotation_stripes.py +2 -0
  27. plotnine/geoms/geom.py +3 -3
  28. plotnine/geoms/geom_bar.py +10 -2
  29. plotnine/geoms/geom_col.py +6 -0
  30. plotnine/geoms/geom_crossbar.py +2 -3
  31. plotnine/geoms/geom_path.py +2 -2
  32. plotnine/geoms/geom_violin.py +24 -7
  33. plotnine/ggplot.py +95 -66
  34. plotnine/guides/guide.py +19 -20
  35. plotnine/guides/guide_colorbar.py +6 -6
  36. plotnine/guides/guide_legend.py +15 -16
  37. plotnine/guides/guides.py +8 -8
  38. plotnine/helpers.py +49 -0
  39. plotnine/iapi.py +33 -7
  40. plotnine/labels.py +8 -3
  41. plotnine/layer.py +4 -4
  42. plotnine/mapping/_env.py +2 -2
  43. plotnine/mapping/_eval_environment.py +85 -0
  44. plotnine/mapping/aes.py +14 -30
  45. plotnine/mapping/evaluation.py +7 -65
  46. plotnine/options.py +14 -7
  47. plotnine/plot_composition/__init__.py +10 -0
  48. plotnine/plot_composition/_compose.py +462 -0
  49. plotnine/plot_composition/_plotspec.py +50 -0
  50. plotnine/plot_composition/_spacer.py +32 -0
  51. plotnine/positions/position_dodge.py +1 -1
  52. plotnine/positions/position_dodge2.py +1 -1
  53. plotnine/positions/position_stack.py +1 -2
  54. plotnine/qplot.py +1 -2
  55. plotnine/scales/__init__.py +0 -6
  56. plotnine/scales/limits.py +7 -7
  57. plotnine/scales/scale.py +4 -4
  58. plotnine/scales/scale_continuous.py +2 -1
  59. plotnine/scales/scale_identity.py +10 -2
  60. plotnine/scales/scale_manual.py +6 -2
  61. plotnine/stats/binning.py +5 -2
  62. plotnine/stats/smoothers.py +3 -5
  63. plotnine/stats/stat.py +3 -3
  64. plotnine/stats/stat_bindot.py +1 -3
  65. plotnine/stats/stat_density.py +2 -2
  66. plotnine/stats/stat_qq_line.py +1 -1
  67. plotnine/stats/stat_sina.py +34 -1
  68. plotnine/themes/elements/__init__.py +3 -0
  69. plotnine/themes/elements/element_text.py +35 -24
  70. plotnine/themes/elements/margin.py +137 -61
  71. plotnine/themes/targets.py +3 -1
  72. plotnine/themes/theme.py +21 -7
  73. plotnine/themes/theme_538.py +0 -1
  74. plotnine/themes/theme_bw.py +0 -1
  75. plotnine/themes/theme_dark.py +0 -1
  76. plotnine/themes/theme_gray.py +32 -34
  77. plotnine/themes/theme_light.py +1 -1
  78. plotnine/themes/theme_matplotlib.py +28 -31
  79. plotnine/themes/theme_seaborn.py +36 -36
  80. plotnine/themes/theme_void.py +25 -27
  81. plotnine/themes/theme_xkcd.py +0 -1
  82. plotnine/themes/themeable.py +369 -169
  83. plotnine/typing.py +3 -3
  84. plotnine/watermark.py +3 -3
  85. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/METADATA +8 -5
  86. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/RECORD +89 -78
  87. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/WHEEL +1 -1
  88. plotnine/_mpl/_plot_side_space.py +0 -888
  89. plotnine/_mpl/_plotnine_tight_layout.py +0 -293
  90. plotnine/_mpl/layout_engine.py +0 -110
  91. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info/licenses}/LICENSE +0 -0
  92. {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/top_level.txt +0 -0
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._x, text._y))
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):
plotnine/_mpl/text.py CHANGED
@@ -1,13 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- import typing
3
+ from functools import lru_cache
4
+ from typing import TYPE_CHECKING
4
5
 
6
+ from matplotlib import artist
5
7
  from matplotlib.text import Text
6
8
 
9
+ from plotnine._utils import ha_as_float, va_as_float
10
+
7
11
  from .patches import StripTextPatch
8
- from .utils import bbox_in_axes_space
12
+ from .utils import bbox_in_axes_space, rel_position
9
13
 
10
- if typing.TYPE_CHECKING:
14
+ if TYPE_CHECKING:
11
15
  from matplotlib.backend_bases import RendererBase
12
16
 
13
17
  from plotnine.iapi import strip_draw_info
@@ -23,56 +27,174 @@ class StripText(Text):
23
27
 
24
28
  def __init__(self, info: strip_draw_info):
25
29
  kwargs = {
26
- "ha": info.ha,
27
- "va": info.va,
28
30
  "rotation": info.rotation,
29
31
  "transform": info.ax.transAxes,
30
32
  "clip_on": False,
31
33
  "zorder": 3.3,
34
+ # Since the text can be rotated, it is simpler to anchor it at
35
+ # the center, align it then do the rotation. Vertically,
36
+ # center_baseline places the text in the visual center, but
37
+ # only if it is one line. For multiline text, we are better
38
+ # off with plain center.
39
+ "ha": "center",
40
+ "va": "center_baseline" if info.is_oneline else "center",
41
+ "rotation_mode": "anchor",
32
42
  }
33
43
 
34
- super().__init__(
35
- info.x,
36
- info.y,
37
- info.label,
38
- **kwargs,
39
- )
44
+ super().__init__(0, 0, info.label, **kwargs)
40
45
  self.draw_info = info
41
46
  self.patch = StripTextPatch(self)
42
47
 
43
- def draw(self, renderer: RendererBase):
44
- if not self.get_visible():
45
- return
48
+ # self.set_horizontalalignment("center")
49
+ # self.set_verticalalignment(
50
+ # "center_baseline" if info.is_oneline else "center"
51
+ # )
52
+ # self.set_rotation_mode("anchor")
53
+
54
+ # TODO: This should really be part of the unit conversions in the
55
+ # margin class.
56
+ @lru_cache(2)
57
+ def _line_height(self, renderer) -> float:
58
+ """
59
+ The line height in display space of the text on the canvas
60
+ """
61
+ # Text string, (width, height), x, y
62
+ parts: list[tuple[str, tuple[float, float], float, float]]
63
+
64
+ try:
65
+ # matplotlib.Text._get_layout is a private API and we cannot
66
+ # tell how using it may fail in the future.
67
+ _, parts, _ = self._get_layout(renderer) # pyright: ignore[reportAttributeAccessIssue]
68
+ except Exception:
69
+ from warnings import warn
70
+
71
+ from plotnine.exceptions import PlotnineWarning
72
+
73
+ # The canvas height is nearly always bigger than the stated
74
+ # fontsize. 1.36 is a good multiplication factor obtained by
75
+ # some rough exploration
76
+ f = 1.36
77
+ size = self.get_fontsize()
78
+ height = round(size * f) if isinstance(size, int) else 14
79
+ warn(
80
+ f"Could not calculate line height for {self.get_text()}. "
81
+ "Using an estimate, please let us know about this at "
82
+ "https://github.com/has2k1/plotnine/issues",
83
+ PlotnineWarning,
84
+ )
85
+ else:
86
+ # The the text has multiple lines, we use the maximum height
87
+ # of anyone single line.
88
+ height = max([p[1][1] for p in parts])
89
+
90
+ return height
91
+
92
+ def _set_position(self, renderer):
93
+ """
94
+ Set the postion of the text within the strip_background
95
+ """
96
+ # We have to two premises that depend on each other:
97
+ #
98
+ # 1. The breadth of the strip_background grows to accomodate
99
+ # the strip_text.
100
+ # 2. The strip_text is justified within the strip_background.
101
+ #
102
+ # From these we note that the strip_background does not need the
103
+ # position of the strip_text, but it needs its size. Therefore
104
+ # we implement StripTextPatch.get_window_extent can use
105
+ # StripText.get_window_extent, peeking only at the size.
106
+ #
107
+ # And we implement StripText._set_position_* to use
108
+ # StripTextPatch.get_window_extent and make the calculations in
109
+ # both methods independent.
110
+ if self.draw_info.position == "top":
111
+ self._set_position_top(renderer)
112
+ else: # "right"
113
+ self._set_position_right(renderer)
114
+
115
+ def _set_position_top(self, renderer):
116
+ """
117
+ Set position of the text within the top strip_background
118
+ """
119
+ info = self.draw_info
120
+ ha, va, ax, m = info.ha, info.va, info.ax, info.margin
121
+
122
+ rel_x, rel_y = ha_as_float(ha), va_as_float(va)
123
+ patch_bbox = bbox_in_axes_space(self.patch, ax, renderer)
124
+ text_bbox = bbox_in_axes_space(self, ax, renderer)
125
+
126
+ # line_height and margins in axes space
127
+ line_height = self._line_height(renderer) / ax.bbox.height
128
+
129
+ x = (
130
+ # Justify horizontally within the strip_background
131
+ rel_position(
132
+ rel_x,
133
+ text_bbox.width + (line_height * (m.l + m.r)),
134
+ patch_bbox.x0,
135
+ patch_bbox.x1,
136
+ )
137
+ + (m.l * line_height)
138
+ + text_bbox.width / 2
139
+ )
140
+ # Setting the y position based on the bounding box is wrong
141
+ y = (
142
+ rel_position(
143
+ rel_y,
144
+ text_bbox.height,
145
+ patch_bbox.y0 + m.b * line_height,
146
+ patch_bbox.y1 - m.t * line_height,
147
+ )
148
+ + text_bbox.height / 2
149
+ )
150
+ self.set_position((x, y))
46
151
 
152
+ def _set_position_right(self, renderer):
153
+ """
154
+ Set position of the text within the bottom strip_background
155
+ """
47
156
  info = self.draw_info
48
- # "fill up" spatch to contain the text
49
- self.patch.update_position_size(renderer)
157
+ ha, va, ax, m = info.ha, info.va, info.ax, info.margin
50
158
 
51
- # Get bbox of spatch in transAxes space
52
- patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
159
+ # bboxes in axes space
160
+ patch_bbox = bbox_in_axes_space(self.patch, ax, renderer)
161
+ text_bbox = bbox_in_axes_space(self, ax, renderer)
53
162
 
54
- # Align patch across the edge of the panel
55
- if info.position == "top":
56
- l, b, w, h = info.x, info.y, info.box_width, patch_bbox.height
57
- b = b + patch_bbox.height * info.strip_align
58
- else: # "right"
59
- l, b, w, h = info.x, info.y, patch_bbox.width, info.box_height
60
- l = l + patch_bbox.width * info.strip_align
163
+ # line_height in axes space
164
+ line_height = self._line_height(renderer) / ax.bbox.width
61
165
 
62
- self.patch.set_bounds(l, b, w, h)
63
- self.patch.set_transform(info.ax.transAxes)
64
- self.patch.set_mutation_scale(0)
166
+ rel_x, rel_y = ha_as_float(ha), va_as_float(va)
65
167
 
66
- # Put text in center of patch
67
- self._x = l + w / 2
68
- self._y = b + h / 2
168
+ x = (
169
+ rel_position(
170
+ rel_x,
171
+ text_bbox.width,
172
+ patch_bbox.x0 + m.l * line_height,
173
+ patch_bbox.x1 - m.r * line_height,
174
+ )
175
+ + text_bbox.width / 2
176
+ )
177
+ y = (
178
+ # Justify vertically within the strip_background
179
+ rel_position(
180
+ rel_y,
181
+ text_bbox.height + ((m.b + m.t) * line_height),
182
+ patch_bbox.y0,
183
+ patch_bbox.y1,
184
+ )
185
+ + (m.b * line_height)
186
+ + text_bbox.height / 2
187
+ )
188
+ self.set_position((x, y))
69
189
 
70
- # "anchor" aligns before rotation so the right-strip get properly
71
- # centered text
72
- self.set_rotation_mode("anchor")
73
- self.set_horizontalalignment("center") # right-strip
74
- self.set_verticalalignment("center_baseline") # top-strip
190
+ @artist.allow_rasterization
191
+ def draw(self, renderer: RendererBase):
192
+ """
193
+ Draw text along with the patch
194
+ """
195
+ if not self.get_visible():
196
+ return
75
197
 
76
- # Draw spatch
198
+ self._set_position(renderer)
77
199
  self.patch.draw(renderer)
78
200
  return super().draw(renderer)
plotnine/_mpl/utils.py CHANGED
@@ -1,18 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- import typing
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from matplotlib.transforms import Affine2D, Bbox
6
6
 
7
7
  from .transforms import ZEROS_BBOX
8
8
 
9
- if typing.TYPE_CHECKING:
9
+ if TYPE_CHECKING:
10
10
  from matplotlib.artist import Artist
11
11
  from matplotlib.axes import Axes
12
12
  from matplotlib.backend_bases import RendererBase
13
13
  from matplotlib.figure import Figure
14
+ from matplotlib.gridspec import SubplotSpec
14
15
  from matplotlib.transforms import Transform
15
16
 
17
+ from .gridspec import p9GridSpec
18
+
16
19
 
17
20
  def bbox_in_figure_space(
18
21
  artist: Artist, fig: Figure, renderer: RendererBase
@@ -51,28 +54,93 @@ def pts_in_figure_space(fig: Figure, pts: float) -> float:
51
54
  return fig.transFigure.inverted().transform([0, pts])[1]
52
55
 
53
56
 
54
- def get_transPanels(fig: Figure) -> Transform:
57
+ def get_transPanels(fig: Figure, gs: p9GridSpec) -> Transform:
55
58
  """
56
59
  Coordinate system of the Panels (facets) area
57
60
 
58
61
  (0, 0) is the bottom-left of the bottom-left panel and
59
62
  (1, 1) is the top right of the top-right panel.
60
63
 
61
- The subplot parameters must be set before calling this function.
62
- i.e. fig.subplots_adjust should have been called.
64
+ The gridspec parameters must be set before calling this function.
65
+ i.e. gs.update have been called.
63
66
  """
64
- # Contains the layout information from which the panel area
65
- # is derived
66
- params = fig.subplotpars
67
+ # The position of the panels area in figure coordinates
68
+ params = gs.get_subplot_params(fig)
67
69
 
68
70
  # Figure width & height in display coordinates
69
71
  W, H = fig.bbox.width, fig.bbox.height
70
72
 
71
73
  # 1. The panels occupy space that is smaller than the figure
72
74
  # 2. That space is contained within the figure
73
- # We create a transform that represent these separable aspects
74
- # (but order matters), and use to transform transFigure
75
+ # We create a transform that represents these separable aspects
76
+ # (but order matters), and use it to transform transFigure
75
77
  sx, sy = params.right - params.left, params.top - params.bottom
76
78
  dx, dy = params.left * W, params.bottom * H
77
79
  transFiguretoPanels = Affine2D().scale(sx, sy).translate(dx, dy)
78
80
  return fig.transFigure + transFiguretoPanels
81
+
82
+
83
+ def rel_position(rel: float, length: float, low: float, high: float) -> float:
84
+ """
85
+ Relatively position an object of a given length between two position
86
+
87
+ Parameters
88
+ ----------
89
+ rel:
90
+ Relative position of the object between the limits.
91
+ length:
92
+ Length of the object
93
+ low:
94
+ Lower limit position
95
+ high:
96
+ Upper limit position
97
+ """
98
+ return low * (1 - rel) + (high - length) * rel
99
+
100
+
101
+ def get_subplotspecs(axs: list[Axes]) -> list[SubplotSpec]:
102
+ """
103
+ Return the SubplotSpecs of the given axes
104
+
105
+ Parameters
106
+ ----------
107
+ axs:
108
+ List of axes
109
+
110
+ Notes
111
+ -----
112
+ This functions returns the innermost subplotspec and it expects
113
+ every axes object to have one.
114
+ """
115
+ subplotspecs: list[SubplotSpec] = []
116
+ for ax in axs:
117
+ if not (subplotspec := ax.get_subplotspec()):
118
+ raise ValueError("Axes has no suplotspec")
119
+ subplotspecs.append(subplotspec)
120
+ return subplotspecs
121
+
122
+
123
+ def draw_gridspec(gs: p9GridSpec, color="black", **kwargs):
124
+ """
125
+ A debug function to draw a rectangle around the gridspec
126
+ """
127
+ draw_bbox(gs.bbox_relative, gs.figure, color, **kwargs)
128
+
129
+
130
+ def draw_bbox(bbox, figure, color="black", **kwargs):
131
+ """
132
+ A debug function to draw a rectangle around a bounding bbox
133
+ """
134
+ from matplotlib.patches import Rectangle
135
+
136
+ figure.add_artist(
137
+ Rectangle(
138
+ xy=bbox.p0,
139
+ width=bbox.width,
140
+ height=bbox.height,
141
+ edgecolor=color,
142
+ fill="facecolor" in kwargs,
143
+ clip_on=False,
144
+ **kwargs,
145
+ )
146
+ )
@@ -35,8 +35,10 @@ if TYPE_CHECKING:
35
35
  DataLike,
36
36
  FloatArray,
37
37
  FloatArrayLike,
38
+ HorizontalJustification,
38
39
  IntArray,
39
- SidePosition,
40
+ Side,
41
+ VerticalJustification,
40
42
  )
41
43
 
42
44
  T = TypeVar("T")
@@ -299,7 +301,7 @@ def ninteraction(df: pd.DataFrame, drop: bool = False) -> list[int]:
299
301
  return _id_var(df[df.columns[0]], drop)
300
302
 
301
303
  # Calculate individual ids
302
- ids = df.apply(_id_var, axis=0)
304
+ ids = df.apply(_id_var, axis=0, drop=drop)
303
305
  ids = ids.reindex(columns=list(reversed(ids.columns)))
304
306
 
305
307
  # Calculate dimensions
@@ -310,8 +312,8 @@ def ninteraction(df: pd.DataFrame, drop: bool = False) -> list[int]:
310
312
 
311
313
  combs = np.array(np.hstack([1, np.cumprod(ndistinct[:-1])]))
312
314
  mat = np.array(ids)
313
- res = (mat - 1) @ combs.T + 1
314
- res = np.array(res).flatten().tolist()
315
+ _res = (mat - 1) @ combs.T + 1
316
+ res: list[int] = np.array(_res).flatten().tolist()
315
317
 
316
318
  if drop:
317
319
  return _id_var(res, drop)
@@ -511,7 +513,7 @@ def remove_missing(
511
513
  if finite:
512
514
  lst = [np.inf, -np.inf]
513
515
  to_replace = {v: lst for v in vars}
514
- data.replace(to_replace, np.nan, inplace=True)
516
+ data.replace(to_replace, np.nan, inplace=True) # pyright: ignore[reportArgumentType,reportCallIssue]
515
517
  txt = "non-finite"
516
518
  else:
517
519
  txt = "missing"
@@ -604,7 +606,7 @@ def to_rgba(
604
606
  return c
605
607
 
606
608
  if is_iterable(colors):
607
- colors = cast(Sequence["ColorType"], colors)
609
+ colors = cast("Sequence[ColorType]", colors)
608
610
 
609
611
  if all(no_color(c) for c in colors):
610
612
  return "none"
@@ -1227,11 +1229,11 @@ def default_field(default: T) -> T:
1227
1229
  return field(default_factory=lambda: deepcopy(default))
1228
1230
 
1229
1231
 
1230
- def get_opposite_side(s: SidePosition) -> SidePosition:
1232
+ def get_opposite_side(s: Side) -> Side:
1231
1233
  """
1232
1234
  Return the opposite side
1233
1235
  """
1234
- lookup: dict[SidePosition, SidePosition] = {
1236
+ lookup: dict[Side, Side] = {
1235
1237
  "right": "left",
1236
1238
  "left": "right",
1237
1239
  "top": "bottom",
@@ -1241,7 +1243,7 @@ def get_opposite_side(s: SidePosition) -> SidePosition:
1241
1243
 
1242
1244
 
1243
1245
  def ensure_xy_location(
1244
- loc: SidePosition | Literal["center"] | float | tuple[float, float],
1246
+ loc: Side | Literal["center"] | float | tuple[float, float],
1245
1247
  ) -> tuple[float, float]:
1246
1248
  """
1247
1249
  Convert input into (x, y) location
@@ -1264,3 +1266,27 @@ def ensure_xy_location(
1264
1266
  if isinstance(h, (int, float)) and isinstance(v, (int, float)):
1265
1267
  return (h, v)
1266
1268
  raise ValueError(f"Cannot make a location from '{loc}'")
1269
+
1270
+
1271
+ def ha_as_float(ha: HorizontalJustification | float) -> float:
1272
+ """
1273
+ Return horizontal alignment as a float
1274
+ """
1275
+ lookup = {"left": 0.0, "center": 0.5, "right": 1.0}
1276
+ return lookup[ha] if isinstance(ha, str) else ha
1277
+
1278
+
1279
+ def va_as_float(va: VerticalJustification | float) -> float:
1280
+ """
1281
+ Return vertical alignment as a float
1282
+ """
1283
+ lookup = {
1284
+ "top": 1.0,
1285
+ "center": 0.5,
1286
+ "bottom": 0.0,
1287
+ # baseline and center_baseline are valid for texts but we do
1288
+ # not handle them accurately
1289
+ "baseline": 0.5,
1290
+ "center_baseline": 0.5,
1291
+ }
1292
+ return lookup[va] if isinstance(va, str) else va
plotnine/_utils/dev.py CHANGED
@@ -1,13 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional
4
3
 
5
-
6
- def get_plotnine_all(use_clipboard=True) -> Optional[str]:
4
+ def get_plotnine_all() -> str:
7
5
  """
8
6
  Generate package level * (star) imports for plotnine
9
-
10
- The contents of __all__ in plotnine/__init__.py
11
7
  """
12
8
  from importlib import import_module
13
9
 
@@ -28,32 +24,54 @@ def get_plotnine_all(use_clipboard=True) -> Optional[str]:
28
24
  "watermark",
29
25
  )
30
26
 
31
- def get_all_from_module(name, quote=False):
27
+ def get_all_from_module(name):
32
28
  """
33
29
  Module level imports
34
30
  """
35
31
  qname = f"plotnine.{name}"
36
32
  m = import_module(qname)
37
- fmt = ('"{}",' if quote else "{},").format
38
- return "\n ".join(fmt(x) for x in sorted(m.__all__))
39
33
 
40
- _imports = "\n".join(
41
- f"from .{name} import (\n {get_all_from_module(name)}\n)"
42
- for name in modules
43
- )
44
- _all = "\n".join(
45
- [
46
- "__all__ = (",
47
- "\n".join(
48
- f" {get_all_from_module(name, True)}" for name in modules
49
- ),
50
- ")",
51
- ]
52
- )
53
- content = f"{_imports}\n\n{_all}"
54
- if use_clipboard:
55
- from pandas.io import clipboard
34
+ return sorted(m.__all__)
35
+
36
+ imports = []
37
+ all_funcs = []
38
+
39
+ for name in modules:
40
+ funcs = get_all_from_module(name)
41
+ import_funcs = "\n ".join(f"{x}," for x in funcs)
42
+ imports.append(f"from .{name} import (\n {import_funcs}\n)")
43
+ all_funcs.extend(funcs)
44
+
45
+ all_funcs = [f' "{x}",' for x in sorted(all_funcs)]
46
+
47
+ _imports = "\n".join(imports)
48
+ _all = "__all__ = (\n" + "\n".join(all_funcs) + "\n)"
49
+
50
+ return f"{_imports}\n\n{_all}"
51
+
52
+
53
+ def get_init_py() -> str:
54
+ """
55
+ Generate plotnine/__init__.py
56
+ """
57
+
58
+ preamble: str = """# Do not edit this file by hand.
59
+ #
60
+ # Generate it using:
61
+ #
62
+ # $ python -c 'from plotnine._utils import dev; print(dev.get_init_py())'
63
+
64
+ from importlib.metadata import PackageNotFoundError, version
65
+
66
+ try:
67
+ __version__ = version("plotnine")
68
+ except PackageNotFoundError:
69
+ # package is not installed
70
+ pass
71
+ finally:
72
+ del version
73
+ del PackageNotFoundError
74
+
75
+ """
56
76
 
57
- clipboard.copy(content) # pyright: ignore
58
- else:
59
- return content
77
+ return preamble + get_plotnine_all()