plotnine 0.15.0.dev3__py3-none-any.whl → 0.15.2__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 +126 -41
- plotnine/_mpl/layout_manager/_layout_tree.py +712 -314
- plotnine/_mpl/layout_manager/_spaces.py +305 -101
- 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 +26 -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 +1 -0
- 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 +2 -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 +8 -5
- 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 +1 -1
- plotnine/mapping/aes.py +85 -49
- 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 +13 -15
- 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 +109 -31
- plotnine/typing.py +17 -6
- plotnine/watermark.py +3 -3
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/METADATA +13 -6
- plotnine-0.15.2.dist-info/RECORD +221 -0
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.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.dev3.dist-info/RECORD +0 -215
- /plotnine/{plot_composition → composition}/_plotspec.py +0 -0
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.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
|
"""
|
|
@@ -48,7 +48,7 @@ def factor(
|
|
|
48
48
|
`categories` attribute (which in turn is the `categories` argument, if
|
|
49
49
|
provided).
|
|
50
50
|
"""
|
|
51
|
-
return pd.Categorical(values, categories=categories, ordered=None)
|
|
51
|
+
return pd.Categorical(values, categories=categories, ordered=None) # pyright: ignore[reportArgumentType]
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def reorder(x, y, fun=np.median, ascending=True):
|
plotnine/mapping/aes.py
CHANGED
|
@@ -8,7 +8,9 @@ from dataclasses import fields
|
|
|
8
8
|
from functools import cached_property
|
|
9
9
|
from typing import TYPE_CHECKING, Any, Dict
|
|
10
10
|
|
|
11
|
+
import numpy as np
|
|
11
12
|
import pandas as pd
|
|
13
|
+
from mizani._colors.utils import is_color_tuple
|
|
12
14
|
|
|
13
15
|
from ..iapi import labels_view
|
|
14
16
|
from .evaluation import after_stat, stage
|
|
@@ -282,14 +284,20 @@ class aes(Dict[str, Any]):
|
|
|
282
284
|
|
|
283
285
|
return result
|
|
284
286
|
|
|
285
|
-
def __radd__(self,
|
|
287
|
+
def __radd__(self, other):
|
|
286
288
|
"""
|
|
287
289
|
Add aesthetic mappings to ggplot
|
|
288
290
|
"""
|
|
289
291
|
self = deepcopy(self)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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)
|
|
293
301
|
|
|
294
302
|
def copy(self):
|
|
295
303
|
return aes(**self)
|
|
@@ -538,23 +546,23 @@ def make_labels(mapping: dict[str, Any] | aes) -> labels_view:
|
|
|
538
546
|
)
|
|
539
547
|
|
|
540
548
|
|
|
541
|
-
|
|
549
|
+
class RepeatAesthetic:
|
|
542
550
|
"""
|
|
543
|
-
|
|
551
|
+
Repeat an Aeshetic a given number of times
|
|
544
552
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
value :
|
|
548
|
-
Value to check
|
|
549
|
-
ae :
|
|
550
|
-
Aesthetic name
|
|
553
|
+
The methods in this class know how to create sequences of aesthetics
|
|
554
|
+
whose values may not be scalar.
|
|
551
555
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
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
|
|
556
559
|
"""
|
|
557
|
-
|
|
560
|
+
|
|
561
|
+
@staticmethod
|
|
562
|
+
def linetype(value: Any, n: int) -> Sequence[Any]:
|
|
563
|
+
"""
|
|
564
|
+
Repeat linetypes
|
|
565
|
+
"""
|
|
558
566
|
named = {
|
|
559
567
|
"solid",
|
|
560
568
|
"dashed",
|
|
@@ -569,47 +577,75 @@ def is_valid_aesthetic(value: Any, ae: str) -> bool:
|
|
|
569
577
|
"",
|
|
570
578
|
}
|
|
571
579
|
if value in named:
|
|
572
|
-
return
|
|
580
|
+
return [value] * n
|
|
573
581
|
|
|
574
582
|
# tuple of the form (offset, (on, off, on, off, ...))
|
|
575
583
|
# e.g (0, (1, 2))
|
|
576
|
-
|
|
577
|
-
isinstance(value, tuple)
|
|
578
|
-
isinstance(value[0], int)
|
|
579
|
-
isinstance(value[1], tuple)
|
|
580
|
-
len(value[1]) % 2 == 0
|
|
581
|
-
all(isinstance(x, int) for x in value[1])
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
"""
|
|
586
600
|
if isinstance(value, str):
|
|
587
|
-
return
|
|
601
|
+
return [value] * n
|
|
602
|
+
if is_color_tuple(value):
|
|
603
|
+
return [tuple(value)] * n
|
|
588
604
|
|
|
605
|
+
raise ValueError(f"{value} is not a known color.")
|
|
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
|
|
589
616
|
# tuple of the form (numsides, style, angle)
|
|
590
617
|
# where style is in the range [0, 3]
|
|
591
618
|
# e.g (4, 1, 45)
|
|
592
|
-
|
|
593
|
-
isinstance(value, tuple)
|
|
594
|
-
all(isinstance(x, int) for x in value)
|
|
595
|
-
0 <= value[1] < 3
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
elif ae in {"color", "fill"}:
|
|
600
|
-
if isinstance(value, str):
|
|
601
|
-
return True
|
|
602
|
-
with suppress(TypeError):
|
|
603
|
-
if isinstance(value, (tuple, list)) and all(
|
|
604
|
-
0 <= x <= 1 for x in value
|
|
605
|
-
):
|
|
606
|
-
return True
|
|
607
|
-
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
|
|
608
625
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
626
|
+
if is_shape_points(value):
|
|
627
|
+
return [tuple(value)] * n
|
|
628
|
+
|
|
629
|
+
raise ValueError(f"{value} is not a know shape.")
|
|
630
|
+
|
|
631
|
+
|
|
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
|
|
613
649
|
|
|
614
650
|
|
|
615
651
|
def has_groups(data: pd.DataFrame) -> bool:
|
plotnine/scales/__init__.py
CHANGED
|
@@ -74,6 +74,7 @@ from .scale_identity import (
|
|
|
74
74
|
scale_linetype_identity,
|
|
75
75
|
scale_shape_identity,
|
|
76
76
|
scale_size_identity,
|
|
77
|
+
scale_stroke_identity,
|
|
77
78
|
)
|
|
78
79
|
|
|
79
80
|
# linetype
|
|
@@ -217,6 +218,7 @@ __all__ = (
|
|
|
217
218
|
"scale_linetype_identity",
|
|
218
219
|
"scale_shape_identity",
|
|
219
220
|
"scale_size_identity",
|
|
221
|
+
"scale_stroke_identity",
|
|
220
222
|
# manual
|
|
221
223
|
"scale_color_manual",
|
|
222
224
|
"scale_colour_manual",
|
plotnine/scales/limits.py
CHANGED
|
@@ -78,10 +78,10 @@ class _lim:
|
|
|
78
78
|
self.aesthetic, series, limits=self.limits, trans=self.trans
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
-
def __radd__(self,
|
|
82
|
-
scale = self.get_scale(
|
|
83
|
-
|
|
84
|
-
return
|
|
81
|
+
def __radd__(self, other):
|
|
82
|
+
scale = self.get_scale(other)
|
|
83
|
+
other.scales.append(scale)
|
|
84
|
+
return other
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
class xlim(_lim):
|
|
@@ -194,7 +194,7 @@ class lims:
|
|
|
194
194
|
def __init__(self, **kwargs):
|
|
195
195
|
self._kwargs = kwargs
|
|
196
196
|
|
|
197
|
-
def __radd__(self,
|
|
197
|
+
def __radd__(self, other):
|
|
198
198
|
"""
|
|
199
199
|
Add limits to ggplot object
|
|
200
200
|
"""
|
|
@@ -206,9 +206,9 @@ class lims:
|
|
|
206
206
|
msg = "Cannot change limits for '{}'"
|
|
207
207
|
raise PlotnineError(msg) from e
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
other += klass(value)
|
|
210
210
|
|
|
211
|
-
return
|
|
211
|
+
return other
|
|
212
212
|
|
|
213
213
|
|
|
214
214
|
def expand_limits(**kwargs):
|
plotnine/scales/scale.py
CHANGED
|
@@ -148,12 +148,12 @@ class scale(
|
|
|
148
148
|
self.aesthetics if self.aesthetics else self._aesthetics
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
def __radd__(self,
|
|
151
|
+
def __radd__(self, other):
|
|
152
152
|
"""
|
|
153
153
|
Add this scale to ggplot object
|
|
154
154
|
"""
|
|
155
|
-
|
|
156
|
-
return
|
|
155
|
+
other.scales.append(copy(self))
|
|
156
|
+
return other
|
|
157
157
|
|
|
158
158
|
def map(self, x, limits=None):
|
|
159
159
|
"""
|