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/flip.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""coord_flip: draw every layer with x and y exchanged.
|
|
2
|
+
|
|
3
|
+
Stats run in the usual orientation first (a boxplot summarises y by x).
|
|
4
|
+
Then bars, boxes, and filled densities are lowered to rectangles, lines, and
|
|
5
|
+
polygons, and every layer swaps its x and y. Both renderers then draw an
|
|
6
|
+
ordinary plot: no sideways drawing code is needed anywhere.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
import math
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from plot3 import stat2d
|
|
18
|
+
from plot3.geoms import _Geom, aes
|
|
19
|
+
|
|
20
|
+
_SWAP = {
|
|
21
|
+
"x": "y", "y": "x", "xmin": "ymin", "ymin": "xmin",
|
|
22
|
+
"xmax": "ymax", "ymax": "xmax", "xend": "yend", "yend": "xend",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def swap_mapping(mapping) -> dict:
|
|
27
|
+
return {_SWAP.get(key, key): value for key, value in dict(mapping or {}).items()}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def flipped_figure(g):
|
|
31
|
+
"""The ggplot with its own mapping, titles, and scales exchanged."""
|
|
32
|
+
out = copy.copy(g)
|
|
33
|
+
out.mapping = aes()
|
|
34
|
+
out.mapping.update(swap_mapping(g.mapping))
|
|
35
|
+
out.labs = {_SWAP.get(k, k): v for k, v in dict(g.labs).items()}
|
|
36
|
+
def moved(scale, axis):
|
|
37
|
+
if scale is None:
|
|
38
|
+
return None
|
|
39
|
+
scale = copy.copy(scale)
|
|
40
|
+
scale.axis = axis
|
|
41
|
+
return scale
|
|
42
|
+
|
|
43
|
+
out.xscale = moved(getattr(g, "yscale", None), "x")
|
|
44
|
+
out.yscale = moved(getattr(g, "xscale", None), "y")
|
|
45
|
+
out.scale_x, out.scale_y = getattr(g, "scale_y", None), getattr(g, "scale_x", None)
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def flip_refs(refs: list) -> list:
|
|
50
|
+
out = []
|
|
51
|
+
for ref in refs:
|
|
52
|
+
clone = copy.copy(ref)
|
|
53
|
+
if ref.kind == "hline":
|
|
54
|
+
clone.kind = "vline"
|
|
55
|
+
elif ref.kind == "vline":
|
|
56
|
+
clone.kind = "hline"
|
|
57
|
+
elif ref.kind == "abline":
|
|
58
|
+
# y = a + b x drawn with axes exchanged is x = a + b y.
|
|
59
|
+
if ref.slope == 0:
|
|
60
|
+
clone.kind = "vline"
|
|
61
|
+
clone.values = [ref.intercept]
|
|
62
|
+
else:
|
|
63
|
+
clone.slope = 1.0 / ref.slope
|
|
64
|
+
clone.intercept = -ref.intercept / ref.slope
|
|
65
|
+
out.append(clone)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def flip_layers(layers: list, g) -> list:
|
|
70
|
+
out = []
|
|
71
|
+
for geom in layers:
|
|
72
|
+
for piece in _lower(geom, g):
|
|
73
|
+
out.append(_swap(piece))
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _merged(geom, g) -> dict:
|
|
78
|
+
from plot3.build import _apply_fill
|
|
79
|
+
|
|
80
|
+
if getattr(geom, "_replace_mapping", False):
|
|
81
|
+
return dict(geom.mapping)
|
|
82
|
+
mapping = _apply_fill(dict(g.mapping), geom.kind)
|
|
83
|
+
mapping.update(_apply_fill(dict(geom.mapping), geom.kind))
|
|
84
|
+
return mapping
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _lower(geom, g) -> list:
|
|
88
|
+
kind = getattr(geom, "kind", "")
|
|
89
|
+
data = geom.data_override if getattr(geom, "data_override", None) is not None else g.data
|
|
90
|
+
if kind == "col":
|
|
91
|
+
mapping = _merged(geom, g)
|
|
92
|
+
proxy = _Geom(color=geom.const_color, alpha=geom.alpha)
|
|
93
|
+
proxy.kind = "col"
|
|
94
|
+
proxy.width = float(getattr(geom, "width", 0.9) or 0.9)
|
|
95
|
+
proxy.position = "identity"
|
|
96
|
+
bars = stat2d.positioned_bars(proxy, mapping, data)
|
|
97
|
+
bars._axis_labels = dict(getattr(geom, "_axis_labels", None) or {}) or getattr(bars, "_axis_labels", None)
|
|
98
|
+
return [bars]
|
|
99
|
+
if kind == "box":
|
|
100
|
+
return _lower_box(geom, g)
|
|
101
|
+
if kind == "area":
|
|
102
|
+
return [_lower_area(geom, g, data)]
|
|
103
|
+
if kind == "line" and getattr(geom, "sort_x", False) and getattr(geom, "_groups", None) is None:
|
|
104
|
+
# geom_line joins points in order of x; keep that order once x is vertical.
|
|
105
|
+
mapping = _merged(geom, g)
|
|
106
|
+
cols = [c for c in (mapping.get("x"), mapping.get("y"), mapping.get("color"), mapping.get("group")) if c]
|
|
107
|
+
from plot3.table import materialize_columns
|
|
108
|
+
|
|
109
|
+
frame = materialize_columns(data, cols)
|
|
110
|
+
keys = [c for c in (mapping.get("group"), mapping.get("color")) if c] + [mapping["x"]]
|
|
111
|
+
frame = frame.sort_values(keys, kind="stable").reset_index(drop=True)
|
|
112
|
+
clone = copy.copy(geom)
|
|
113
|
+
clone.data_override = frame
|
|
114
|
+
clone.sort_x = False
|
|
115
|
+
clone.mapping = aes()
|
|
116
|
+
clone.mapping.update(mapping)
|
|
117
|
+
clone._replace_mapping = True
|
|
118
|
+
return [clone]
|
|
119
|
+
return [geom]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _lower_box(geom, g) -> list:
|
|
123
|
+
"""A boxplot as a box polygon, median and whisker lines, and outliers."""
|
|
124
|
+
stats = geom.data_override
|
|
125
|
+
xcol = geom.mapping.get("x")
|
|
126
|
+
colour = geom.mapping.get("color")
|
|
127
|
+
colour = colour if colour and colour in stats.columns else None
|
|
128
|
+
axis = stat2d._axis(stats[xcol])
|
|
129
|
+
groups, levels = stat2d._colour_groups(stats, colour if colour != xcol else None)
|
|
130
|
+
n_groups = len(levels) if colour and colour != xcol else 1
|
|
131
|
+
width = float(getattr(geom, "width", 0.75)) / max(n_groups, 1)
|
|
132
|
+
offsets = stat2d._dodge_offsets(n_groups, float(getattr(geom, "width", 0.75)))
|
|
133
|
+
centres = axis.values + (offsets[groups] if n_groups > 1 else 0.0)
|
|
134
|
+
half = width / 2.0
|
|
135
|
+
ymin, lower, middle, upper, ymax = (stats[c].to_numpy(np.float64) for c in ("ymin", "lower", "middle", "upper", "ymax"))
|
|
136
|
+
keep = {colour: stats[colour].tolist()} if colour else {}
|
|
137
|
+
rx, ry, rect_groups, extra = stat2d._rect_rows(centres - half, centres + half, lower, upper, keep)
|
|
138
|
+
box_frame = pd.DataFrame({"x": axis.out(np.asarray(rx)), "y": ry})
|
|
139
|
+
box_map = {"x": "x", "y": "y"}
|
|
140
|
+
if colour:
|
|
141
|
+
box_frame[colour] = extra[colour]
|
|
142
|
+
box_map["colour"] = colour
|
|
143
|
+
box = stat2d._layer("poly", box_frame, box_map, geom, _groups=rect_groups, linewidth=1.2)
|
|
144
|
+
box.alpha = geom.alpha if geom.alpha is not None else 0.35
|
|
145
|
+
stat2d._levels_hook(box, axis)
|
|
146
|
+
lx, ly, lc, starts = [], [], [], []
|
|
147
|
+
for i in range(len(stats)):
|
|
148
|
+
c = centres[i]
|
|
149
|
+
for path_x, path_y in (
|
|
150
|
+
([c - half, c + half], [middle[i], middle[i]]), # median
|
|
151
|
+
([c, c], [ymin[i], lower[i]]), # lower whisker
|
|
152
|
+
([c, c], [upper[i], ymax[i]]), # upper whisker
|
|
153
|
+
):
|
|
154
|
+
if not all(math.isfinite(v) for v in path_y):
|
|
155
|
+
continue
|
|
156
|
+
starts.append([len(lx), 2])
|
|
157
|
+
lx += path_x
|
|
158
|
+
ly += path_y
|
|
159
|
+
if colour:
|
|
160
|
+
lc += [stats[colour].iloc[i]] * 2
|
|
161
|
+
line_frame = pd.DataFrame({"x": axis.out(np.asarray(lx)), "y": ly})
|
|
162
|
+
line_map = {"x": "x", "y": "y"}
|
|
163
|
+
if colour:
|
|
164
|
+
line_frame[colour] = lc
|
|
165
|
+
line_map["colour"] = colour
|
|
166
|
+
lines = stat2d._layer("line", line_frame, line_map, geom, _groups=starts, linewidth=1.5)
|
|
167
|
+
stat2d._levels_hook(lines, axis)
|
|
168
|
+
layers = [box, lines]
|
|
169
|
+
outliers = getattr(geom, "_outlier_frame", None)
|
|
170
|
+
y_name = getattr(geom, "_y_name", None)
|
|
171
|
+
if outliers is not None and len(outliers) and y_name in outliers.columns:
|
|
172
|
+
index = {str(level): i for i, level in enumerate(axis.levels or [])}
|
|
173
|
+
ox = np.array([index.get(str(v), np.nan) for v in outliers[xcol]], dtype=np.float64)
|
|
174
|
+
dots = pd.DataFrame({"x": ox, "y": outliers[y_name].to_numpy(np.float64)})
|
|
175
|
+
dot_map = {"x": "x", "y": "y"}
|
|
176
|
+
if colour and colour in outliers.columns:
|
|
177
|
+
dots[colour] = outliers[colour].to_numpy()
|
|
178
|
+
dot_map["colour"] = colour
|
|
179
|
+
dot = stat2d._layer("point", dots, dot_map, geom)
|
|
180
|
+
dot.size = float(getattr(geom, "outlier_size", 3.0))
|
|
181
|
+
stat2d._levels_hook(dot, axis)
|
|
182
|
+
layers.append(dot)
|
|
183
|
+
for layer in layers:
|
|
184
|
+
layer._axis_labels = {"x": xcol, "y": y_name or "y"}
|
|
185
|
+
return layers
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _lower_area(geom, g, data) -> _Geom:
|
|
189
|
+
"""A filled density or area as a band from its baseline."""
|
|
190
|
+
mapping = _merged(geom, g)
|
|
191
|
+
xcol, ycol = mapping["x"], mapping["y"]
|
|
192
|
+
colour = mapping.get("color")
|
|
193
|
+
colour = colour if colour and colour in data.columns else None
|
|
194
|
+
frame = data
|
|
195
|
+
axis = stat2d._axis(frame[xcol])
|
|
196
|
+
baseline = float(getattr(geom, "_baseline", 0.0) or 0.0)
|
|
197
|
+
groups, levels = stat2d._colour_groups(frame, colour)
|
|
198
|
+
pieces = []
|
|
199
|
+
for gi in range(len(levels)):
|
|
200
|
+
rows = np.flatnonzero(groups == gi)
|
|
201
|
+
xs = axis.values[rows]
|
|
202
|
+
ys = frame[ycol].to_numpy(np.float64)[rows]
|
|
203
|
+
value = frame[colour].iloc[rows[0]] if colour and rows.size else None
|
|
204
|
+
pieces.append(stat2d._band(xs, np.full_like(ys, baseline), ys, value, colour, axis))
|
|
205
|
+
band = stat2d._bands_layer(pieces, colour, geom, axis, geom.alpha if geom.alpha is not None else 0.35)
|
|
206
|
+
band._axis_labels = dict(getattr(geom, "_axis_labels", None) or {"x": xcol, "y": ycol})
|
|
207
|
+
band._baseline_zero = True
|
|
208
|
+
return band
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _swap(geom) -> _Geom:
|
|
212
|
+
out = copy.copy(geom)
|
|
213
|
+
out.mapping = aes()
|
|
214
|
+
out.mapping.update(swap_mapping(geom.mapping))
|
|
215
|
+
out._violin_levels = getattr(geom, "_y_levels", None)
|
|
216
|
+
out._y_levels = getattr(geom, "_violin_levels", None)
|
|
217
|
+
labels = getattr(geom, "_axis_labels", None)
|
|
218
|
+
if labels:
|
|
219
|
+
out._axis_labels = {_SWAP.get(k, k): v for k, v in labels.items()}
|
|
220
|
+
if getattr(geom, "_annotations", None):
|
|
221
|
+
out._annotations = [dict(a, x=a["y"], y=a["x"]) for a in geom._annotations]
|
|
222
|
+
lock = getattr(geom, "_axis_lock", None)
|
|
223
|
+
if lock:
|
|
224
|
+
out._axis_lock = {_SWAP.get(k, k): v for k, v in lock.items()}
|
|
225
|
+
if hasattr(geom, "nudge_x") or hasattr(geom, "nudge_y"):
|
|
226
|
+
out.nudge_x, out.nudge_y = getattr(geom, "nudge_y", 0.0), getattr(geom, "nudge_x", 0.0)
|
|
227
|
+
if getattr(geom, "_x_domain", None) is not None:
|
|
228
|
+
out._x_domain = None
|
|
229
|
+
out.sort_x = False
|
|
230
|
+
return out
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
_SIDE_SWAP = {"b": "l", "l": "b", "t": "r", "r": "t"}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def flip_rugs(rugs: list) -> list:
|
|
237
|
+
"""geom_rug under coord_flip: x values move to the left side, y to the bottom."""
|
|
238
|
+
out = []
|
|
239
|
+
for rug in rugs:
|
|
240
|
+
clone = copy.copy(rug)
|
|
241
|
+
clone.mapping = aes()
|
|
242
|
+
clone.mapping.update(swap_mapping(rug.mapping))
|
|
243
|
+
clone.sides = "".join(_SIDE_SWAP[side] for side in rug.sides)
|
|
244
|
+
out.append(clone)
|
|
245
|
+
return out
|