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.
- plotnine/__init__.py +31 -37
- plotnine/_mpl/gridspec.py +265 -0
- plotnine/_mpl/layout_manager/__init__.py +6 -0
- plotnine/_mpl/layout_manager/_engine.py +87 -0
- plotnine/_mpl/layout_manager/_layout_items.py +957 -0
- plotnine/_mpl/layout_manager/_layout_tree.py +905 -0
- plotnine/_mpl/layout_manager/_spaces.py +1154 -0
- plotnine/_mpl/patches.py +70 -34
- plotnine/_mpl/text.py +159 -37
- plotnine/_mpl/utils.py +78 -10
- plotnine/_utils/__init__.py +35 -9
- plotnine/_utils/dev.py +45 -27
- plotnine/_utils/yippie.py +115 -0
- plotnine/animation.py +1 -1
- plotnine/coords/coord.py +3 -3
- plotnine/coords/coord_trans.py +1 -1
- plotnine/data/__init__.py +43 -8
- plotnine/data/anscombe-quartet.csv +45 -0
- plotnine/doctools.py +2 -2
- plotnine/facets/facet.py +34 -43
- plotnine/facets/facet_grid.py +14 -6
- plotnine/facets/facet_wrap.py +3 -5
- plotnine/facets/strips.py +20 -33
- plotnine/geoms/annotate.py +3 -3
- plotnine/geoms/annotation_logticks.py +2 -0
- plotnine/geoms/annotation_stripes.py +2 -0
- plotnine/geoms/geom.py +3 -3
- plotnine/geoms/geom_bar.py +10 -2
- plotnine/geoms/geom_col.py +6 -0
- plotnine/geoms/geom_crossbar.py +2 -3
- plotnine/geoms/geom_path.py +2 -2
- plotnine/geoms/geom_violin.py +24 -7
- plotnine/ggplot.py +95 -66
- plotnine/guides/guide.py +19 -20
- plotnine/guides/guide_colorbar.py +6 -6
- plotnine/guides/guide_legend.py +15 -16
- plotnine/guides/guides.py +8 -8
- plotnine/helpers.py +49 -0
- plotnine/iapi.py +33 -7
- plotnine/labels.py +8 -3
- plotnine/layer.py +4 -4
- plotnine/mapping/_env.py +2 -2
- plotnine/mapping/_eval_environment.py +85 -0
- plotnine/mapping/aes.py +14 -30
- plotnine/mapping/evaluation.py +7 -65
- plotnine/options.py +14 -7
- plotnine/plot_composition/__init__.py +10 -0
- plotnine/plot_composition/_compose.py +462 -0
- plotnine/plot_composition/_plotspec.py +50 -0
- plotnine/plot_composition/_spacer.py +32 -0
- plotnine/positions/position_dodge.py +1 -1
- plotnine/positions/position_dodge2.py +1 -1
- plotnine/positions/position_stack.py +1 -2
- plotnine/qplot.py +1 -2
- plotnine/scales/__init__.py +0 -6
- plotnine/scales/limits.py +7 -7
- plotnine/scales/scale.py +4 -4
- plotnine/scales/scale_continuous.py +2 -1
- plotnine/scales/scale_identity.py +10 -2
- plotnine/scales/scale_manual.py +6 -2
- plotnine/stats/binning.py +5 -2
- plotnine/stats/smoothers.py +3 -5
- plotnine/stats/stat.py +3 -3
- plotnine/stats/stat_bindot.py +1 -3
- plotnine/stats/stat_density.py +2 -2
- plotnine/stats/stat_qq_line.py +1 -1
- plotnine/stats/stat_sina.py +34 -1
- plotnine/themes/elements/__init__.py +3 -0
- plotnine/themes/elements/element_text.py +35 -24
- plotnine/themes/elements/margin.py +137 -61
- plotnine/themes/targets.py +3 -1
- plotnine/themes/theme.py +21 -7
- plotnine/themes/theme_538.py +0 -1
- plotnine/themes/theme_bw.py +0 -1
- plotnine/themes/theme_dark.py +0 -1
- plotnine/themes/theme_gray.py +32 -34
- plotnine/themes/theme_light.py +1 -1
- plotnine/themes/theme_matplotlib.py +28 -31
- plotnine/themes/theme_seaborn.py +36 -36
- plotnine/themes/theme_void.py +25 -27
- plotnine/themes/theme_xkcd.py +0 -1
- plotnine/themes/themeable.py +369 -169
- plotnine/typing.py +3 -3
- plotnine/watermark.py +3 -3
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/METADATA +8 -5
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/RECORD +89 -78
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/WHEEL +1 -1
- plotnine/_mpl/_plot_side_space.py +0 -888
- plotnine/_mpl/_plotnine_tight_layout.py +0 -293
- plotnine/_mpl/layout_engine.py +0 -110
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info/licenses}/LICENSE +0 -0
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/top_level.txt +0 -0
plotnine/guides/guide_legend.py
CHANGED
|
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|
|
25
25
|
|
|
26
26
|
from plotnine.geoms.geom import geom
|
|
27
27
|
from plotnine.layer import layer
|
|
28
|
-
from plotnine.typing import
|
|
28
|
+
from plotnine.typing import Side
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
# See guides.py for terminology
|
|
@@ -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, {})
|
|
@@ -226,10 +225,10 @@ class guide_legend(guide):
|
|
|
226
225
|
ncol = int(np.ceil(nbreak / 15))
|
|
227
226
|
|
|
228
227
|
if nrow is None:
|
|
229
|
-
ncol = cast(int, ncol)
|
|
228
|
+
ncol = cast("int", ncol)
|
|
230
229
|
nrow = int(np.ceil(nbreak / ncol))
|
|
231
230
|
elif ncol is None:
|
|
232
|
-
nrow = cast(int, nrow)
|
|
231
|
+
nrow = cast("int", nrow)
|
|
233
232
|
ncol = int(np.ceil(nbreak / nrow))
|
|
234
233
|
|
|
235
234
|
return nrow, ncol
|
|
@@ -255,7 +254,7 @@ class guide_legend(guide):
|
|
|
255
254
|
elements = self.elements
|
|
256
255
|
|
|
257
256
|
# title
|
|
258
|
-
title = cast(str, self.title)
|
|
257
|
+
title = cast("str", self.title)
|
|
259
258
|
title_box = TextArea(title)
|
|
260
259
|
targets.legend_title = title_box._text # type: ignore
|
|
261
260
|
|
|
@@ -282,7 +281,7 @@ class guide_legend(guide):
|
|
|
282
281
|
targets.legend_key = drawings
|
|
283
282
|
|
|
284
283
|
# Match Drawings with labels to create the entries
|
|
285
|
-
lookup: dict[
|
|
284
|
+
lookup: dict[Side, tuple[type[PackerBase], slice]] = {
|
|
286
285
|
"right": (HPacker, reverse),
|
|
287
286
|
"left": (HPacker, obverse),
|
|
288
287
|
"bottom": (VPacker, reverse),
|
|
@@ -381,7 +380,7 @@ class GuideElementsLegend(GuideElements):
|
|
|
381
380
|
)
|
|
382
381
|
|
|
383
382
|
@cached_property
|
|
384
|
-
def text_position(self) ->
|
|
383
|
+
def text_position(self) -> Side:
|
|
385
384
|
if not (pos := self.theme.getp("legend_text_position")):
|
|
386
385
|
pos = "right"
|
|
387
386
|
return pos
|
|
@@ -403,7 +402,7 @@ class GuideElementsLegend(GuideElements):
|
|
|
403
402
|
dimensions are big enough.
|
|
404
403
|
"""
|
|
405
404
|
# Note the different height sizes for the entries
|
|
406
|
-
guide = cast(guide_legend, self.guide)
|
|
405
|
+
guide = cast("guide_legend", self.guide)
|
|
407
406
|
min_size = (
|
|
408
407
|
self.theme.getp("legend_key_width"),
|
|
409
408
|
self.theme.getp("legend_key_height"),
|
|
@@ -452,7 +451,7 @@ class GuideElementsLegend(GuideElements):
|
|
|
452
451
|
If legend is horizontal, then key heights must be equal, so we
|
|
453
452
|
use the maximum
|
|
454
453
|
"""
|
|
455
|
-
hs = [h for
|
|
454
|
+
hs = [h for _, h in self._key_dimensions]
|
|
456
455
|
if self.is_horizontal:
|
|
457
456
|
return [max(hs)] * len(hs)
|
|
458
457
|
return hs
|
plotnine/guides/guides.py
CHANGED
|
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
|
|
35
35
|
NoGuide,
|
|
36
36
|
Orientation,
|
|
37
37
|
ScaledAestheticsName,
|
|
38
|
-
|
|
38
|
+
Side,
|
|
39
39
|
TextJustification,
|
|
40
40
|
)
|
|
41
41
|
|
|
@@ -104,7 +104,7 @@ class guides:
|
|
|
104
104
|
raise ValueError("Got a guide for color and colour, choose one.")
|
|
105
105
|
rename_aesthetics(self)
|
|
106
106
|
|
|
107
|
-
def __radd__(self,
|
|
107
|
+
def __radd__(self, other: ggplot):
|
|
108
108
|
"""
|
|
109
109
|
Add guides to the plot
|
|
110
110
|
|
|
@@ -120,9 +120,9 @@ class guides:
|
|
|
120
120
|
"""
|
|
121
121
|
for f in fields(self):
|
|
122
122
|
if (g := getattr(self, f.name)) is not None:
|
|
123
|
-
setattr(
|
|
123
|
+
setattr(other.guides, f.name, g)
|
|
124
124
|
|
|
125
|
-
return
|
|
125
|
+
return other
|
|
126
126
|
|
|
127
127
|
def _build(self) -> Sequence[guide]:
|
|
128
128
|
"""
|
|
@@ -326,7 +326,7 @@ class guides:
|
|
|
326
326
|
|
|
327
327
|
# Group together guides for each position
|
|
328
328
|
groups: dict[
|
|
329
|
-
tuple[
|
|
329
|
+
tuple[Side, float]
|
|
330
330
|
| tuple[tuple[float, float], tuple[float, float]],
|
|
331
331
|
list[PackerBase],
|
|
332
332
|
] = defaultdict(list)
|
|
@@ -342,8 +342,8 @@ class guides:
|
|
|
342
342
|
if isinstance(position, str) and isinstance(just, (float, int)):
|
|
343
343
|
setattr(legends, position, outside_legend(aob, just))
|
|
344
344
|
else:
|
|
345
|
-
position = cast(tuple[float, float], position)
|
|
346
|
-
just = cast(tuple[float, float], just)
|
|
345
|
+
position = cast("tuple[float, float]", position)
|
|
346
|
+
just = cast("tuple[float, float]", just)
|
|
347
347
|
legends.inside.append(inside_legend(aob, just, position))
|
|
348
348
|
|
|
349
349
|
return legends
|
|
@@ -467,7 +467,7 @@ class GuidesElements:
|
|
|
467
467
|
if just is None:
|
|
468
468
|
just = (0.5, 0.5)
|
|
469
469
|
elif just in VALID_JUSTIFICATION_WORDS:
|
|
470
|
-
just = ensure_xy_location(just)
|
|
470
|
+
just = ensure_xy_location(just)
|
|
471
471
|
elif isinstance(just, (float, int)):
|
|
472
472
|
just = (just, just)
|
|
473
473
|
return just[idx]
|
plotnine/helpers.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
|
|
9
|
+
from plotnine import ggplot
|
|
10
|
+
|
|
11
|
+
__all__ = ("get_aesthetic_limits",)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_aesthetic_limits(
|
|
15
|
+
plot: ggplot,
|
|
16
|
+
ae: str,
|
|
17
|
+
) -> (
|
|
18
|
+
tuple[float, float]
|
|
19
|
+
| Sequence[str]
|
|
20
|
+
| list[tuple[float]]
|
|
21
|
+
| list[Sequence[str]]
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Get the limits of an aesthetic
|
|
25
|
+
|
|
26
|
+
These are the limits before they are expanded.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
plot :
|
|
31
|
+
ggplot object
|
|
32
|
+
|
|
33
|
+
ae :
|
|
34
|
+
Name of aesthetic
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
out :
|
|
39
|
+
The limits of the aesthetic. If the plot is facetted, (has many
|
|
40
|
+
panels), it is a sequence of limits, one for each panel.
|
|
41
|
+
"""
|
|
42
|
+
plot = deepcopy(plot)
|
|
43
|
+
plot._build()
|
|
44
|
+
limits = [
|
|
45
|
+
getattr(panel, ae).limits
|
|
46
|
+
for panel in plot._build_objs.layout.panel_params
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
return limits[0] if len(limits) == 1 else limits
|
plotnine/iapi.py
CHANGED
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import itertools
|
|
11
11
|
from copy import copy
|
|
12
12
|
from dataclasses import dataclass, field, fields
|
|
13
|
+
from functools import cached_property
|
|
13
14
|
from typing import TYPE_CHECKING
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
@@ -19,11 +20,14 @@ if TYPE_CHECKING:
|
|
|
19
20
|
from matplotlib.figure import Figure
|
|
20
21
|
|
|
21
22
|
from plotnine.scales.scale import scale
|
|
23
|
+
from plotnine.themes.elements.margin import margin
|
|
22
24
|
from plotnine.typing import (
|
|
23
25
|
CoordRange,
|
|
24
26
|
FloatArrayLike,
|
|
27
|
+
HorizontalJustification,
|
|
25
28
|
ScaledAestheticsName,
|
|
26
29
|
StripPosition,
|
|
30
|
+
VerticalJustification,
|
|
27
31
|
)
|
|
28
32
|
|
|
29
33
|
from ._mpl.offsetbox import FlexibleAnchoredOffsetbox
|
|
@@ -76,6 +80,7 @@ class labels_view:
|
|
|
76
80
|
title: Optional[str] = None
|
|
77
81
|
caption: Optional[str] = None
|
|
78
82
|
subtitle: Optional[str] = None
|
|
83
|
+
tag: Optional[str] = None
|
|
79
84
|
|
|
80
85
|
def update(self, other: labels_view):
|
|
81
86
|
"""
|
|
@@ -228,13 +233,27 @@ class strip_draw_info:
|
|
|
228
233
|
Information required to draw strips
|
|
229
234
|
"""
|
|
230
235
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
bg_x: float
|
|
237
|
+
"""Left of the strip background in transAxes"""
|
|
238
|
+
|
|
239
|
+
bg_y: float
|
|
240
|
+
"""Bottom of the strip background in transAxes"""
|
|
241
|
+
|
|
242
|
+
ha: HorizontalJustification | float
|
|
243
|
+
"""Horizontal justification of strip text within the background"""
|
|
244
|
+
|
|
245
|
+
va: VerticalJustification | float
|
|
246
|
+
"""Vertical justification of strip text within the background"""
|
|
247
|
+
|
|
248
|
+
bg_width: float
|
|
249
|
+
"""Width of the strip background in transAxes"""
|
|
250
|
+
|
|
251
|
+
bg_height: float
|
|
252
|
+
"""Height of the strip background in transAxes"""
|
|
253
|
+
|
|
254
|
+
margin: margin
|
|
255
|
+
"""Strip text margin with the units in lines"""
|
|
256
|
+
|
|
238
257
|
strip_align: float
|
|
239
258
|
position: StripPosition
|
|
240
259
|
label: str
|
|
@@ -242,6 +261,13 @@ class strip_draw_info:
|
|
|
242
261
|
rotation: float
|
|
243
262
|
layout: layout_details
|
|
244
263
|
|
|
264
|
+
@cached_property
|
|
265
|
+
def is_oneline(self) -> bool:
|
|
266
|
+
"""
|
|
267
|
+
Whether the strip text is a single line
|
|
268
|
+
"""
|
|
269
|
+
return len(self.label.split("\n")) == 1
|
|
270
|
+
|
|
245
271
|
|
|
246
272
|
@dataclass
|
|
247
273
|
class strip_label_details:
|
plotnine/labels.py
CHANGED
|
@@ -90,6 +90,11 @@ class labs:
|
|
|
90
90
|
The caption at the bottom of the plot.
|
|
91
91
|
"""
|
|
92
92
|
|
|
93
|
+
tag: str | None = None
|
|
94
|
+
"""
|
|
95
|
+
A plot tag
|
|
96
|
+
"""
|
|
97
|
+
|
|
93
98
|
def __post_init__(self):
|
|
94
99
|
kwargs: dict[str, str] = {
|
|
95
100
|
f.name: value
|
|
@@ -98,12 +103,12 @@ class labs:
|
|
|
98
103
|
}
|
|
99
104
|
self.labels = labels_view(**rename_aesthetics(kwargs))
|
|
100
105
|
|
|
101
|
-
def __radd__(self,
|
|
106
|
+
def __radd__(self, other: p9.ggplot) -> p9.ggplot:
|
|
102
107
|
"""
|
|
103
108
|
Add labels to ggplot object
|
|
104
109
|
"""
|
|
105
|
-
|
|
106
|
-
return
|
|
110
|
+
other.labels.update(self.labels)
|
|
111
|
+
return other
|
|
107
112
|
|
|
108
113
|
|
|
109
114
|
class xlab(labs):
|
plotnine/layer.py
CHANGED
|
@@ -125,16 +125,16 @@ class layer:
|
|
|
125
125
|
lkwargs[param] = geom.DEFAULT_PARAMS[param]
|
|
126
126
|
return layer(**lkwargs)
|
|
127
127
|
|
|
128
|
-
def __radd__(self,
|
|
128
|
+
def __radd__(self, other: ggplot) -> ggplot:
|
|
129
129
|
"""
|
|
130
130
|
Add layer to ggplot object
|
|
131
131
|
"""
|
|
132
132
|
try:
|
|
133
|
-
|
|
133
|
+
other.layers.append(self)
|
|
134
134
|
except AttributeError as e:
|
|
135
|
-
msg = f"Cannot add layer to object of type {type(
|
|
135
|
+
msg = f"Cannot add layer to object of type {type(other)!r}"
|
|
136
136
|
raise PlotnineError(msg) from e
|
|
137
|
-
return
|
|
137
|
+
return other
|
|
138
138
|
|
|
139
139
|
def __deepcopy__(self, memo: dict[Any, Any]) -> layer:
|
|
140
140
|
"""
|
plotnine/mapping/_env.py
CHANGED
|
@@ -122,7 +122,7 @@ class Environment:
|
|
|
122
122
|
def __hash__(self):
|
|
123
123
|
return hash((Environment, tuple(self._namespace_ids())))
|
|
124
124
|
|
|
125
|
-
def __getstate__(
|
|
125
|
+
def __getstate__(self):
|
|
126
126
|
"""
|
|
127
127
|
Return state with no namespaces
|
|
128
128
|
"""
|
|
@@ -198,7 +198,7 @@ class StackedLookup(MutableMapping):
|
|
|
198
198
|
def __repr__(self):
|
|
199
199
|
return f"{self.__class__.__name__}({self.stack})"
|
|
200
200
|
|
|
201
|
-
def __getstate__(
|
|
201
|
+
def __getstate__(self):
|
|
202
202
|
"""
|
|
203
203
|
Return state with no namespace
|
|
204
204
|
"""
|
|
@@ -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
|
|
@@ -298,14 +282,14 @@ class aes(Dict[str, Any]):
|
|
|
298
282
|
|
|
299
283
|
return result
|
|
300
284
|
|
|
301
|
-
def __radd__(self,
|
|
285
|
+
def __radd__(self, other):
|
|
302
286
|
"""
|
|
303
287
|
Add aesthetic mappings to ggplot
|
|
304
288
|
"""
|
|
305
289
|
self = deepcopy(self)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return
|
|
290
|
+
other.mapping.update(self)
|
|
291
|
+
other.labels.update(make_labels(self))
|
|
292
|
+
return other
|
|
309
293
|
|
|
310
294
|
def copy(self):
|
|
311
295
|
return aes(**self)
|
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 = {}
|
plotnine/options.py
CHANGED
|
@@ -10,14 +10,11 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from plotnine import theme
|
|
11
11
|
from plotnine.typing import FigureFormat
|
|
12
12
|
|
|
13
|
-
close_all_figures = False
|
|
14
|
-
"""
|
|
15
|
-
Development flag, e.g. set to `True` to prevent
|
|
16
|
-
the queuing up of figures when errors happen.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
13
|
current_theme: Optional[theme | Type[theme]] = None
|
|
20
|
-
"""Theme used when none is added to the ggplot object
|
|
14
|
+
"""Theme used when none is added to the ggplot object
|
|
15
|
+
|
|
16
|
+
Another way to do it, to set a default theme using `theme_set()`.
|
|
17
|
+
"""
|
|
21
18
|
|
|
22
19
|
base_family: str = "sans-serif"
|
|
23
20
|
"""
|
|
@@ -77,6 +74,11 @@ def get_option(name: str) -> Any:
|
|
|
77
74
|
----------
|
|
78
75
|
name :
|
|
79
76
|
Name of the option
|
|
77
|
+
|
|
78
|
+
Notes
|
|
79
|
+
-----
|
|
80
|
+
See [reference](/reference/#options) for a list of all the available
|
|
81
|
+
options.
|
|
80
82
|
"""
|
|
81
83
|
d = globals()
|
|
82
84
|
|
|
@@ -103,6 +105,11 @@ def set_option(name: str, value: Any) -> Any:
|
|
|
103
105
|
-------
|
|
104
106
|
:
|
|
105
107
|
Old value of the option
|
|
108
|
+
|
|
109
|
+
Notes
|
|
110
|
+
-----
|
|
111
|
+
See [reference](/reference/#options) for a list of all the available
|
|
112
|
+
options.
|
|
106
113
|
"""
|
|
107
114
|
d = globals()
|
|
108
115
|
|