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.
@@ -20,6 +20,11 @@ class geom_bar(geom_rect):
20
20
  Parameters
21
21
  ----------
22
22
  {common_parameters}
23
+ just : float, default=0.5
24
+ How to align the column with respect to the axis breaks. The default
25
+ `0.5` aligns the center of the column with the break. `0` aligns the
26
+ left of the of the column with the break and `1` aligns the right of
27
+ the column with the break.
23
28
  width : float, default=None
24
29
  Bar width. If `None`{.py}, the width is set to
25
30
  `90%` of the resolution of the data.
@@ -35,6 +40,7 @@ class geom_bar(geom_rect):
35
40
  "stat": "count",
36
41
  "position": "stack",
37
42
  "na_rm": False,
43
+ "just": 0.5,
38
44
  "width": None,
39
45
  }
40
46
 
@@ -45,6 +51,8 @@ class geom_bar(geom_rect):
45
51
  else:
46
52
  data["width"] = resolution(data["x"], False) * 0.9
47
53
 
54
+ just = self.params.get("just", 0.5)
55
+
48
56
  bool_idx = data["y"] < 0
49
57
 
50
58
  data["ymin"] = 0.0
@@ -53,7 +61,7 @@ class geom_bar(geom_rect):
53
61
  data["ymax"] = data["y"]
54
62
  data.loc[bool_idx, "ymax"] = 0.0
55
63
 
56
- data["xmin"] = data["x"] - data["width"] / 2
57
- data["xmax"] = data["x"] + data["width"] / 2
64
+ data["xmin"] = data["x"] - data["width"] * just
65
+ data["xmax"] = data["x"] + data["width"] * (1 - just)
58
66
  del data["width"]
59
67
  return data
@@ -17,6 +17,11 @@ class geom_col(geom_bar):
17
17
  Parameters
18
18
  ----------
19
19
  {common_parameters}
20
+ just : float, default=0.5
21
+ How to align the column with respect to the axis breaks. The default
22
+ `0.5` aligns the center of the column with the break. `0` aligns the
23
+ left of the of the column with the break and `1` aligns the right of
24
+ the column with the break.
20
25
  width : float, default=None
21
26
  Bar width. If `None`{.py}, the width is set to
22
27
  `90%` of the resolution of the data.
@@ -32,5 +37,6 @@ class geom_col(geom_bar):
32
37
  "stat": "identity",
33
38
  "position": "stack",
34
39
  "na_rm": False,
40
+ "just": 0.5,
35
41
  "width": None,
36
42
  }
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import typing
3
+ from typing import TYPE_CHECKING, cast
4
4
 
5
5
  import numpy as np
6
6
  import pandas as pd
@@ -11,7 +11,7 @@ from .geom import geom
11
11
  from .geom_path import geom_path
12
12
  from .geom_polygon import geom_polygon
13
13
 
14
- if typing.TYPE_CHECKING:
14
+ if TYPE_CHECKING:
15
15
  from typing import Any
16
16
 
17
17
  from matplotlib.axes import Axes
@@ -115,10 +115,17 @@ class geom_violin(geom):
115
115
  ax: Axes,
116
116
  **params: Any,
117
117
  ):
118
- quantiles = params["draw_quantiles"]
119
- style = params["style"]
118
+ quantiles = params.pop("draw_quantiles")
119
+ style = params.pop("style")
120
+ zorder = params.pop("zorder")
121
+
122
+ for i, (group, df) in enumerate(data.groupby("group")):
123
+ # Place the violins with the smalleer group number on top
124
+ # of those with larger numbers. The group_zorder values should be
125
+ # in the range [zorder, zorder + 1) to stay within the layer.
126
+ group = cast("int", group)
127
+ group_zorder = zorder + 0.9 / group
120
128
 
121
- for i, (_, df) in enumerate(data.groupby("group")):
122
129
  # Find the points for the line to go all the way around
123
130
  df["xminv"] = df["x"] - df["violinwidth"] * (df["x"] - df["xmin"])
124
131
  df["xmaxv"] = df["x"] + df["violinwidth"] * (df["xmax"] - df["x"])
@@ -156,7 +163,12 @@ class geom_violin(geom):
156
163
 
157
164
  # plot violin polygon
158
165
  geom_polygon.draw_group(
159
- polygon_df, panel_params, coord, ax, **params
166
+ polygon_df,
167
+ panel_params,
168
+ coord,
169
+ ax,
170
+ zorder=group_zorder,
171
+ **params,
160
172
  )
161
173
 
162
174
  if quantiles is not None:
@@ -174,7 +186,12 @@ class geom_violin(geom):
174
186
 
175
187
  # plot quantile segments
176
188
  geom_path.draw_group(
177
- segment_df, panel_params, coord, ax, **params
189
+ segment_df,
190
+ panel_params,
191
+ coord,
192
+ ax,
193
+ zorder=group_zorder,
194
+ **params,
178
195
  )
179
196
 
180
197
 
plotnine/guides/guide.py CHANGED
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  from typing_extensions import Self
19
19
 
20
20
  from plotnine import aes, guides
21
- from plotnine.layer import Layers
21
+ from plotnine.layer import Layers, layer
22
22
  from plotnine.scales.scale import scale
23
23
  from plotnine.typing import (
24
24
  LegendPosition,
@@ -79,7 +79,7 @@ class guide(ABC, metaclass=Register):
79
79
  self.elements = cast("GuideElements", None)
80
80
  self.guides_elements: GuidesElements
81
81
 
82
- def legend_aesthetics(self, layer):
82
+ def legend_aesthetics(self, layer: layer):
83
83
  """
84
84
  Return the aesthetics that contribute to the legend
85
85
 
@@ -171,21 +171,20 @@ class guide_legend(guide):
171
171
  # Modify aesthetics
172
172
 
173
173
  # When doing after_scale evaluations, we only consider those
174
- # for the aesthetics of this legend. The reduces the spurious
175
- # warnings where an evaluation of another aesthetic failed yet
176
- # it is not needed.
174
+ # for the aesthetics that are valid for this layer/geom.
177
175
  aes_modifiers = {
178
- ae: expr
179
- for ae, expr in l.mapping._scaled.items()
180
- if ae in matched_set
176
+ ae: l.mapping._scaled[ae]
177
+ for ae in l.geom.aesthetics() & l.mapping._scaled.keys()
181
178
  }
182
179
 
183
180
  try:
184
181
  data = l.use_defaults(data, aes_modifiers)
185
182
  except PlotnineError:
186
183
  warn(
187
- "Failed to apply `after_scale` modifications "
188
- "to the legend.",
184
+ "Failed to apply `after_scale` modifications to the "
185
+ "legend. This probably should not happen. Help us "
186
+ "discover why, please open and issue at "
187
+ "https://github.com/has2k1/plotnine/issues",
189
188
  PlotnineWarning,
190
189
  )
191
190
  data = l.use_defaults(data, {})
plotnine/iapi.py CHANGED
@@ -22,8 +22,10 @@ if TYPE_CHECKING:
22
22
  from plotnine.typing import (
23
23
  CoordRange,
24
24
  FloatArrayLike,
25
+ HorizontalJustification,
25
26
  ScaledAestheticsName,
26
27
  StripPosition,
28
+ VerticalJustification,
27
29
  )
28
30
 
29
31
  from ._mpl.offsetbox import FlexibleAnchoredOffsetbox
@@ -231,8 +233,8 @@ class strip_draw_info:
231
233
 
232
234
  x: float
233
235
  y: float
234
- ha: str
235
- va: str
236
+ ha: HorizontalJustification | float
237
+ va: VerticalJustification | float
236
238
  box_width: float
237
239
  box_height: float
238
240
  strip_text_margin: float
@@ -0,0 +1,85 @@
1
+ """
2
+ These are functions that can be called by the user inside the aes()
3
+ mapping. This is meant to make it easy to transform column-variables
4
+ as easily as is possible in ggplot2.
5
+
6
+ We only implement the most common functions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ if TYPE_CHECKING:
17
+ from typing import Any, Sequence
18
+
19
+ __all__ = (
20
+ "factor",
21
+ "reorder",
22
+ )
23
+
24
+
25
+ def factor(
26
+ values: Sequence[Any],
27
+ categories: Sequence[Any] | None = None,
28
+ ordered: bool | None = None,
29
+ ) -> pd.Categorical:
30
+ """
31
+ Turn x in to a categorical (factor) variable
32
+
33
+ It is just an alias to `pandas.Categorical`
34
+
35
+ Parameters
36
+ ----------
37
+ values :
38
+ The values of the categorical. If categories are given, values not in
39
+ categories will be replaced with NaN.
40
+ categories :
41
+ The unique categories for this categorical. If not given, the
42
+ categories are assumed to be the unique values of `values`
43
+ (sorted, if possible, otherwise in the order in which they appear).
44
+ ordered :
45
+ Whether or not this categorical is treated as a ordered categorical.
46
+ If True, the resulting categorical will be ordered.
47
+ An ordered categorical respects, when sorted, the order of its
48
+ `categories` attribute (which in turn is the `categories` argument, if
49
+ provided).
50
+ """
51
+ return pd.Categorical(values, categories=categories, ordered=None)
52
+
53
+
54
+ def reorder(x, y, fun=np.median, ascending=True):
55
+ """
56
+ Reorder categorical by sorting along another variable
57
+
58
+ It is the order of the categories that changes. Values in x
59
+ are grouped by categories and summarised to determine the
60
+ new order.
61
+
62
+ Credit: Copied from plydata
63
+
64
+ Parameters
65
+ ----------
66
+ x : list-like
67
+ Values that will make up the categorical.
68
+ y : list-like
69
+ Values by which `c` will be ordered.
70
+ fun : callable
71
+ Summarising function to `x` for each category in `c`.
72
+ Default is the *median*.
73
+ ascending : bool
74
+ If `True`, the `c` is ordered in ascending order of `x`.
75
+ """
76
+ if len(x) != len(y):
77
+ raise ValueError(f"Lengths are not equal. {len(x)=}, {len(x)=}")
78
+ summary = (
79
+ pd.Series(y)
80
+ .groupby(x, observed=True)
81
+ .apply(fun)
82
+ .sort_values(ascending=ascending)
83
+ )
84
+ cats = summary.index.to_list()
85
+ return pd.Categorical(x, categories=cats)
plotnine/mapping/aes.py CHANGED
@@ -1,19 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- import typing
5
4
  from collections.abc import Iterable, Sequence
6
5
  from contextlib import suppress
7
6
  from copy import deepcopy
8
7
  from dataclasses import fields
9
- from typing import Any, Dict
8
+ from functools import cached_property
9
+ from typing import TYPE_CHECKING, Any, Dict
10
10
 
11
11
  import pandas as pd
12
12
 
13
13
  from ..iapi import labels_view
14
14
  from .evaluation import after_stat, stage
15
15
 
16
- if typing.TYPE_CHECKING:
16
+ if TYPE_CHECKING:
17
17
  from typing import Protocol, TypeVar
18
18
 
19
19
  class ColorOrColour(Protocol):
@@ -171,27 +171,11 @@ class aes(Dict[str, Any]):
171
171
  ggplot(df, aes(x="df.index", y="np.sin(gam ma)"))
172
172
  ```
173
173
 
174
- `aes` has 2 internal methods you can use to transform variables being
175
- mapped.
174
+ `aes` has 2 internal functions that you can use in your expressions
175
+ when transforming the variables.
176
176
 
177
- 1. `factor` - This function turns the variable into a factor.
178
- It is just an alias to `pandas.Categorical`:
179
-
180
- ```python
181
- ggplot(mtcars, aes(x="factor(cyl)")) + geom_bar()
182
- ```
183
-
184
- 2. `reorder` - This function changes the order of first variable
185
- based on values of the second variable:
186
-
187
- ```python
188
- df = pd.DataFrame({
189
- "x": ["b", "d", "c", "a"],
190
- "y": [1, 2, 3, 4]
191
- })
192
-
193
- ggplot(df, aes("reorder(x, y)", "y")) + geom_col()
194
- ```
177
+ 1. [](:func:`~plotnine.mapping._eval_environment.factor`)
178
+ 1. [](:func:`~plotnine.mapping._eval_environment.reorder`)
195
179
 
196
180
  **The group aesthetic**
197
181
 
@@ -237,7 +221,7 @@ class aes(Dict[str, Any]):
237
221
  kwargs[name] = after_stat(_after_stat)
238
222
  return kwargs
239
223
 
240
- @property
224
+ @cached_property
241
225
  def _starting(self) -> dict[str, Any]:
242
226
  """
243
227
  Return the subset of aesthetics mapped from the layer data
@@ -254,7 +238,7 @@ class aes(Dict[str, Any]):
254
238
 
255
239
  return d
256
240
 
257
- @property
241
+ @cached_property
258
242
  def _calculated(self) -> dict[str, Any]:
259
243
  """
260
244
  Return only the aesthetics mapped to calculated statistics
@@ -269,7 +253,7 @@ class aes(Dict[str, Any]):
269
253
 
270
254
  return d
271
255
 
272
- @property
256
+ @cached_property
273
257
  def _scaled(self) -> dict[str, Any]:
274
258
  """
275
259
  Return only the aesthetics mapped to after scaling
@@ -1,15 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import numbers
4
- import typing
4
+ from typing import TYPE_CHECKING
5
5
 
6
6
  import numpy as np
7
7
  import pandas as pd
8
8
  import pandas.api.types as pdtypes
9
9
 
10
10
  from ..exceptions import PlotnineError
11
+ from ._eval_environment import factor, reorder
11
12
 
12
- if typing.TYPE_CHECKING:
13
+ if TYPE_CHECKING:
13
14
  from typing import Any
14
15
 
15
16
  from . import aes
@@ -18,6 +19,9 @@ if typing.TYPE_CHECKING:
18
19
 
19
20
  __all__ = ("after_stat", "after_scale", "stage")
20
21
 
22
+
23
+ EVAL_ENVIRONMENT = {"factor": factor, "reorder": reorder}
24
+
21
25
  _TPL_EVAL_FAIL = """\
22
26
  Could not evaluate the '{}' mapping: '{}' \
23
27
  (original error: {})"""
@@ -108,68 +112,6 @@ def after_scale(x):
108
112
  return stage(after_scale=x)
109
113
 
110
114
 
111
- def reorder(x, y, fun=np.median, ascending=True):
112
- """
113
- Reorder categorical by sorting along another variable
114
-
115
- It is the order of the categories that changes. Values in x
116
- are grouped by categories and summarised to determine the
117
- new order.
118
-
119
- Credit: Copied from plydata
120
-
121
- Parameters
122
- ----------
123
- x : list-like
124
- Values that will make up the categorical.
125
- y : list-like
126
- Values by which `c` will be ordered.
127
- fun : callable
128
- Summarising function to `x` for each category in `c`.
129
- Default is the *median*.
130
- ascending : bool
131
- If `True`, the `c` is ordered in ascending order of `x`.
132
-
133
- Examples
134
- --------
135
- >>> c = list('abbccc')
136
- >>> x = [11, 2, 2, 3, 33, 3]
137
- >>> cat_reorder(c, x)
138
- [a, b, b, c, c, c]
139
- Categories (3, object): [b, c, a]
140
- >>> cat_reorder(c, x, fun=max)
141
- [a, b, b, c, c, c]
142
- Categories (3, object): [b, a, c]
143
- >>> cat_reorder(c, x, fun=max, ascending=False)
144
- [a, b, b, c, c, c]
145
- Categories (3, object): [c, a, b]
146
- >>> c_ordered = pd.Categorical(c, ordered=True)
147
- >>> cat_reorder(c_ordered, x)
148
- [a, b, b, c, c, c]
149
- Categories (3, object): [b < c < a]
150
- >>> cat_reorder(c + ['d'], x)
151
- Traceback (most recent call last):
152
- ...
153
- ValueError: Lengths are not equal. len(c) is 7 and len(x) is 6.
154
- """
155
- if len(x) != len(y):
156
- raise ValueError(f"Lengths are not equal. {len(x)=}, {len(x)=}")
157
- summary = (
158
- pd.Series(y)
159
- .groupby(x, observed=True)
160
- .apply(fun)
161
- .sort_values(ascending=ascending)
162
- )
163
- cats = summary.index.to_list()
164
- return pd.Categorical(x, categories=cats)
165
-
166
-
167
- # These are function that can be called by the user inside the aes()
168
- # mapping. This is meant to make the variable transformations as easy
169
- # as they are in ggplot2
170
- AES_INNER_NAMESPACE = {"factor": pd.Categorical, "reorder": reorder}
171
-
172
-
173
115
  def evaluate(
174
116
  aesthetics: aes | dict[str, Any], data: pd.DataFrame, env: Environment
175
117
  ) -> pd.DataFrame:
@@ -207,7 +149,7 @@ def evaluate(
207
149
  3 16
208
150
  4 25
209
151
  """
210
- env = env.with_outer_namespace(AES_INNER_NAMESPACE)
152
+ env = env.with_outer_namespace(EVAL_ENVIRONMENT)
211
153
 
212
154
  # Store evaluation results in a dict column in a dict
213
155
  evaled = {}
@@ -283,11 +283,20 @@ class Compose:
283
283
  )
284
284
  return figure
285
285
 
286
- def save(
287
- self, filename: str | Path | BytesIO, save_format: str | None = None
288
- ):
286
+ def save(self, filename: str | Path | BytesIO, format: str | None = None):
287
+ """
288
+ Save a Compose object as an image file
289
+
290
+ Parameters
291
+ ----------
292
+ filename :
293
+ File name to write the plot to. If not specified, a name
294
+ format :
295
+ Image format to use, automatically extract from
296
+ file name extension.
297
+ """
289
298
  figure = self.draw()
290
- figure.savefig(filename, format=save_format)
299
+ figure.savefig(filename, format=format)
291
300
 
292
301
 
293
302
  @dataclass
@@ -57,6 +57,15 @@ class stat_sina(stat):
57
57
  - `area` - Scale by the largest density/bin among the different sinas
58
58
  - `count` - areas are scaled proportionally to the number of points
59
59
  - `width` - Only scale according to the maxwidth parameter.
60
+ style :
61
+ Type of sina plot to draw. The options are
62
+ ```python
63
+ 'full' # Regular (2 sided)
64
+ 'left' # Left-sided half
65
+ 'right' # Right-sided half
66
+ 'left-right' # Alternate (left first) half by the group
67
+ 'right-left' # Alternate (right first) half by the group
68
+ ```
60
69
 
61
70
  See Also
62
71
  --------
@@ -91,6 +100,7 @@ class stat_sina(stat):
91
100
  "bin_limit": 1,
92
101
  "random_state": None,
93
102
  "scale": "area",
103
+ "style": "full",
94
104
  }
95
105
  CREATES = {"scaled"}
96
106
 
@@ -245,6 +255,29 @@ class stat_sina(stat):
245
255
 
246
256
  def finish_layer(self, data, params):
247
257
  # Rescale x in case positions have been adjusted
258
+ style = params["style"]
259
+ x_mean = data["x"].to_numpy()
248
260
  x_mod = (data["xmax"] - data["xmin"]) / data["width"]
249
261
  data["x"] = data["x"] + data["x_diff"] * x_mod
262
+ x = data["x"].to_numpy()
263
+ even = data["group"].to_numpy() % 2 == 0
264
+
265
+ def mirror_x(bool_idx):
266
+ """
267
+ Mirror x locations along the mean value
268
+ """
269
+ data.loc[bool_idx, "x"] = (
270
+ 2 * x_mean[bool_idx] - data.loc[bool_idx, "x"]
271
+ )
272
+
273
+ match style:
274
+ case "left":
275
+ mirror_x(x_mean < x)
276
+ case "right":
277
+ mirror_x(x < x_mean)
278
+ case "left-right":
279
+ mirror_x(even & (x < x_mean) | ~even & (x_mean < x))
280
+ case "right-left":
281
+ mirror_x(even & (x_mean < x) | ~even & (x < x_mean))
282
+
250
283
  return data
@@ -164,6 +164,7 @@ class element_text(element_base):
164
164
  "size",
165
165
  "style",
166
166
  "va",
167
+ "ma",
167
168
  "weight",
168
169
  "rotation_mode",
169
170
  )
@@ -21,7 +21,7 @@ class ThemeTargets:
21
21
  """
22
22
  Artists that will be themed
23
23
 
24
- This includes only artist that cannot be accessed easily from
24
+ This includes only artist that cannot be easily accessed from
25
25
  the figure or the axes.
26
26
  """
27
27
 
@@ -43,6 +43,7 @@ class theme_gray(theme):
43
43
  family=base_family,
44
44
  style="normal",
45
45
  color="black",
46
+ ma="center",
46
47
  size=base_size,
47
48
  linespacing=0.9,
48
49
  rotation=0,
@@ -811,7 +811,7 @@ class strip_text_x(MixinSequenceOfValues):
811
811
  theme_element : element_text
812
812
  """
813
813
 
814
- _omit = ["margin"]
814
+ _omit = ["margin", "ha"]
815
815
 
816
816
  def apply_figure(self, figure: Figure, targets: ThemeTargets):
817
817
  super().apply_figure(figure, targets)
@@ -834,7 +834,7 @@ class strip_text_y(MixinSequenceOfValues):
834
834
  theme_element : element_text
835
835
  """
836
836
 
837
- _omit = ["margin"]
837
+ _omit = ["margin", "va"]
838
838
 
839
839
  def apply_figure(self, figure: Figure, targets: ThemeTargets):
840
840
  super().apply_figure(figure, targets)
@@ -890,7 +890,7 @@ class axis_text_x(MixinSequenceOfValues):
890
890
  creates a margin of 5 points.
891
891
  """
892
892
 
893
- _omit = ["margin"]
893
+ _omit = ["margin", "va"]
894
894
 
895
895
  def apply_ax(self, ax: Axes):
896
896
  super().apply_ax(ax)
@@ -923,7 +923,7 @@ class axis_text_y(MixinSequenceOfValues):
923
923
  creates a margin of 5 points.
924
924
  """
925
925
 
926
- _omit = ["margin"]
926
+ _omit = ["margin", "ha"]
927
927
 
928
928
  def apply_ax(self, ax: Axes):
929
929
  super().apply_ax(ax)
@@ -1080,7 +1080,7 @@ class axis_ticks_minor_x(MixinSequenceOfValues):
1080
1080
  # to invisible. Theming should not change those artists to visible,
1081
1081
  # so we return early.
1082
1082
  params = ax.xaxis.get_tick_params(which="minor")
1083
- if not params.get("left", False):
1083
+ if not params.get("bottom", False):
1084
1084
  return
1085
1085
 
1086
1086
  # We have to use both
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotnine
3
- Version: 0.15.0.dev1
3
+ Version: 0.15.0.dev3
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)