plotnine 0.15.0.dev2__py3-none-any.whl → 0.15.1__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 +2 -0
- plotnine/_mpl/layout_manager/_engine.py +1 -1
- plotnine/_mpl/layout_manager/_layout_items.py +128 -83
- plotnine/_mpl/layout_manager/_layout_tree.py +761 -310
- plotnine/_mpl/layout_manager/_spaces.py +320 -103
- plotnine/_mpl/patches.py +70 -34
- plotnine/_mpl/text.py +144 -63
- plotnine/_mpl/utils.py +1 -1
- plotnine/_utils/__init__.py +50 -107
- plotnine/_utils/context.py +78 -2
- plotnine/_utils/ipython.py +35 -51
- plotnine/_utils/quarto.py +21 -0
- plotnine/_utils/yippie.py +115 -0
- plotnine/composition/__init__.py +11 -0
- plotnine/composition/_beside.py +55 -0
- plotnine/composition/_compose.py +471 -0
- plotnine/composition/_plot_spacer.py +60 -0
- plotnine/composition/_stack.py +55 -0
- plotnine/coords/coord.py +3 -3
- plotnine/data/__init__.py +31 -0
- plotnine/data/anscombe-quartet.csv +45 -0
- plotnine/doctools.py +4 -4
- plotnine/facets/facet.py +4 -4
- plotnine/facets/strips.py +17 -28
- plotnine/geoms/annotate.py +13 -13
- plotnine/geoms/annotation_logticks.py +7 -8
- plotnine/geoms/annotation_stripes.py +6 -6
- plotnine/geoms/geom.py +60 -27
- plotnine/geoms/geom_abline.py +3 -2
- plotnine/geoms/geom_area.py +2 -2
- plotnine/geoms/geom_bar.py +11 -2
- plotnine/geoms/geom_bin_2d.py +6 -2
- plotnine/geoms/geom_blank.py +0 -3
- plotnine/geoms/geom_boxplot.py +8 -4
- plotnine/geoms/geom_col.py +8 -2
- plotnine/geoms/geom_count.py +6 -2
- plotnine/geoms/geom_crossbar.py +3 -3
- plotnine/geoms/geom_density_2d.py +6 -2
- plotnine/geoms/geom_dotplot.py +2 -2
- plotnine/geoms/geom_errorbar.py +2 -2
- plotnine/geoms/geom_errorbarh.py +2 -2
- plotnine/geoms/geom_histogram.py +1 -1
- plotnine/geoms/geom_hline.py +3 -2
- plotnine/geoms/geom_linerange.py +2 -2
- plotnine/geoms/geom_map.py +5 -5
- plotnine/geoms/geom_path.py +11 -12
- plotnine/geoms/geom_point.py +4 -5
- plotnine/geoms/geom_pointdensity.py +4 -0
- plotnine/geoms/geom_pointrange.py +3 -5
- plotnine/geoms/geom_polygon.py +2 -3
- plotnine/geoms/geom_qq.py +4 -0
- plotnine/geoms/geom_qq_line.py +4 -0
- plotnine/geoms/geom_quantile.py +4 -0
- plotnine/geoms/geom_raster.py +4 -5
- plotnine/geoms/geom_rect.py +3 -4
- plotnine/geoms/geom_ribbon.py +7 -7
- plotnine/geoms/geom_rug.py +1 -1
- plotnine/geoms/geom_segment.py +2 -2
- plotnine/geoms/geom_sina.py +3 -3
- plotnine/geoms/geom_smooth.py +7 -3
- plotnine/geoms/geom_step.py +2 -2
- plotnine/geoms/geom_text.py +2 -3
- plotnine/geoms/geom_violin.py +28 -8
- plotnine/geoms/geom_vline.py +3 -2
- plotnine/ggplot.py +64 -85
- plotnine/guides/guide.py +7 -10
- plotnine/guides/guide_colorbar.py +3 -3
- plotnine/guides/guide_legend.py +3 -3
- plotnine/guides/guides.py +6 -6
- plotnine/helpers.py +49 -0
- plotnine/iapi.py +28 -5
- plotnine/labels.py +3 -3
- plotnine/layer.py +36 -19
- plotnine/mapping/_atomic.py +178 -0
- plotnine/mapping/_env.py +13 -2
- plotnine/mapping/_eval_environment.py +85 -0
- plotnine/mapping/aes.py +91 -72
- plotnine/mapping/evaluation.py +7 -65
- plotnine/scales/__init__.py +2 -0
- plotnine/scales/limits.py +7 -7
- plotnine/scales/scale.py +3 -3
- plotnine/scales/scale_color.py +82 -18
- plotnine/scales/scale_continuous.py +6 -4
- plotnine/scales/scale_datetime.py +28 -14
- plotnine/scales/scale_discrete.py +1 -1
- plotnine/scales/scale_identity.py +21 -2
- plotnine/scales/scale_manual.py +8 -2
- plotnine/scales/scale_xy.py +2 -2
- plotnine/stats/binning.py +4 -1
- plotnine/stats/smoothers.py +23 -36
- plotnine/stats/stat.py +20 -32
- plotnine/stats/stat_bin.py +6 -5
- plotnine/stats/stat_bin_2d.py +11 -9
- plotnine/stats/stat_bindot.py +13 -16
- plotnine/stats/stat_boxplot.py +6 -6
- plotnine/stats/stat_count.py +6 -9
- plotnine/stats/stat_density.py +7 -10
- plotnine/stats/stat_density_2d.py +12 -8
- plotnine/stats/stat_ecdf.py +7 -6
- plotnine/stats/stat_ellipse.py +9 -6
- plotnine/stats/stat_function.py +10 -8
- plotnine/stats/stat_hull.py +6 -3
- plotnine/stats/stat_identity.py +5 -2
- plotnine/stats/stat_pointdensity.py +5 -7
- plotnine/stats/stat_qq.py +46 -20
- plotnine/stats/stat_qq_line.py +16 -11
- plotnine/stats/stat_quantile.py +15 -9
- plotnine/stats/stat_sina.py +45 -14
- plotnine/stats/stat_smooth.py +8 -10
- plotnine/stats/stat_sum.py +5 -2
- plotnine/stats/stat_summary.py +7 -10
- plotnine/stats/stat_summary_bin.py +11 -14
- plotnine/stats/stat_unique.py +5 -2
- plotnine/stats/stat_ydensity.py +8 -11
- plotnine/themes/elements/__init__.py +2 -1
- plotnine/themes/elements/element_line.py +17 -9
- plotnine/themes/elements/margin.py +64 -1
- plotnine/themes/theme.py +9 -1
- 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 +6 -5
- plotnine/themes/theme_light.py +1 -1
- plotnine/themes/theme_matplotlib.py +5 -5
- plotnine/themes/theme_seaborn.py +7 -4
- plotnine/themes/theme_void.py +9 -8
- plotnine/themes/theme_xkcd.py +0 -1
- plotnine/themes/themeable.py +110 -32
- plotnine/typing.py +17 -6
- plotnine/watermark.py +3 -3
- {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/METADATA +13 -6
- plotnine-0.15.1.dist-info/RECORD +221 -0
- {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/WHEEL +1 -1
- plotnine/plot_composition/__init__.py +0 -10
- plotnine/plot_composition/_compose.py +0 -436
- plotnine/plot_composition/_spacer.py +0 -32
- plotnine-0.15.0.dev2.dist-info/RECORD +0 -214
- /plotnine/{plot_composition → composition}/_plotspec.py +0 -0
- {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.0.dev2.dist-info → plotnine-0.15.1.dist-info}/top_level.txt +0 -0
plotnine/layer.py
CHANGED
|
@@ -8,7 +8,7 @@ import pandas as pd
|
|
|
8
8
|
|
|
9
9
|
from ._utils import array_kind, check_required_aesthetics, ninteraction
|
|
10
10
|
from .exceptions import PlotnineError
|
|
11
|
-
from .mapping.aes import NO_GROUP, SCALED_AESTHETICS, aes
|
|
11
|
+
from .mapping.aes import NO_GROUP, SCALED_AESTHETICS, aes, make_labels
|
|
12
12
|
from .mapping.evaluation import evaluate, stage
|
|
13
13
|
|
|
14
14
|
if typing.TYPE_CHECKING:
|
|
@@ -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
|
"""
|
|
@@ -163,6 +163,7 @@ class layer:
|
|
|
163
163
|
self._make_layer_data(plot.data)
|
|
164
164
|
self._make_layer_mapping(plot.mapping)
|
|
165
165
|
self._make_layer_environments(plot.environment)
|
|
166
|
+
self._share_layer_params()
|
|
166
167
|
|
|
167
168
|
def _make_layer_data(self, plot_data: DataLike | None):
|
|
168
169
|
"""
|
|
@@ -250,6 +251,14 @@ class layer:
|
|
|
250
251
|
self.geom.environment = plot_environment
|
|
251
252
|
self.stat.environment = plot_environment
|
|
252
253
|
|
|
254
|
+
def _share_layer_params(self):
|
|
255
|
+
"""
|
|
256
|
+
Pass necessary layer parameters to the geom
|
|
257
|
+
"""
|
|
258
|
+
self.geom.params["zorder"] = self.zorder
|
|
259
|
+
self.geom.params["raster"] = self.raster
|
|
260
|
+
self.geom.params["inherit_aes"] = self.inherit_aes
|
|
261
|
+
|
|
253
262
|
def compute_aesthetics(self, plot: ggplot):
|
|
254
263
|
"""
|
|
255
264
|
Return a dataframe where the columns match the aesthetic mappings
|
|
@@ -278,10 +287,10 @@ class layer:
|
|
|
278
287
|
if not len(data):
|
|
279
288
|
return
|
|
280
289
|
|
|
281
|
-
|
|
290
|
+
self.stat.setup_params(data)
|
|
282
291
|
data = self.stat.use_defaults(data)
|
|
283
292
|
data = self.stat.setup_data(data)
|
|
284
|
-
data = self.stat.compute_layer(data,
|
|
293
|
+
data = self.stat.compute_layer(data, layout)
|
|
285
294
|
self.data = data
|
|
286
295
|
|
|
287
296
|
def map_statistic(self, plot: ggplot):
|
|
@@ -289,7 +298,9 @@ class layer:
|
|
|
289
298
|
Mapping aesthetics to computed statistics
|
|
290
299
|
"""
|
|
291
300
|
# Mixin default stat aesthetic mappings
|
|
292
|
-
calculated =
|
|
301
|
+
calculated = (
|
|
302
|
+
aes(**self.stat.DEFAULT_AES)._calculated | self.mapping._calculated
|
|
303
|
+
)
|
|
293
304
|
|
|
294
305
|
if not len(self.data) or not calculated:
|
|
295
306
|
return
|
|
@@ -320,6 +331,9 @@ class layer:
|
|
|
320
331
|
if len(data) == 0:
|
|
321
332
|
return
|
|
322
333
|
|
|
334
|
+
self.geom.params.update(self.stat.params)
|
|
335
|
+
self.geom.setup_params(data)
|
|
336
|
+
self.geom.setup_aes_params(data)
|
|
323
337
|
data = self.geom.setup_data(data)
|
|
324
338
|
|
|
325
339
|
check_required_aesthetics(
|
|
@@ -357,14 +371,10 @@ class layer:
|
|
|
357
371
|
coord : coord
|
|
358
372
|
Type of coordinate axes
|
|
359
373
|
"""
|
|
360
|
-
params = copy(self.geom.params)
|
|
361
|
-
params.update(self.stat.params)
|
|
362
|
-
params["zorder"] = self.zorder
|
|
363
|
-
params["raster"] = self.raster
|
|
364
374
|
self.data = self.geom.handle_na(self.data)
|
|
365
375
|
# At this point each layer must have the data
|
|
366
376
|
# that is created by the plot build process
|
|
367
|
-
self.geom.draw_layer(self.data, layout, coord
|
|
377
|
+
self.geom.draw_layer(self.data, layout, coord)
|
|
368
378
|
|
|
369
379
|
def use_defaults(
|
|
370
380
|
self,
|
|
@@ -399,7 +409,14 @@ class layer:
|
|
|
399
409
|
"""
|
|
400
410
|
Prepare/modify data for plotting
|
|
401
411
|
"""
|
|
402
|
-
self.stat.finish_layer(self.data
|
|
412
|
+
self.stat.finish_layer(self.data)
|
|
413
|
+
|
|
414
|
+
def update_labels(self, plot: ggplot):
|
|
415
|
+
"""
|
|
416
|
+
Update label data for the ggplot from the mappings in this layer
|
|
417
|
+
"""
|
|
418
|
+
plot.labels.add_defaults(self.mapping.labels)
|
|
419
|
+
plot.labels.add_defaults(make_labels(self.stat.DEFAULT_AES))
|
|
403
420
|
|
|
404
421
|
|
|
405
422
|
class Layers(List[layer]):
|
|
@@ -450,7 +467,9 @@ class Layers(List[layer]):
|
|
|
450
467
|
return [l.data for l in self]
|
|
451
468
|
|
|
452
469
|
def setup(self, plot: ggplot):
|
|
453
|
-
|
|
470
|
+
# If zorder is 0, it is left to MPL
|
|
471
|
+
for i, l in enumerate(self, start=1):
|
|
472
|
+
l.zorder = i
|
|
454
473
|
l.setup(plot)
|
|
455
474
|
|
|
456
475
|
def setup_data(self):
|
|
@@ -458,9 +477,7 @@ class Layers(List[layer]):
|
|
|
458
477
|
l.setup_data()
|
|
459
478
|
|
|
460
479
|
def draw(self, layout: Layout, coord: coord):
|
|
461
|
-
|
|
462
|
-
for i, l in enumerate(self, start=1):
|
|
463
|
-
l.zorder = i
|
|
480
|
+
for l in self:
|
|
464
481
|
l.draw(layout, coord)
|
|
465
482
|
|
|
466
483
|
def compute_aesthetics(self, plot: ggplot):
|
|
@@ -501,7 +518,7 @@ class Layers(List[layer]):
|
|
|
501
518
|
|
|
502
519
|
def update_labels(self, plot: ggplot):
|
|
503
520
|
for l in self:
|
|
504
|
-
|
|
521
|
+
l.update_labels(plot)
|
|
505
522
|
|
|
506
523
|
|
|
507
524
|
def add_group(data: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Generic,
|
|
8
|
+
Literal,
|
|
9
|
+
Sequence,
|
|
10
|
+
TypeAlias,
|
|
11
|
+
TypeVar,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from mizani._colors.utils import is_color_tuple
|
|
16
|
+
|
|
17
|
+
# NOTE:For now we shall use these class privately and not list them
|
|
18
|
+
# in documentation. We can't deal with assigning Sequence[ae_value]
|
|
19
|
+
# to an aesthetic.
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
"linetype",
|
|
23
|
+
"color",
|
|
24
|
+
"colour",
|
|
25
|
+
"fill",
|
|
26
|
+
"shape",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
|
|
31
|
+
ShapeType: TypeAlias = (
|
|
32
|
+
str | tuple[int, Literal[0, 1, 2], float] | Sequence[tuple[float, float]]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ae_value(Generic[T]):
|
|
38
|
+
"""
|
|
39
|
+
Atomic aesthetic value
|
|
40
|
+
|
|
41
|
+
The goal of this base class is simplify working with the more complex
|
|
42
|
+
aesthetic values. e.g. if a value is a tuple, we don't want it to be
|
|
43
|
+
seen as a sequence of values when assigning it to a dataframe column.
|
|
44
|
+
The subclasses should be able to recognise valid aesthetic values and
|
|
45
|
+
repeat (using multiplication) the value any number of times.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
value: T
|
|
49
|
+
|
|
50
|
+
def __mul__(self, n: int) -> Sequence[T]:
|
|
51
|
+
"""
|
|
52
|
+
Repeat value n times
|
|
53
|
+
"""
|
|
54
|
+
return [self.value] * n
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class linetype(ae_value[str | tuple]):
|
|
59
|
+
"""
|
|
60
|
+
A single linetype value
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __post_init__(self):
|
|
64
|
+
value = self.value
|
|
65
|
+
named = {
|
|
66
|
+
" ",
|
|
67
|
+
"",
|
|
68
|
+
"-",
|
|
69
|
+
"--",
|
|
70
|
+
"-.",
|
|
71
|
+
":",
|
|
72
|
+
"None",
|
|
73
|
+
"none",
|
|
74
|
+
"dashdot",
|
|
75
|
+
"dashed",
|
|
76
|
+
"dotted",
|
|
77
|
+
"solid",
|
|
78
|
+
}
|
|
79
|
+
if self.value in named:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# tuple of the form (offset, (on, off, on, off, ...))
|
|
83
|
+
# e.g (0, (1, 2))
|
|
84
|
+
if (
|
|
85
|
+
isinstance(value, tuple)
|
|
86
|
+
and isinstance(value[0], int)
|
|
87
|
+
and isinstance(value[1], tuple)
|
|
88
|
+
and len(value[1]) % 2 == 0
|
|
89
|
+
and all(isinstance(x, int) for x in value[1])
|
|
90
|
+
):
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
raise ValueError(f"{value} is not a known linetype.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class color(ae_value[str | tuple]):
|
|
98
|
+
"""
|
|
99
|
+
A single color value
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __post_init__(self):
|
|
103
|
+
if isinstance(self.value, str):
|
|
104
|
+
return
|
|
105
|
+
elif is_color_tuple(self.value):
|
|
106
|
+
self.value = tuple(self.value)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
raise ValueError(f"{self.value} is not a known color.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
colour = color
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class fill(color):
|
|
117
|
+
"""
|
|
118
|
+
A single color value
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class shape(ae_value[ShapeType]):
|
|
124
|
+
"""
|
|
125
|
+
A single shape value
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __post_init__(self):
|
|
129
|
+
from matplotlib.path import Path
|
|
130
|
+
|
|
131
|
+
from ..scales.scale_shape import FILLED_SHAPES, UNFILLED_SHAPES
|
|
132
|
+
|
|
133
|
+
value = self.value
|
|
134
|
+
|
|
135
|
+
with suppress(TypeError):
|
|
136
|
+
if value in (FILLED_SHAPES | UNFILLED_SHAPES):
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if isinstance(value, Path):
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# tuple of the form (numsides, style, angle)
|
|
143
|
+
# where style is in the range [0, 3]
|
|
144
|
+
# e.g (4, 1, 45)
|
|
145
|
+
if (
|
|
146
|
+
isinstance(value, tuple)
|
|
147
|
+
and len(value) == 3
|
|
148
|
+
and isinstance(value[0], int)
|
|
149
|
+
and value[1] in (0, 1, 2)
|
|
150
|
+
and isinstance(value[2], (float, int))
|
|
151
|
+
):
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if is_shape_points(value):
|
|
155
|
+
self.value = tuple(value) # pyright: ignore[reportAttributeAccessIssue]
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
raise ValueError(f"{value} is not a known shape.")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_shape_points(obj: Any) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Return True if obj is like Sequence[tuple[float, float]]
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def is_numeric(obj) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Return True if obj is a python or numpy float or integer
|
|
169
|
+
"""
|
|
170
|
+
return isinstance(obj, (float, int, np.floating, np.integer))
|
|
171
|
+
|
|
172
|
+
if not iter(obj):
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
return all(is_numeric(a) and is_numeric(b) for a, b in obj)
|
|
177
|
+
except (ValueError, TypeError):
|
|
178
|
+
return False
|
plotnine/mapping/_env.py
CHANGED
|
@@ -10,6 +10,8 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from collections.abc import Iterator
|
|
11
11
|
from typing import Any, Hashable, Protocol, Self
|
|
12
12
|
|
|
13
|
+
from patsy.eval import EvalEnvironment
|
|
14
|
+
|
|
13
15
|
class SupportsGetItem(Protocol):
|
|
14
16
|
"""
|
|
15
17
|
Supports __getitem__
|
|
@@ -110,6 +112,15 @@ class Environment:
|
|
|
110
112
|
finally:
|
|
111
113
|
del frame
|
|
112
114
|
|
|
115
|
+
def to_patsy_env(self) -> EvalEnvironment:
|
|
116
|
+
"""
|
|
117
|
+
Convert a plotnine environment to a patsy environment
|
|
118
|
+
"""
|
|
119
|
+
from patsy.eval import EvalEnvironment
|
|
120
|
+
|
|
121
|
+
eval_env = EvalEnvironment(self.namespaces)
|
|
122
|
+
return eval_env
|
|
123
|
+
|
|
113
124
|
def _namespace_ids(self):
|
|
114
125
|
return [id(n) for n in self.namespaces]
|
|
115
126
|
|
|
@@ -122,7 +133,7 @@ class Environment:
|
|
|
122
133
|
def __hash__(self):
|
|
123
134
|
return hash((Environment, tuple(self._namespace_ids())))
|
|
124
135
|
|
|
125
|
-
def __getstate__(
|
|
136
|
+
def __getstate__(self):
|
|
126
137
|
"""
|
|
127
138
|
Return state with no namespaces
|
|
128
139
|
"""
|
|
@@ -198,7 +209,7 @@ class StackedLookup(MutableMapping):
|
|
|
198
209
|
def __repr__(self):
|
|
199
210
|
return f"{self.__class__.__name__}({self.stack})"
|
|
200
211
|
|
|
201
|
-
def __getstate__(
|
|
212
|
+
def __getstate__(self):
|
|
202
213
|
"""
|
|
203
214
|
Return state with no namespace
|
|
204
215
|
"""
|
|
@@ -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) # pyright: ignore[reportArgumentType]
|
|
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,20 +1,21 @@
|
|
|
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
8
|
from functools import cached_property
|
|
10
|
-
from typing import Any, Dict
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Dict
|
|
11
10
|
|
|
11
|
+
import numpy as np
|
|
12
12
|
import pandas as pd
|
|
13
|
+
from mizani._colors.utils import is_color_tuple
|
|
13
14
|
|
|
14
15
|
from ..iapi import labels_view
|
|
15
16
|
from .evaluation import after_stat, stage
|
|
16
17
|
|
|
17
|
-
if
|
|
18
|
+
if TYPE_CHECKING:
|
|
18
19
|
from typing import Protocol, TypeVar
|
|
19
20
|
|
|
20
21
|
class ColorOrColour(Protocol):
|
|
@@ -172,27 +173,11 @@ class aes(Dict[str, Any]):
|
|
|
172
173
|
ggplot(df, aes(x="df.index", y="np.sin(gam ma)"))
|
|
173
174
|
```
|
|
174
175
|
|
|
175
|
-
`aes` has 2 internal
|
|
176
|
-
|
|
176
|
+
`aes` has 2 internal functions that you can use in your expressions
|
|
177
|
+
when transforming the variables.
|
|
177
178
|
|
|
178
|
-
1.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
```python
|
|
182
|
-
ggplot(mtcars, aes(x="factor(cyl)")) + geom_bar()
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
2. `reorder` - This function changes the order of first variable
|
|
186
|
-
based on values of the second variable:
|
|
187
|
-
|
|
188
|
-
```python
|
|
189
|
-
df = pd.DataFrame({
|
|
190
|
-
"x": ["b", "d", "c", "a"],
|
|
191
|
-
"y": [1, 2, 3, 4]
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
ggplot(df, aes("reorder(x, y)", "y")) + geom_col()
|
|
195
|
-
```
|
|
179
|
+
1. [](:func:`~plotnine.mapping._eval_environment.factor`)
|
|
180
|
+
1. [](:func:`~plotnine.mapping._eval_environment.reorder`)
|
|
196
181
|
|
|
197
182
|
**The group aesthetic**
|
|
198
183
|
|
|
@@ -299,14 +284,20 @@ class aes(Dict[str, Any]):
|
|
|
299
284
|
|
|
300
285
|
return result
|
|
301
286
|
|
|
302
|
-
def __radd__(self,
|
|
287
|
+
def __radd__(self, other):
|
|
303
288
|
"""
|
|
304
289
|
Add aesthetic mappings to ggplot
|
|
305
290
|
"""
|
|
306
291
|
self = deepcopy(self)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
292
|
+
other.mapping.update(self)
|
|
293
|
+
return other
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def labels(self) -> labels_view:
|
|
297
|
+
"""
|
|
298
|
+
The labels for this mapping
|
|
299
|
+
"""
|
|
300
|
+
return make_labels(self)
|
|
310
301
|
|
|
311
302
|
def copy(self):
|
|
312
303
|
return aes(**self)
|
|
@@ -555,23 +546,23 @@ def make_labels(mapping: dict[str, Any] | aes) -> labels_view:
|
|
|
555
546
|
)
|
|
556
547
|
|
|
557
548
|
|
|
558
|
-
|
|
549
|
+
class RepeatAesthetic:
|
|
559
550
|
"""
|
|
560
|
-
|
|
551
|
+
Repeat an Aeshetic a given number of times
|
|
561
552
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
value :
|
|
565
|
-
Value to check
|
|
566
|
-
ae :
|
|
567
|
-
Aesthetic name
|
|
553
|
+
The methods in this class know how to create sequences of aesthetics
|
|
554
|
+
whose values may not be scalar.
|
|
568
555
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
valid.
|
|
556
|
+
Some aesthetics may have valid values that are not scalar. e.g.
|
|
557
|
+
sequences. Inserting one of such a value in a dataframe as a column
|
|
558
|
+
would either lead to the wrong input or fail. The s
|
|
573
559
|
"""
|
|
574
|
-
|
|
560
|
+
|
|
561
|
+
@staticmethod
|
|
562
|
+
def linetype(value: Any, n: int) -> Sequence[Any]:
|
|
563
|
+
"""
|
|
564
|
+
Repeat linetypes
|
|
565
|
+
"""
|
|
575
566
|
named = {
|
|
576
567
|
"solid",
|
|
577
568
|
"dashed",
|
|
@@ -586,47 +577,75 @@ def is_valid_aesthetic(value: Any, ae: str) -> bool:
|
|
|
586
577
|
"",
|
|
587
578
|
}
|
|
588
579
|
if value in named:
|
|
589
|
-
return
|
|
580
|
+
return [value] * n
|
|
590
581
|
|
|
591
582
|
# tuple of the form (offset, (on, off, on, off, ...))
|
|
592
583
|
# e.g (0, (1, 2))
|
|
593
|
-
|
|
594
|
-
isinstance(value, tuple)
|
|
595
|
-
isinstance(value[0], int)
|
|
596
|
-
isinstance(value[1], tuple)
|
|
597
|
-
len(value[1]) % 2 == 0
|
|
598
|
-
all(isinstance(x, int) for x in value[1])
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
584
|
+
if (
|
|
585
|
+
isinstance(value, tuple)
|
|
586
|
+
and isinstance(value[0], int)
|
|
587
|
+
and isinstance(value[1], tuple)
|
|
588
|
+
and len(value[1]) % 2 == 0
|
|
589
|
+
and all(isinstance(x, int) for x in value[1])
|
|
590
|
+
):
|
|
591
|
+
return [value] * n
|
|
592
|
+
|
|
593
|
+
raise ValueError(f"{value} is not a known linetype.")
|
|
594
|
+
|
|
595
|
+
@staticmethod
|
|
596
|
+
def color(value: Any, n: int) -> Sequence[Any]:
|
|
597
|
+
"""
|
|
598
|
+
Repeat colors
|
|
599
|
+
"""
|
|
603
600
|
if isinstance(value, str):
|
|
604
|
-
return
|
|
601
|
+
return [value] * n
|
|
602
|
+
if is_color_tuple(value):
|
|
603
|
+
return [tuple(value)] * n
|
|
604
|
+
|
|
605
|
+
raise ValueError(f"{value} is not a known color.")
|
|
605
606
|
|
|
607
|
+
fill = color
|
|
608
|
+
|
|
609
|
+
@staticmethod
|
|
610
|
+
def shape(value: Any, n: int) -> Any:
|
|
611
|
+
"""
|
|
612
|
+
Repeat shapes
|
|
613
|
+
"""
|
|
614
|
+
if isinstance(value, str):
|
|
615
|
+
return [value] * n
|
|
606
616
|
# tuple of the form (numsides, style, angle)
|
|
607
617
|
# where style is in the range [0, 3]
|
|
608
618
|
# e.g (4, 1, 45)
|
|
609
|
-
|
|
610
|
-
isinstance(value, tuple)
|
|
611
|
-
all(isinstance(x, int) for x in value)
|
|
612
|
-
0 <= value[1] < 3
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
0 <= x <= 1 for x in value
|
|
622
|
-
):
|
|
623
|
-
return True
|
|
624
|
-
return False
|
|
619
|
+
if (
|
|
620
|
+
isinstance(value, tuple)
|
|
621
|
+
and all(isinstance(x, int) for x in value)
|
|
622
|
+
and 0 <= value[1] < 3
|
|
623
|
+
):
|
|
624
|
+
return [value] * n
|
|
625
|
+
|
|
626
|
+
if is_shape_points(value):
|
|
627
|
+
return [tuple(value)] * n
|
|
628
|
+
|
|
629
|
+
raise ValueError(f"{value} is not a know shape.")
|
|
630
|
+
|
|
625
631
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
632
|
+
def is_shape_points(obj: Any) -> bool:
|
|
633
|
+
"""
|
|
634
|
+
Return True if obj is like Sequence[tuple[float, float]]
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
def is_numeric(obj) -> bool:
|
|
638
|
+
"""
|
|
639
|
+
Return True if obj is a python or numpy float or integer
|
|
640
|
+
"""
|
|
641
|
+
return isinstance(obj, (float, int, np.floating, np.integer))
|
|
642
|
+
|
|
643
|
+
if not iter(obj):
|
|
644
|
+
return False
|
|
645
|
+
try:
|
|
646
|
+
return all(is_numeric(a) and is_numeric(b) for a, b in obj)
|
|
647
|
+
except TypeError:
|
|
648
|
+
return False
|
|
630
649
|
|
|
631
650
|
|
|
632
651
|
def has_groups(data: pd.DataFrame) -> bool:
|