plot3 0.4.0__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.
- plot3/__init__.py +301 -0
- plot3/__version__.py +1 -0
- plot3/aesexpr.py +271 -0
- plot3/build.py +3948 -0
- plot3/calculus.py +1179 -0
- plot3/compose.py +285 -0
- plot3/contour.py +476 -0
- plot3/craft.py +142 -0
- plot3/encode.py +68 -0
- plot3/expr.py +1557 -0
- plot3/flip.py +245 -0
- plot3/function.py +1301 -0
- plot3/geoms.py +2558 -0
- plot3/ggplot.py +713 -0
- plot3/io.py +76 -0
- plot3/jupyter.py +514 -0
- plot3/latexin.py +616 -0
- plot3/masking.py +494 -0
- plot3/mathtext.py +842 -0
- plot3/payload.py +216 -0
- plot3/remote.py +220 -0
- plot3/scales.py +387 -0
- plot3/scaling.py +636 -0
- plot3/special.py +407 -0
- plot3/stat2d.py +1539 -0
- plot3/static.py +3760 -0
- plot3/stats3d.py +462 -0
- plot3/table.py +775 -0
- plot3/themes.py +104 -0
- plot3/viewer.py +3354 -0
- plot3-0.4.0.dist-info/METADATA +504 -0
- plot3-0.4.0.dist-info/RECORD +35 -0
- plot3-0.4.0.dist-info/WHEEL +5 -0
- plot3-0.4.0.dist-info/licenses/LICENSE +21 -0
- plot3-0.4.0.dist-info/top_level.txt +1 -0
plot3/__init__.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""plot3 — grammar-of-graphics plotting on three.js."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from plot3.__version__ import __version__
|
|
6
|
+
from plot3.geoms import (
|
|
7
|
+
aes,
|
|
8
|
+
annotate,
|
|
9
|
+
area,
|
|
10
|
+
arrow,
|
|
11
|
+
coord_3d,
|
|
12
|
+
coord_equal,
|
|
13
|
+
coord_fixed,
|
|
14
|
+
expand_limits,
|
|
15
|
+
coord_cartesian,
|
|
16
|
+
coord_flip,
|
|
17
|
+
coord_polar,
|
|
18
|
+
derivative,
|
|
19
|
+
facet_grid,
|
|
20
|
+
facet_wrap,
|
|
21
|
+
label_both,
|
|
22
|
+
label_value,
|
|
23
|
+
labeller,
|
|
24
|
+
as_labeller,
|
|
25
|
+
geom_bar,
|
|
26
|
+
geom_box3d,
|
|
27
|
+
geom_boxplot,
|
|
28
|
+
geom_col,
|
|
29
|
+
geom_density,
|
|
30
|
+
geom_abline,
|
|
31
|
+
geom_area,
|
|
32
|
+
geom_crossbar,
|
|
33
|
+
geom_errorbar,
|
|
34
|
+
geom_errorbarh,
|
|
35
|
+
geom_freqpoly,
|
|
36
|
+
geom_bin_2d,
|
|
37
|
+
geom_blank,
|
|
38
|
+
guide_legend,
|
|
39
|
+
guide_colourbar,
|
|
40
|
+
guide_colorbar,
|
|
41
|
+
guide_none,
|
|
42
|
+
position_dodge2,
|
|
43
|
+
stat_bin,
|
|
44
|
+
stat_count,
|
|
45
|
+
stat_density,
|
|
46
|
+
stat_function,
|
|
47
|
+
stat_smooth,
|
|
48
|
+
vars,
|
|
49
|
+
geom_count,
|
|
50
|
+
geom_hex,
|
|
51
|
+
geom_density_2d,
|
|
52
|
+
geom_density_2d_filled,
|
|
53
|
+
geom_density2d,
|
|
54
|
+
stat_density_2d,
|
|
55
|
+
geom_contour,
|
|
56
|
+
stat_ellipse,
|
|
57
|
+
geom_polygon,
|
|
58
|
+
geom_function,
|
|
59
|
+
geom_hline,
|
|
60
|
+
geom_histogram,
|
|
61
|
+
geom_isosurface,
|
|
62
|
+
geom_jitter,
|
|
63
|
+
geom_label,
|
|
64
|
+
geom_line,
|
|
65
|
+
geom_linerange,
|
|
66
|
+
geom_path,
|
|
67
|
+
geom_point,
|
|
68
|
+
geom_point3d,
|
|
69
|
+
geom_pointrange,
|
|
70
|
+
geom_qq,
|
|
71
|
+
geom_qq_line,
|
|
72
|
+
geom_raster,
|
|
73
|
+
geom_rect,
|
|
74
|
+
geom_ribbon,
|
|
75
|
+
geom_rug,
|
|
76
|
+
geom_segment,
|
|
77
|
+
geom_smooth,
|
|
78
|
+
geom_step,
|
|
79
|
+
geom_surface,
|
|
80
|
+
geom_text,
|
|
81
|
+
geom_tile,
|
|
82
|
+
geom_vector_field,
|
|
83
|
+
geom_vline,
|
|
84
|
+
geom_violin,
|
|
85
|
+
ggtitle,
|
|
86
|
+
guides,
|
|
87
|
+
labs,
|
|
88
|
+
position_dodge,
|
|
89
|
+
position_jitter,
|
|
90
|
+
position_jitterdodge,
|
|
91
|
+
position_nudge,
|
|
92
|
+
position_fill,
|
|
93
|
+
position_stack,
|
|
94
|
+
stat_density_3d,
|
|
95
|
+
stat_ecdf,
|
|
96
|
+
stat_qq,
|
|
97
|
+
stat_qq_line,
|
|
98
|
+
stat_summary,
|
|
99
|
+
tangent,
|
|
100
|
+
xlab,
|
|
101
|
+
ylab,
|
|
102
|
+
scale_color_continuous,
|
|
103
|
+
scale_color_viridis_c,
|
|
104
|
+
scale_colour_continuous,
|
|
105
|
+
scale_colour_viridis_c,
|
|
106
|
+
scale_fill_viridis_c,
|
|
107
|
+
scale_x_log10,
|
|
108
|
+
scale_y_log10,
|
|
109
|
+
theme,
|
|
110
|
+
theme_bw,
|
|
111
|
+
theme_classic,
|
|
112
|
+
theme_dark,
|
|
113
|
+
theme_light,
|
|
114
|
+
theme_minimal,
|
|
115
|
+
theme_lidar,
|
|
116
|
+
theme_grey,
|
|
117
|
+
theme_gray,
|
|
118
|
+
theme_linedraw,
|
|
119
|
+
element_blank,
|
|
120
|
+
element_line,
|
|
121
|
+
element_rect,
|
|
122
|
+
element_text,
|
|
123
|
+
theme_void,
|
|
124
|
+
slider,
|
|
125
|
+
transition_states,
|
|
126
|
+
transition_time,
|
|
127
|
+
)
|
|
128
|
+
from plot3.compose import plot_annotation, plot_layout
|
|
129
|
+
from plot3.scaling import * # noqa: F401,F403 - scale_* functions
|
|
130
|
+
from plot3.scaling import __all__ as _scaling_all
|
|
131
|
+
from plot3.ggplot import autohide, ggsave, ggplot # show via ggplot.show
|
|
132
|
+
from plot3.io import read_bin
|
|
133
|
+
from plot3.jupyter import (
|
|
134
|
+
disable_r_style,
|
|
135
|
+
enable_r_style,
|
|
136
|
+
load_ipython_extension,
|
|
137
|
+
register_plot3,
|
|
138
|
+
)
|
|
139
|
+
from plot3.payload import (
|
|
140
|
+
PAYLOAD_VERSION,
|
|
141
|
+
build_payload,
|
|
142
|
+
display_payload,
|
|
143
|
+
load_payload,
|
|
144
|
+
render_payload,
|
|
145
|
+
save_payload,
|
|
146
|
+
validate_payload,
|
|
147
|
+
)
|
|
148
|
+
from plot3.remote import (
|
|
149
|
+
MIME_PLOT3,
|
|
150
|
+
has_craft_host,
|
|
151
|
+
is_remote_kernel,
|
|
152
|
+
remote_ggplot_payload,
|
|
153
|
+
show_remote,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"ggplot",
|
|
158
|
+
"aes",
|
|
159
|
+
"geom_point",
|
|
160
|
+
"geom_point3d",
|
|
161
|
+
"geom_surface",
|
|
162
|
+
"geom_isosurface",
|
|
163
|
+
"stat_density_3d",
|
|
164
|
+
"geom_line",
|
|
165
|
+
"geom_path",
|
|
166
|
+
"geom_col",
|
|
167
|
+
"geom_bar",
|
|
168
|
+
"geom_histogram",
|
|
169
|
+
"geom_boxplot",
|
|
170
|
+
"geom_density",
|
|
171
|
+
"geom_function",
|
|
172
|
+
"geom_vector_field",
|
|
173
|
+
"geom_violin",
|
|
174
|
+
"geom_jitter",
|
|
175
|
+
"geom_errorbar",
|
|
176
|
+
"geom_linerange",
|
|
177
|
+
"geom_pointrange",
|
|
178
|
+
"geom_ribbon",
|
|
179
|
+
"geom_smooth",
|
|
180
|
+
"stat_summary",
|
|
181
|
+
"geom_hline",
|
|
182
|
+
"geom_vline",
|
|
183
|
+
"geom_abline",
|
|
184
|
+
"geom_text",
|
|
185
|
+
"geom_label",
|
|
186
|
+
"geom_tile",
|
|
187
|
+
"geom_raster",
|
|
188
|
+
"geom_area",
|
|
189
|
+
"geom_step",
|
|
190
|
+
"geom_segment",
|
|
191
|
+
"geom_rect",
|
|
192
|
+
"geom_qq",
|
|
193
|
+
"geom_qq_line",
|
|
194
|
+
"stat_qq",
|
|
195
|
+
"stat_qq_line",
|
|
196
|
+
"stat_ecdf",
|
|
197
|
+
"coord_flip",
|
|
198
|
+
"annotate",
|
|
199
|
+
"arrow",
|
|
200
|
+
"position_dodge",
|
|
201
|
+
"position_stack",
|
|
202
|
+
"position_fill",
|
|
203
|
+
"area",
|
|
204
|
+
"tangent",
|
|
205
|
+
"derivative",
|
|
206
|
+
"facet_wrap",
|
|
207
|
+
"facet_grid",
|
|
208
|
+
"coord_3d",
|
|
209
|
+
"coord_equal",
|
|
210
|
+
"coord_polar",
|
|
211
|
+
"labs",
|
|
212
|
+
"scale_colour_continuous",
|
|
213
|
+
"scale_color_continuous",
|
|
214
|
+
"scale_colour_viridis_c",
|
|
215
|
+
"scale_color_viridis_c",
|
|
216
|
+
"scale_x_log10",
|
|
217
|
+
"scale_y_log10",
|
|
218
|
+
"transition_time",
|
|
219
|
+
"transition_states",
|
|
220
|
+
"slider",
|
|
221
|
+
"theme",
|
|
222
|
+
"theme_bw",
|
|
223
|
+
"theme_classic",
|
|
224
|
+
"theme_dark",
|
|
225
|
+
"theme_light",
|
|
226
|
+
"theme_minimal",
|
|
227
|
+
"theme_lidar",
|
|
228
|
+
"theme_grey",
|
|
229
|
+
"theme_gray",
|
|
230
|
+
"theme_linedraw",
|
|
231
|
+
"element_blank",
|
|
232
|
+
"element_line",
|
|
233
|
+
"element_rect",
|
|
234
|
+
"element_text",
|
|
235
|
+
"geom_box3d",
|
|
236
|
+
"theme_void",
|
|
237
|
+
"scale_fill_viridis_c",
|
|
238
|
+
"ggtitle",
|
|
239
|
+
"xlab",
|
|
240
|
+
"ylab",
|
|
241
|
+
"guides",
|
|
242
|
+
"coord_cartesian",
|
|
243
|
+
"geom_rug",
|
|
244
|
+
"geom_crossbar",
|
|
245
|
+
"geom_errorbarh",
|
|
246
|
+
"geom_freqpoly",
|
|
247
|
+
"geom_bin_2d",
|
|
248
|
+
"geom_blank", "guide_legend", "guide_colourbar", "guide_colorbar", "guide_none",
|
|
249
|
+
"position_dodge2", "stat_bin", "stat_count", "stat_density", "stat_function",
|
|
250
|
+
"stat_smooth", "vars",
|
|
251
|
+
"geom_count",
|
|
252
|
+
"geom_hex",
|
|
253
|
+
"geom_density_2d",
|
|
254
|
+
"geom_density_2d_filled",
|
|
255
|
+
"geom_density2d",
|
|
256
|
+
"stat_density_2d",
|
|
257
|
+
"geom_contour",
|
|
258
|
+
"stat_ellipse",
|
|
259
|
+
"geom_polygon",
|
|
260
|
+
"coord_fixed",
|
|
261
|
+
"label_both",
|
|
262
|
+
"label_value",
|
|
263
|
+
"labeller",
|
|
264
|
+
"as_labeller",
|
|
265
|
+
"expand_limits",
|
|
266
|
+
"position_jitter",
|
|
267
|
+
"position_jitterdodge",
|
|
268
|
+
"position_nudge",
|
|
269
|
+
"ggsave",
|
|
270
|
+
"plot_layout",
|
|
271
|
+
"plot_annotation",
|
|
272
|
+
"read_bin",
|
|
273
|
+
"autohide",
|
|
274
|
+
"enable_r_style",
|
|
275
|
+
"disable_r_style",
|
|
276
|
+
"load_ipython_extension",
|
|
277
|
+
"register_plot3",
|
|
278
|
+
"PAYLOAD_VERSION",
|
|
279
|
+
"build_payload",
|
|
280
|
+
"render_payload",
|
|
281
|
+
"validate_payload",
|
|
282
|
+
"save_payload",
|
|
283
|
+
"load_payload",
|
|
284
|
+
"display_payload",
|
|
285
|
+
"MIME_PLOT3",
|
|
286
|
+
"is_remote_kernel",
|
|
287
|
+
"has_craft_host",
|
|
288
|
+
"remote_ggplot_payload",
|
|
289
|
+
"show_remote",
|
|
290
|
+
"__version__",
|
|
291
|
+
*_scaling_all,
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
# Auto-register magics when loaded inside IPython (CRAFT / Jupyter).
|
|
295
|
+
try:
|
|
296
|
+
from IPython import get_ipython as _get_ipython
|
|
297
|
+
|
|
298
|
+
if _get_ipython is not None and _get_ipython() is not None:
|
|
299
|
+
register_plot3(quiet=False)
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
plot3/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
plot3/aesexpr.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Expressions in aes(): ``aes(ymin="mean - se")``, ``aes(colour="factor(cyl)")``.
|
|
2
|
+
|
|
3
|
+
ggplot2 evaluates ``aes()`` in the data, so R code maps computed values
|
|
4
|
+
directly. plot3 reads such a string as an expression over the data's
|
|
5
|
+
columns: arithmetic, comparisons, and a short list of functions, evaluated
|
|
6
|
+
with NumPy. Nothing else runs, so a string from a file cannot execute code.
|
|
7
|
+
The computed values become a column named by the expression, which then
|
|
8
|
+
titles the axis or legend, as in ggplot2.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import re
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from plot3.table import ColumnNotFound, detect_backend, has_column
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _factor(values):
|
|
24
|
+
from plot3.scales import ordered_levels
|
|
25
|
+
|
|
26
|
+
array = np.asarray(values)
|
|
27
|
+
if array.dtype.kind == "f" and np.all(np.isnan(array) | (array == np.round(array))):
|
|
28
|
+
# factor(cyl) reads 4, 6, 8, not 4.0, 6.0, 8.0.
|
|
29
|
+
array = pd.array(np.where(np.isnan(array), 0, array).astype(np.int64), dtype="Int64")
|
|
30
|
+
array[np.isnan(np.asarray(values, dtype=np.float64))] = pd.NA
|
|
31
|
+
series = pd.Series(array)
|
|
32
|
+
levels = ordered_levels([v for v in pd.unique(series) if not pd.isna(v)])
|
|
33
|
+
return pd.Categorical(series, categories=levels)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _round(values, digits=0):
|
|
37
|
+
return np.round(np.asarray(values, dtype=np.float64), int(digits))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ifelse(cond, yes, no):
|
|
41
|
+
return np.where(np.asarray(cond, dtype=bool), yes, no)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _scalar(fn):
|
|
45
|
+
return lambda values: fn(np.asarray(values, dtype=np.float64))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_FUNCTIONS = {
|
|
49
|
+
"log": np.log, "log10": np.log10, "log2": np.log2, "log1p": np.log1p,
|
|
50
|
+
"exp": np.exp, "sqrt": np.sqrt, "abs": np.abs, "floor": np.floor,
|
|
51
|
+
"ceiling": np.ceil, "ceil": np.ceil, "sin": np.sin, "cos": np.cos, "tan": np.tan,
|
|
52
|
+
"round": _round, "ifelse": _ifelse, "pmin": np.fmin, "pmax": np.fmax,
|
|
53
|
+
"factor": _factor, "as_factor": _factor,
|
|
54
|
+
"as_numeric": lambda v: pd.to_numeric(pd.Series(np.asarray(v)), errors="coerce").to_numpy(),
|
|
55
|
+
"as_character": lambda v: np.asarray(v).astype(str),
|
|
56
|
+
"mean": _scalar(np.nanmean), "median": _scalar(np.nanmedian),
|
|
57
|
+
"sd": _scalar(lambda a: np.nanstd(a, ddof=1)), "min": _scalar(np.nanmin),
|
|
58
|
+
"max": _scalar(np.nanmax), "sum": _scalar(np.nansum),
|
|
59
|
+
}
|
|
60
|
+
_CONSTANTS = {"True": True, "False": False, "pi": np.pi}
|
|
61
|
+
_BINARY = {
|
|
62
|
+
ast.Add: np.add, ast.Sub: np.subtract, ast.Mult: np.multiply, ast.Div: np.true_divide,
|
|
63
|
+
ast.Pow: np.power, ast.Mod: np.mod, ast.FloorDiv: np.floor_divide,
|
|
64
|
+
ast.BitAnd: np.logical_and, ast.BitOr: np.logical_or,
|
|
65
|
+
}
|
|
66
|
+
_COMPARE = {
|
|
67
|
+
ast.Eq: np.equal, ast.NotEq: np.not_equal, ast.Lt: np.less, ast.LtE: np.less_equal,
|
|
68
|
+
ast.Gt: np.greater, ast.GtE: np.greater_equal,
|
|
69
|
+
}
|
|
70
|
+
_STAT_FORMS = re.compile(r"^\s*(after_stat|stat|after_scale|stage)\s*\(|^\.\.\w+\.\.$")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _translate(text: str) -> tuple[str, dict[str, str]]:
|
|
74
|
+
"""R spellings to Python ones; backtick names to placeholders."""
|
|
75
|
+
names: dict[str, str] = {}
|
|
76
|
+
|
|
77
|
+
def tick(match):
|
|
78
|
+
key = f"__col{len(names)}"
|
|
79
|
+
names[key] = match.group(1)
|
|
80
|
+
return key
|
|
81
|
+
|
|
82
|
+
out = re.sub(r"`([^`]+)`", tick, text)
|
|
83
|
+
out = out.replace("^", "**")
|
|
84
|
+
out = re.sub(r"\bas\.(factor|numeric|character)\s*\(", r"as_\1(", out)
|
|
85
|
+
out = re.sub(r"\bTRUE\b", "True", out)
|
|
86
|
+
out = re.sub(r"\bFALSE\b", "False", out)
|
|
87
|
+
out = re.sub(r"!(?!=)", " not ", out)
|
|
88
|
+
return out, names
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse(text: str):
|
|
92
|
+
"""(tree, backtick names) when ``text`` is an expression, else None.
|
|
93
|
+
|
|
94
|
+
A bare name or number is not an expression: it is a column (or a typo
|
|
95
|
+
the missing-column message explains)."""
|
|
96
|
+
if not isinstance(text, str) or _STAT_FORMS.search(text):
|
|
97
|
+
return None
|
|
98
|
+
source, names = _translate(text)
|
|
99
|
+
try:
|
|
100
|
+
tree = ast.parse(source.strip(), mode="eval")
|
|
101
|
+
except SyntaxError:
|
|
102
|
+
return None
|
|
103
|
+
if isinstance(tree.body, (ast.Name, ast.Constant)):
|
|
104
|
+
return None
|
|
105
|
+
for node in ast.walk(tree):
|
|
106
|
+
if isinstance(node, ast.Call):
|
|
107
|
+
if not isinstance(node.func, ast.Name) or node.func.id not in _FUNCTIONS:
|
|
108
|
+
return None
|
|
109
|
+
elif isinstance(node, (ast.Attribute, ast.Subscript, ast.Lambda, ast.ListComp,
|
|
110
|
+
ast.DictComp, ast.SetComp, ast.GeneratorExp, ast.Starred)):
|
|
111
|
+
return None
|
|
112
|
+
return tree, names
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def columns_used(tree, names: dict[str, str]) -> list[str]:
|
|
116
|
+
"""Names read as values. A column may share a function's name (mean):
|
|
117
|
+
only a name being called is the function."""
|
|
118
|
+
called = {id(node.func) for node in ast.walk(tree) if isinstance(node, ast.Call)}
|
|
119
|
+
used = []
|
|
120
|
+
for node in ast.walk(tree):
|
|
121
|
+
if isinstance(node, ast.Name) and id(node) not in called and node.id not in _CONSTANTS:
|
|
122
|
+
used.append(names.get(node.id, node.id))
|
|
123
|
+
return list(dict.fromkeys(used))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _evaluate(node, frame: pd.DataFrame, names: dict[str, str]):
|
|
127
|
+
if isinstance(node, ast.Expression):
|
|
128
|
+
return _evaluate(node.body, frame, names)
|
|
129
|
+
if isinstance(node, ast.Constant):
|
|
130
|
+
return node.value
|
|
131
|
+
if isinstance(node, ast.Name):
|
|
132
|
+
if node.id in _CONSTANTS:
|
|
133
|
+
return _CONSTANTS[node.id]
|
|
134
|
+
column = frame[names.get(node.id, node.id)]
|
|
135
|
+
if pd.api.types.is_numeric_dtype(column) and not pd.api.types.is_bool_dtype(column):
|
|
136
|
+
return column.to_numpy(dtype=np.float64, na_value=np.nan)
|
|
137
|
+
return column.to_numpy()
|
|
138
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _BINARY:
|
|
139
|
+
return _BINARY[type(node.op)](_evaluate(node.left, frame, names), _evaluate(node.right, frame, names))
|
|
140
|
+
if isinstance(node, ast.UnaryOp):
|
|
141
|
+
value = _evaluate(node.operand, frame, names)
|
|
142
|
+
if isinstance(node.op, ast.USub):
|
|
143
|
+
return np.negative(value)
|
|
144
|
+
if isinstance(node.op, ast.UAdd):
|
|
145
|
+
return value
|
|
146
|
+
if isinstance(node.op, (ast.Not, ast.Invert)):
|
|
147
|
+
return np.logical_not(value)
|
|
148
|
+
if isinstance(node, ast.BoolOp):
|
|
149
|
+
values = [_evaluate(v, frame, names) for v in node.values]
|
|
150
|
+
combine = np.logical_and if isinstance(node.op, ast.And) else np.logical_or
|
|
151
|
+
out = values[0]
|
|
152
|
+
for value in values[1:]:
|
|
153
|
+
out = combine(out, value)
|
|
154
|
+
return out
|
|
155
|
+
if isinstance(node, ast.Compare):
|
|
156
|
+
left = _evaluate(node.left, frame, names)
|
|
157
|
+
out = None
|
|
158
|
+
for op, right_node in zip(node.ops, node.comparators):
|
|
159
|
+
if type(op) not in _COMPARE:
|
|
160
|
+
raise ValueError(f"aes() expressions do not support {type(op).__name__}")
|
|
161
|
+
right = _evaluate(right_node, frame, names)
|
|
162
|
+
step = _COMPARE[type(op)](left, right)
|
|
163
|
+
out = step if out is None else np.logical_and(out, step)
|
|
164
|
+
left = right
|
|
165
|
+
return out
|
|
166
|
+
if isinstance(node, ast.Call):
|
|
167
|
+
args = [_evaluate(a, frame, names) for a in node.args]
|
|
168
|
+
kwargs = {k.arg: _evaluate(k.value, frame, names) for k in node.keywords}
|
|
169
|
+
return _FUNCTIONS[node.func.id](*args, **kwargs)
|
|
170
|
+
raise ValueError(f"aes() expressions do not support {type(node).__name__}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _rows(data: Any, cols: list[str]) -> pd.DataFrame:
|
|
174
|
+
"""The columns as pandas, every row kept (missing values too)."""
|
|
175
|
+
backend = detect_backend(data)
|
|
176
|
+
if backend == "pandas":
|
|
177
|
+
return data.loc[:, cols]
|
|
178
|
+
from plot3.table import _polars_to_pandas, resolve_polars
|
|
179
|
+
|
|
180
|
+
import polars as pl
|
|
181
|
+
|
|
182
|
+
frame = resolve_polars(data) if backend == "tidy" else data
|
|
183
|
+
if isinstance(frame, pl.LazyFrame):
|
|
184
|
+
frame = frame.collect()
|
|
185
|
+
return _polars_to_pandas(frame.select(cols))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def with_expressions(data: Any, texts) -> Any:
|
|
189
|
+
"""``data`` plus one column per expression in ``texts`` that is not
|
|
190
|
+
already a column. Other backends than pandas and polars are returned
|
|
191
|
+
unchanged (their columns are positions)."""
|
|
192
|
+
if data is None:
|
|
193
|
+
return data
|
|
194
|
+
wanted = []
|
|
195
|
+
for text in dict.fromkeys(t for t in texts if isinstance(t, str)):
|
|
196
|
+
if has_column(data, text):
|
|
197
|
+
continue
|
|
198
|
+
parsed = parse(text)
|
|
199
|
+
if parsed is not None:
|
|
200
|
+
wanted.append((text, parsed))
|
|
201
|
+
if not wanted:
|
|
202
|
+
return data
|
|
203
|
+
backend = detect_backend(data)
|
|
204
|
+
if backend not in {"pandas", "polars", "tidy"}:
|
|
205
|
+
return data
|
|
206
|
+
computed: dict[str, Any] = {}
|
|
207
|
+
for text, (tree, names) in wanted:
|
|
208
|
+
used = columns_used(tree, names)
|
|
209
|
+
missing = [c for c in used if not has_column(data, c)]
|
|
210
|
+
if missing:
|
|
211
|
+
raise ColumnNotFound(missing, data)
|
|
212
|
+
frame = _rows(data, used)
|
|
213
|
+
value = _evaluate(tree, frame, names)
|
|
214
|
+
if np.ndim(value) == 0:
|
|
215
|
+
value = np.full(len(frame), value)
|
|
216
|
+
computed[text] = value
|
|
217
|
+
if backend == "pandas":
|
|
218
|
+
out = data.copy()
|
|
219
|
+
for text, value in computed.items():
|
|
220
|
+
out[text] = value
|
|
221
|
+
return out
|
|
222
|
+
import polars as pl
|
|
223
|
+
|
|
224
|
+
frame = data
|
|
225
|
+
if backend == "tidy":
|
|
226
|
+
from plot3.table import resolve_polars
|
|
227
|
+
|
|
228
|
+
frame = resolve_polars(data)
|
|
229
|
+
if isinstance(frame, pl.LazyFrame):
|
|
230
|
+
frame = frame.collect()
|
|
231
|
+
series = []
|
|
232
|
+
for text, value in computed.items():
|
|
233
|
+
if isinstance(value, pd.Categorical):
|
|
234
|
+
# polars keeps the levels' order as an Enum.
|
|
235
|
+
levels = [str(c) for c in value.categories]
|
|
236
|
+
series.append(pl.Series(text, np.asarray(value.astype(str)), dtype=pl.Enum(levels)))
|
|
237
|
+
else:
|
|
238
|
+
series.append(pl.Series(text, np.asarray(value)))
|
|
239
|
+
return frame.with_columns(series)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def add_expression_columns(g):
|
|
243
|
+
"""A copy of the figure whose data carry a column for each aes()
|
|
244
|
+
expression its layers use."""
|
|
245
|
+
import copy
|
|
246
|
+
|
|
247
|
+
def texts(mapping) -> list:
|
|
248
|
+
return [v for k, v in dict(mapping or {}).items()]
|
|
249
|
+
|
|
250
|
+
shared = []
|
|
251
|
+
layers = []
|
|
252
|
+
changed = False
|
|
253
|
+
for layer in g.layers:
|
|
254
|
+
mapping = texts(g.mapping) + texts(getattr(layer, "mapping", None))
|
|
255
|
+
own = getattr(layer, "layer_data", None)
|
|
256
|
+
if own is not None:
|
|
257
|
+
new = with_expressions(own, mapping)
|
|
258
|
+
if new is not own:
|
|
259
|
+
layer = copy.copy(layer)
|
|
260
|
+
layer.layer_data = new
|
|
261
|
+
changed = True
|
|
262
|
+
elif getattr(layer, "kind", None) not in {"function", "vector"}:
|
|
263
|
+
shared.extend(mapping)
|
|
264
|
+
layers.append(layer)
|
|
265
|
+
data = with_expressions(g.data, shared) if g.data is not None else None
|
|
266
|
+
if data is g.data and not changed:
|
|
267
|
+
return g
|
|
268
|
+
out = copy.copy(g)
|
|
269
|
+
out.data = data
|
|
270
|
+
out.layers = layers
|
|
271
|
+
return out
|