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.
- plotnine/_mpl/layout_manager/_layout_items.py +219 -118
- plotnine/_mpl/layout_manager/_layout_tree.py +168 -115
- plotnine/_mpl/layout_manager/_spaces.py +22 -9
- plotnine/_mpl/patches.py +1 -1
- plotnine/_mpl/text.py +59 -24
- plotnine/_utils/__init__.py +1 -1
- plotnine/facets/strips.py +5 -2
- plotnine/geoms/geom_bar.py +10 -2
- plotnine/geoms/geom_col.py +6 -0
- plotnine/geoms/geom_violin.py +24 -7
- plotnine/guides/guide.py +2 -2
- plotnine/guides/guide_legend.py +7 -8
- plotnine/iapi.py +4 -2
- plotnine/mapping/_eval_environment.py +85 -0
- plotnine/mapping/aes.py +10 -26
- plotnine/mapping/evaluation.py +7 -65
- plotnine/plot_composition/_compose.py +13 -4
- plotnine/stats/stat_sina.py +33 -0
- plotnine/themes/elements/element_text.py +1 -0
- plotnine/themes/targets.py +1 -1
- plotnine/themes/theme_gray.py +1 -0
- plotnine/themes/themeable.py +5 -5
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/METADATA +1 -1
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/RECORD +27 -26
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/WHEEL +1 -1
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/top_level.txt +0 -0
plotnine/geoms/geom_bar.py
CHANGED
|
@@ -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"]
|
|
57
|
-
data["xmax"] = data["x"] + data["width"]
|
|
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
|
plotnine/geoms/geom_col.py
CHANGED
|
@@ -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
|
}
|
plotnine/geoms/geom_violin.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
|
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
|
|
119
|
-
style = params
|
|
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,
|
|
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,
|
|
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
|
|
plotnine/guides/guide_legend.py
CHANGED
|
@@ -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
|
|
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:
|
|
179
|
-
for ae
|
|
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
|
-
"
|
|
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:
|
|
235
|
-
va:
|
|
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
|
|
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
|
|
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
|
|
175
|
-
|
|
174
|
+
`aes` has 2 internal functions that you can use in your expressions
|
|
175
|
+
when transforming the variables.
|
|
176
176
|
|
|
177
|
-
1.
|
|
178
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
256
|
+
@cached_property
|
|
273
257
|
def _scaled(self) -> dict[str, Any]:
|
|
274
258
|
"""
|
|
275
259
|
Return only the aesthetics mapped to after scaling
|
plotnine/mapping/evaluation.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import numbers
|
|
4
|
-
import
|
|
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
|
|
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(
|
|
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
|
-
|
|
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=
|
|
299
|
+
figure.savefig(filename, format=format)
|
|
291
300
|
|
|
292
301
|
|
|
293
302
|
@dataclass
|
plotnine/stats/stat_sina.py
CHANGED
|
@@ -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
|
plotnine/themes/targets.py
CHANGED
plotnine/themes/theme_gray.py
CHANGED
plotnine/themes/themeable.py
CHANGED
|
@@ -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("
|
|
1083
|
+
if not params.get("bottom", False):
|
|
1084
1084
|
return
|
|
1085
1085
|
|
|
1086
1086
|
# We have to use both
|