plotlibs 0.3.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.
- plotlib/__init__.py +38 -0
- plotlibs/__init__.py +110 -0
- plotlibs/backends/__init__.py +0 -0
- plotlibs/backends/renderer.py +520 -0
- plotlibs/colors.py +107 -0
- plotlibs/data.py +208 -0
- plotlibs/db.py +204 -0
- plotlibs/eda.py +157 -0
- plotlibs/fast.py +187 -0
- plotlibs/figure.py +617 -0
- plotlibs/ml.py +205 -0
- plotlibs/pyplot.py +283 -0
- plotlibs/style.py +96 -0
- plotlibs-0.3.0.dist-info/METADATA +228 -0
- plotlibs-0.3.0.dist-info/RECORD +18 -0
- plotlibs-0.3.0.dist-info/WHEEL +5 -0
- plotlibs-0.3.0.dist-info/licenses/LICENSE +21 -0
- plotlibs-0.3.0.dist-info/top_level.txt +2 -0
plotlibs/figure.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""plotlibs.figure — Figure + Axes with matplotlib-compatible API.
|
|
2
|
+
|
|
3
|
+
Speed design:
|
|
4
|
+
- artists are plain dicts (no heavy objects)
|
|
5
|
+
- limits computed vectorized with numpy
|
|
6
|
+
- actual rasterization deferred to backends/renderer.py (Pillow, C-level draws)
|
|
7
|
+
- auto min-max decimation for big lines
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import itertools
|
|
12
|
+
import math
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from . import style
|
|
17
|
+
from .colors import CYCLE, to_rgb
|
|
18
|
+
from .fast import as_xy, decimate, nice_limits
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Axes:
|
|
22
|
+
_ids = itertools.count()
|
|
23
|
+
|
|
24
|
+
def __init__(self, fig: "Figure", rect=(0.125, 0.11, 0.775, 0.77)):
|
|
25
|
+
self.figure = fig
|
|
26
|
+
self.id = next(Axes._ids)
|
|
27
|
+
self.rect = tuple(rect) # (left, bottom, width, height) in 0-1
|
|
28
|
+
self.lines: list[dict] = []
|
|
29
|
+
self.scatters: list[dict] = []
|
|
30
|
+
self.bars: list[dict] = []
|
|
31
|
+
self.hists: list[dict] = []
|
|
32
|
+
self.images: list[dict] = []
|
|
33
|
+
self.fills: list[dict] = []
|
|
34
|
+
self.pies: list[dict] = []
|
|
35
|
+
self.texts: list[dict] = []
|
|
36
|
+
self._hlines: list[dict] = []
|
|
37
|
+
self._vlines: list[dict] = []
|
|
38
|
+
# --- stats / EDA / ML artists (جديد 0.2.0) ---
|
|
39
|
+
self.boxes: list[dict] = []
|
|
40
|
+
self.violins: list[dict] = []
|
|
41
|
+
self.kdes: list[dict] = []
|
|
42
|
+
self.heatmaps: list[dict] = []
|
|
43
|
+
self.areas: list[dict] = []
|
|
44
|
+
self.stems: list[dict] = []
|
|
45
|
+
self._color_idx = 0
|
|
46
|
+
self._xlim = None
|
|
47
|
+
self._ylim = None
|
|
48
|
+
self._xlabel = ""
|
|
49
|
+
self._ylabel = ""
|
|
50
|
+
self._title = ""
|
|
51
|
+
self._legend_labels: list[str] = []
|
|
52
|
+
self._grid = style.rcParams.get("axes.grid", False)
|
|
53
|
+
self._xscale = "linear"
|
|
54
|
+
self._yscale = "linear"
|
|
55
|
+
self.facecolor = style.rcParams.get("axes.facecolor", "white")
|
|
56
|
+
|
|
57
|
+
# ---------- internal ----------
|
|
58
|
+
def _next_color(self, given=None):
|
|
59
|
+
if given is not None:
|
|
60
|
+
return given
|
|
61
|
+
c = CYCLE[self._color_idx % len(CYCLE)]
|
|
62
|
+
self._color_idx += 1
|
|
63
|
+
return c
|
|
64
|
+
|
|
65
|
+
# ---------- plotting API (matplotlib-compatible) ----------
|
|
66
|
+
def plot(self, x, y=None, fmt: str = "", **kw):
|
|
67
|
+
"""plot(x, y) / plot(y) / plot(x, y, 'ro--'). Returns list[Line2D-dict]."""
|
|
68
|
+
color = kw.pop("color", kw.pop("c", None))
|
|
69
|
+
label = kw.pop("label", "")
|
|
70
|
+
lw = kw.pop("linewidth", kw.pop("lw", style.rcParams["lines.linewidth"]))
|
|
71
|
+
ls = kw.pop("linestyle", kw.pop("ls", "-"))
|
|
72
|
+
marker = kw.pop("marker", None)
|
|
73
|
+
ms = kw.pop("markersize", kw.pop("ms", style.rcParams["lines.markersize"]))
|
|
74
|
+
alpha = kw.pop("alpha", 1.0)
|
|
75
|
+
if isinstance(y, str) and fmt == "":
|
|
76
|
+
fmt, y = y, None
|
|
77
|
+
if fmt:
|
|
78
|
+
# parse 'ro--' style fmt
|
|
79
|
+
f = fmt.strip()
|
|
80
|
+
for ch, col in (("b", "b"), ("g", "g"), ("r", "r"), ("c", "c"),
|
|
81
|
+
("m", "m"), ("y", "y"), ("k", "k"), ("w", "w")):
|
|
82
|
+
if ch in f and color is None:
|
|
83
|
+
color = col
|
|
84
|
+
break
|
|
85
|
+
for m in ("o", "s", "^", "v", "D", "x", "+", "*", ".", "d"):
|
|
86
|
+
if m in f:
|
|
87
|
+
marker = marker or m
|
|
88
|
+
if "--" in f:
|
|
89
|
+
ls = "--"
|
|
90
|
+
elif "-." in f:
|
|
91
|
+
ls = "-."
|
|
92
|
+
elif ":" in f:
|
|
93
|
+
ls = ":"
|
|
94
|
+
elif "-" in f:
|
|
95
|
+
ls = "-"
|
|
96
|
+
elif f.strip("bgrcmykwos^vDx+*.d") == "":
|
|
97
|
+
ls = "none"
|
|
98
|
+
x, y = as_xy(x, y)
|
|
99
|
+
line = dict(x=x, y=y, color=self._next_color(color), label=label,
|
|
100
|
+
linewidth=float(lw), linestyle=ls, marker=marker,
|
|
101
|
+
markersize=float(ms), alpha=float(alpha))
|
|
102
|
+
self.lines.append(line)
|
|
103
|
+
if label:
|
|
104
|
+
self._legend_labels.append(label)
|
|
105
|
+
return [line]
|
|
106
|
+
|
|
107
|
+
def scatter(self, x, y, s=None, c=None, color=None, label="", alpha=1.0,
|
|
108
|
+
marker="o", **kw):
|
|
109
|
+
x = np.asarray(x, dtype=float).ravel()
|
|
110
|
+
y = np.asarray(y, dtype=float).ravel()
|
|
111
|
+
n = min(x.size, y.size)
|
|
112
|
+
x, y = x[:n], y[:n]
|
|
113
|
+
col = c if c is not None else color
|
|
114
|
+
if isinstance(col, (list, np.ndarray)) and np.asarray(col).ndim == 2:
|
|
115
|
+
colors = [tuple(v) for v in np.asarray(col)]
|
|
116
|
+
elif isinstance(col, (list, np.ndarray)) and np.asarray(col).ndim == 1 and len(np.asarray(col)) == n and n > 4:
|
|
117
|
+
# scalar-mapped -> simple colormap (viridis approx: blue->yellow)
|
|
118
|
+
v = np.asarray(col, dtype=float)
|
|
119
|
+
v = (v - v.min()) / (v.ptp() or 1.0)
|
|
120
|
+
colors = [(int(68 + v_i * 187), int(1 + v_i * 230), int(84 + v_i * 60)) for v_i in v]
|
|
121
|
+
else:
|
|
122
|
+
colors = self._next_color(col)
|
|
123
|
+
sc = dict(x=x, y=y, s=s, colors=colors, label=label, alpha=float(alpha), marker=marker)
|
|
124
|
+
self.scatters.append(sc)
|
|
125
|
+
if label:
|
|
126
|
+
self._legend_labels.append(label)
|
|
127
|
+
return sc
|
|
128
|
+
|
|
129
|
+
def bar(self, x, height, width=0.8, color=None, label="", alpha=1.0, **kw):
|
|
130
|
+
xa = np.asarray(x).ravel()
|
|
131
|
+
h = np.asarray(height, dtype=float).ravel()
|
|
132
|
+
if xa.dtype.kind in "USO" or (xa.size != h.size):
|
|
133
|
+
# categorical or mismatched -> positions 0..n-1
|
|
134
|
+
xpos = np.arange(h.size, dtype=float)
|
|
135
|
+
try:
|
|
136
|
+
self._xticklabels = [str(v) for v in xa.ravel()[: h.size]]
|
|
137
|
+
self._xtickpos = xpos
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
else:
|
|
141
|
+
xpos = xa.astype(float)
|
|
142
|
+
bottom = kw.pop("bottom", 0.0)
|
|
143
|
+
b = np.asarray(bottom, dtype=float).ravel()
|
|
144
|
+
if b.size == 1:
|
|
145
|
+
b = np.full(h.size, float(b[0]))
|
|
146
|
+
bars = dict(x=np.asarray(xpos).ravel(), h=h, w=float(width) if np.ndim(width) == 0 else np.asarray(width),
|
|
147
|
+
bottom=b, color=self._next_color(color), label=label, alpha=float(alpha), horizontal=False)
|
|
148
|
+
self.bars.append(bars)
|
|
149
|
+
if label:
|
|
150
|
+
self._legend_labels.append(label)
|
|
151
|
+
return bars
|
|
152
|
+
|
|
153
|
+
def barh(self, y, width, height=0.8, color=None, label="", alpha=1.0, **kw):
|
|
154
|
+
y = np.asarray(y).ravel()
|
|
155
|
+
w = np.asarray(width, dtype=float).ravel()
|
|
156
|
+
if y.size != w.size and np.asarray(y).dtype.kind in "iuf":
|
|
157
|
+
y = np.arange(w.size)
|
|
158
|
+
bars = dict(x=y, h=w, w=float(height) if np.ndim(height) == 0 else np.asarray(height),
|
|
159
|
+
bottom=np.zeros(w.size), color=self._next_color(color),
|
|
160
|
+
label=label, alpha=float(alpha), horizontal=True)
|
|
161
|
+
self.bars.append(bars)
|
|
162
|
+
if label:
|
|
163
|
+
self._legend_labels.append(label)
|
|
164
|
+
return bars
|
|
165
|
+
|
|
166
|
+
def hist(self, x, bins=10, color=None, label="", alpha=1.0, density=False, **kw):
|
|
167
|
+
x = np.asarray(x, dtype=float).ravel()
|
|
168
|
+
x = x[np.isfinite(x)]
|
|
169
|
+
counts, edges = np.histogram(x, bins=bins, density=density)
|
|
170
|
+
h = dict(counts=counts, edges=edges, color=self._next_color(color),
|
|
171
|
+
label=label, alpha=float(alpha))
|
|
172
|
+
self.hists.append(h)
|
|
173
|
+
if label:
|
|
174
|
+
self._legend_labels.append(label)
|
|
175
|
+
return counts, edges, h
|
|
176
|
+
|
|
177
|
+
def imshow(self, X, cmap=None, vmin=None, vmax=None, origin="upper",
|
|
178
|
+
extent=None, aspect="auto", **kw):
|
|
179
|
+
img = dict(data=np.asarray(X), cmap=cmap or style.rcParams["image.cmap"],
|
|
180
|
+
vmin=vmin, vmax=vmax, origin=origin, extent=extent)
|
|
181
|
+
self.images.append(img)
|
|
182
|
+
return img
|
|
183
|
+
|
|
184
|
+
def pie(self, x, labels=None, autopct=None, colors=None, startangle=0, **kw):
|
|
185
|
+
fracs = np.asarray(x, dtype=float).ravel()
|
|
186
|
+
s = fracs.sum() or 1.0
|
|
187
|
+
fracs = fracs / s
|
|
188
|
+
p = dict(fracs=fracs, labels=list(labels) if labels is not None else None,
|
|
189
|
+
autopct=autopct, colors=colors, startangle=float(startangle))
|
|
190
|
+
self.pies.append(p)
|
|
191
|
+
return fracs
|
|
192
|
+
|
|
193
|
+
def fill_between(self, x, y1, y2=0, color=None, alpha=0.35, label="", **kw):
|
|
194
|
+
x = np.asarray(x, dtype=float).ravel()
|
|
195
|
+
y1 = np.asarray(y1, dtype=float).ravel()
|
|
196
|
+
y2 = np.asarray(y2, dtype=float).ravel() if np.ndim(y2) else np.full_like(x, float(y2))
|
|
197
|
+
n = min(x.size, y1.size, y2.size)
|
|
198
|
+
f = dict(x=x[:n], y1=y1[:n], y2=y2[:n], color=self._next_color(color),
|
|
199
|
+
alpha=float(alpha), label=label)
|
|
200
|
+
self.fills.append(f)
|
|
201
|
+
if label:
|
|
202
|
+
self._legend_labels.append(label)
|
|
203
|
+
return f
|
|
204
|
+
|
|
205
|
+
def step(self, x, y, color=None, label="", linewidth=None, **kw):
|
|
206
|
+
x, y = as_xy(x, y)
|
|
207
|
+
# convert to step path (pre)
|
|
208
|
+
xs = np.repeat(x, 2)[1:]
|
|
209
|
+
xs = np.append(xs, x[-1]) if x.size else xs
|
|
210
|
+
ys = np.repeat(y, 2)[:-1] if y.size else y
|
|
211
|
+
n = min(xs.size, ys.size)
|
|
212
|
+
return self.plot(xs[:n], ys[:n], color=color, label=label,
|
|
213
|
+
linewidth=linewidth or style.rcParams["lines.linewidth"])[0]
|
|
214
|
+
|
|
215
|
+
def errorbar(self, x, y, yerr=None, xerr=None, fmt="o", color=None,
|
|
216
|
+
label="", capsize=3, **kw):
|
|
217
|
+
line = self.plot(x, y, fmt, color=color, label=label, **kw)[0]
|
|
218
|
+
if yerr is not None:
|
|
219
|
+
x_, y_ = as_xy(x, y)
|
|
220
|
+
e = np.asarray(yerr, dtype=float).ravel()
|
|
221
|
+
if e.size == 1:
|
|
222
|
+
e = np.full_like(y_, float(e[0]))
|
|
223
|
+
self._vlines.append(dict(kind="errorbar-y", x=x_, y=y_, e=e[: len(x_)],
|
|
224
|
+
color=line["color"]))
|
|
225
|
+
if xerr is not None:
|
|
226
|
+
x_, y_ = as_xy(x, y)
|
|
227
|
+
e = np.asarray(xerr, dtype=float).ravel()
|
|
228
|
+
if e.size == 1:
|
|
229
|
+
e = np.full_like(x_, float(e[0]))
|
|
230
|
+
self._hlines.append(dict(kind="errorbar-x", x=x_, y=y_, e=e[: len(x_)],
|
|
231
|
+
color=line["color"]))
|
|
232
|
+
return line
|
|
233
|
+
|
|
234
|
+
def axhline(self, y=0, color="k", linestyle="--", linewidth=1.0, **kw):
|
|
235
|
+
self._hlines.append(dict(kind="axhline", y=float(y), color=color,
|
|
236
|
+
linestyle=linestyle, linewidth=float(linewidth)))
|
|
237
|
+
return self._hlines[-1]
|
|
238
|
+
|
|
239
|
+
def axvline(self, x=0, color="k", linestyle="--", linewidth=1.0, **kw):
|
|
240
|
+
self._vlines.append(dict(kind="axvline", x=float(x), color=color,
|
|
241
|
+
linestyle=linestyle, linewidth=float(linewidth)))
|
|
242
|
+
return self._vlines[-1]
|
|
243
|
+
|
|
244
|
+
def text(self, x, y, s, fontsize=None, color="black", **kw):
|
|
245
|
+
t = dict(x=x, y=y, s=str(s), fontsize=fontsize or style.rcParams["font.size"],
|
|
246
|
+
color=color, data_coords=True)
|
|
247
|
+
self.texts.append(t)
|
|
248
|
+
return t
|
|
249
|
+
|
|
250
|
+
# ---------- stats / EDA (جديد — لجعل المكتبة مشهورة عند المحللين) ----------
|
|
251
|
+
def boxplot(self, data, labels=None, color=None, **kw):
|
|
252
|
+
"""boxplot(list of arrays). متوافق مع matplotlib."""
|
|
253
|
+
if isinstance(data, np.ndarray) and data.ndim == 2:
|
|
254
|
+
cols = [data[:, i] for i in range(data.shape[1])]
|
|
255
|
+
elif isinstance(data, (list, tuple)) and data and np.ndim(data[0]) == 0:
|
|
256
|
+
cols = [np.asarray(data, dtype=float)]
|
|
257
|
+
else:
|
|
258
|
+
try:
|
|
259
|
+
cols = [np.asarray(c, dtype=float).ravel() for c in data]
|
|
260
|
+
except Exception:
|
|
261
|
+
cols = [np.asarray(data, dtype=float).ravel()]
|
|
262
|
+
b = dict(cols=cols, labels=list(labels) if labels is not None else None,
|
|
263
|
+
color=self._next_color(color))
|
|
264
|
+
self.boxes.append(b)
|
|
265
|
+
return b
|
|
266
|
+
|
|
267
|
+
def violinplot(self, data, labels=None, color=None, points=120, **kw):
|
|
268
|
+
if isinstance(data, np.ndarray) and data.ndim == 2:
|
|
269
|
+
cols = [data[:, i] for i in range(data.shape[1])]
|
|
270
|
+
else:
|
|
271
|
+
try:
|
|
272
|
+
cols = [np.asarray(c, dtype=float).ravel() for c in data]
|
|
273
|
+
except Exception:
|
|
274
|
+
cols = [np.asarray(data, dtype=float).ravel()]
|
|
275
|
+
v = dict(cols=cols, labels=list(labels) if labels is not None else None,
|
|
276
|
+
color=self._next_color(color), points=int(points))
|
|
277
|
+
self.violins.append(v)
|
|
278
|
+
return v
|
|
279
|
+
|
|
280
|
+
def kde(self, data, color=None, label="", fill=True, bw=None, **kw):
|
|
281
|
+
"""منحنى الكثافة KDE (بديل seaborn.kdeplot)."""
|
|
282
|
+
from .fast import gaussian_kde_1d
|
|
283
|
+
v = np.asarray(data, dtype=float).ravel()
|
|
284
|
+
xs, ys = gaussian_kde_1d(v, bw=bw) if v.size else (np.array([0, 1]), np.zeros(2))
|
|
285
|
+
k = dict(x=xs, y=ys, color=self._next_color(color), label=label,
|
|
286
|
+
fill=bool(fill))
|
|
287
|
+
self.kdes.append(k)
|
|
288
|
+
if label:
|
|
289
|
+
self._legend_labels.append(label)
|
|
290
|
+
return k
|
|
291
|
+
|
|
292
|
+
density = kde
|
|
293
|
+
|
|
294
|
+
def heatmap(self, matrix, xticks=None, yticks=None, annot=False, cmap="viridis",
|
|
295
|
+
vmin=None, vmax=None, **kw):
|
|
296
|
+
m = np.asarray(matrix, dtype=float)
|
|
297
|
+
h = dict(data=m, xticks=list(xticks) if xticks is not None else None,
|
|
298
|
+
yticks=list(yticks) if yticks is not None else None,
|
|
299
|
+
annot=bool(annot), cmap=cmap, vmin=vmin, vmax=vmax)
|
|
300
|
+
self.heatmaps.append(h)
|
|
301
|
+
return h
|
|
302
|
+
|
|
303
|
+
def corr(self, data, cols=None, annot=True, cmap="viridis", **kw):
|
|
304
|
+
"""مصفوفة ارتباط جاهزة من DataFrame/dict مباشرة."""
|
|
305
|
+
from .data import corr_matrix
|
|
306
|
+
if isinstance(data, np.ndarray):
|
|
307
|
+
m = np.asarray(data, dtype=float)
|
|
308
|
+
c = np.corrcoef(m, rowvar=False) if m.ndim == 2 else np.eye(1)
|
|
309
|
+
labels = cols or [f"c{i}" for i in range(c.shape[0])]
|
|
310
|
+
return self.heatmap(np.nan_to_num(c), xticks=labels, yticks=labels,
|
|
311
|
+
annot=annot, cmap=cmap)
|
|
312
|
+
c, labels = corr_matrix(data, cols)
|
|
313
|
+
return self.heatmap(c, xticks=labels, yticks=labels, annot=annot, cmap=cmap,
|
|
314
|
+
vmin=-1, vmax=1)
|
|
315
|
+
|
|
316
|
+
def corrcoef(self, *a, **k):
|
|
317
|
+
return self.corr(*a, **k)
|
|
318
|
+
|
|
319
|
+
def countplot(self, values, color=None, label="", **kw):
|
|
320
|
+
"""رسم تكرار الفئات (بديل seaborn.countplot)."""
|
|
321
|
+
vals = np.asarray(values).ravel()
|
|
322
|
+
uniq, counts = np.unique(vals, return_counts=True)
|
|
323
|
+
order = np.argsort(-counts, kind="stable")
|
|
324
|
+
uniq, counts = uniq[order], counts[order]
|
|
325
|
+
xpos = np.arange(len(uniq), dtype=float)
|
|
326
|
+
b = dict(x=xpos, h=counts.astype(float), w=np.full(len(uniq), 0.6),
|
|
327
|
+
bottom=np.zeros(len(uniq)), color=self._next_color(color),
|
|
328
|
+
label=label, alpha=1.0, horizontal=False)
|
|
329
|
+
self.bars.append(b)
|
|
330
|
+
try:
|
|
331
|
+
self._xticklabels = [str(v) for v in uniq]
|
|
332
|
+
self._xtickpos = xpos
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
if label:
|
|
336
|
+
self._legend_labels.append(label)
|
|
337
|
+
return b
|
|
338
|
+
|
|
339
|
+
count = countplot
|
|
340
|
+
|
|
341
|
+
def area(self, x, *ys, labels=None, colors=None, alpha=0.5, **kw):
|
|
342
|
+
"""منحنى مساحي مكدّس (stackplot)."""
|
|
343
|
+
x = np.asarray(x, dtype=float).ravel()
|
|
344
|
+
arrs = [np.asarray(y, dtype=float).ravel()[: x.size] for y in ys]
|
|
345
|
+
a = dict(x=x, ys=arrs,
|
|
346
|
+
labels=list(labels) if labels is not None else [""] * len(arrs),
|
|
347
|
+
colors=list(colors) if colors is not None else [self._next_color(None) for _ in arrs],
|
|
348
|
+
alpha=float(alpha))
|
|
349
|
+
self.areas.append(a)
|
|
350
|
+
for lb in a["labels"]:
|
|
351
|
+
if lb:
|
|
352
|
+
self._legend_labels.append(lb)
|
|
353
|
+
return a
|
|
354
|
+
|
|
355
|
+
stackplot = area
|
|
356
|
+
|
|
357
|
+
def hist2d(self, x, y, bins=30, cmap="viridis", **kw):
|
|
358
|
+
"""كثافة ثنائية الأبعاد (بديل plt.hist2d)."""
|
|
359
|
+
x = np.asarray(x, dtype=float).ravel()
|
|
360
|
+
y = np.asarray(y, dtype=float).ravel()
|
|
361
|
+
n = min(x.size, y.size)
|
|
362
|
+
H, xe, ye = np.histogram2d(x[:n], y[:n], bins=bins)
|
|
363
|
+
h = dict(data=H.T, xticks=None, yticks=None, annot=False, cmap=cmap,
|
|
364
|
+
vmin=None, vmax=None, extent=(xe[0], xe[-1], ye[0], ye[-1]))
|
|
365
|
+
self.heatmaps.append(h)
|
|
366
|
+
return H, xe, ye, h
|
|
367
|
+
|
|
368
|
+
def stem(self, x, y=None, color=None, label="", **kw):
|
|
369
|
+
if y is None:
|
|
370
|
+
y = np.asarray(x, dtype=float).ravel()
|
|
371
|
+
x = np.arange(y.size, dtype=float)
|
|
372
|
+
s = dict(x=np.asarray(x, dtype=float).ravel(),
|
|
373
|
+
y=np.asarray(y, dtype=float).ravel(),
|
|
374
|
+
color=self._next_color(color), label=label)
|
|
375
|
+
self.stems.append(s)
|
|
376
|
+
if label:
|
|
377
|
+
self._legend_labels.append(label)
|
|
378
|
+
return s
|
|
379
|
+
|
|
380
|
+
def table(self, data, col_labels=None, row_labels=None, **kw):
|
|
381
|
+
t = dict(kind="table")
|
|
382
|
+
self.texts.append(dict(x=0.5, y=-0.05, s=f"[table {np.asarray(data).shape}]",
|
|
383
|
+
fontsize=9, color="black", data_coords=False))
|
|
384
|
+
return t
|
|
385
|
+
|
|
386
|
+
# ---------- cosmetics, matplotlib-compatible ----------
|
|
387
|
+
def set_xlim(self, left=None, right=None):
|
|
388
|
+
l, r = self.get_xlim()
|
|
389
|
+
if left is not None:
|
|
390
|
+
l = left
|
|
391
|
+
if right is not None:
|
|
392
|
+
r = right
|
|
393
|
+
self._xlim = (l, r)
|
|
394
|
+
return self._xlim
|
|
395
|
+
|
|
396
|
+
def set_ylim(self, bottom=None, top=None):
|
|
397
|
+
b, t = self.get_ylim()
|
|
398
|
+
if bottom is not None:
|
|
399
|
+
b = bottom
|
|
400
|
+
if top is not None:
|
|
401
|
+
t = top
|
|
402
|
+
self._ylim = (b, t)
|
|
403
|
+
return self._ylim
|
|
404
|
+
|
|
405
|
+
def get_xlim(self):
|
|
406
|
+
if self._xlim is not None:
|
|
407
|
+
return self._xlim
|
|
408
|
+
return self._auto_limits("x")
|
|
409
|
+
|
|
410
|
+
def get_ylim(self):
|
|
411
|
+
if self._ylim is not None:
|
|
412
|
+
return self._ylim
|
|
413
|
+
return self._auto_limits("y")
|
|
414
|
+
|
|
415
|
+
def _auto_limits(self, axis):
|
|
416
|
+
lo, hi = np.inf, -np.inf
|
|
417
|
+
def upd(v):
|
|
418
|
+
nonlocal lo, hi
|
|
419
|
+
try:
|
|
420
|
+
v = np.asarray(v, dtype=float).ravel()
|
|
421
|
+
except Exception:
|
|
422
|
+
return
|
|
423
|
+
v = v[np.isfinite(v)]
|
|
424
|
+
if v.size:
|
|
425
|
+
lo, hi = min(lo, v.min()), max(hi, v.max())
|
|
426
|
+
for ln in self.lines:
|
|
427
|
+
upd(ln["x"] if axis == "x" else ln["y"])
|
|
428
|
+
for sc in self.scatters:
|
|
429
|
+
upd(sc["x"] if axis == "x" else sc["y"])
|
|
430
|
+
for b in self.bars:
|
|
431
|
+
if not b["horizontal"]:
|
|
432
|
+
if axis == "x":
|
|
433
|
+
upd(b["x"])
|
|
434
|
+
else:
|
|
435
|
+
upd(np.concatenate([b["bottom"], b["bottom"] + b["h"]]))
|
|
436
|
+
else:
|
|
437
|
+
if axis == "x":
|
|
438
|
+
upd(np.concatenate([b["bottom"], b["bottom"] + b["h"]]))
|
|
439
|
+
else:
|
|
440
|
+
upd(b["x"])
|
|
441
|
+
for h in self.hists:
|
|
442
|
+
if axis == "x":
|
|
443
|
+
upd(h["edges"])
|
|
444
|
+
else:
|
|
445
|
+
upd(np.append(h["counts"], 0))
|
|
446
|
+
for f in self.fills:
|
|
447
|
+
if axis == "x":
|
|
448
|
+
upd(f["x"])
|
|
449
|
+
else:
|
|
450
|
+
upd(np.concatenate([f["y1"], f["y2"]]))
|
|
451
|
+
for im in self.images:
|
|
452
|
+
if im["extent"] is not None:
|
|
453
|
+
e = im["extent"]
|
|
454
|
+
upd([e[0], e[1]] if axis == "x" else [e[2], e[3]])
|
|
455
|
+
else:
|
|
456
|
+
d = im["data"]
|
|
457
|
+
upd([0, d.shape[1]] if axis == "x" else [0, d.shape[0]])
|
|
458
|
+
for k in self.kdes:
|
|
459
|
+
upd(k["x"] if axis == "x" else k["y"])
|
|
460
|
+
for b in self.boxes + self.violins:
|
|
461
|
+
if axis == "x":
|
|
462
|
+
upd([1, len(b["cols"])])
|
|
463
|
+
else:
|
|
464
|
+
for c in b["cols"]:
|
|
465
|
+
upd(c)
|
|
466
|
+
for hm in self.heatmaps:
|
|
467
|
+
d = np.asarray(hm["data"])
|
|
468
|
+
if axis == "x":
|
|
469
|
+
upd([0, d.shape[1]])
|
|
470
|
+
else:
|
|
471
|
+
upd([0, d.shape[0]])
|
|
472
|
+
for a in self.areas:
|
|
473
|
+
if axis == "x":
|
|
474
|
+
upd(a["x"])
|
|
475
|
+
else:
|
|
476
|
+
if a["ys"]:
|
|
477
|
+
try:
|
|
478
|
+
upd(np.sum(np.stack([np.asarray(v, dtype=float) for v in a["ys"]]), axis=0))
|
|
479
|
+
for v in a["ys"]:
|
|
480
|
+
upd(v)
|
|
481
|
+
except Exception:
|
|
482
|
+
pass
|
|
483
|
+
for s in self.stems:
|
|
484
|
+
upd(s["x"] if axis == "x" else s["y"])
|
|
485
|
+
if not np.isfinite(lo):
|
|
486
|
+
return (0.0, 1.0)
|
|
487
|
+
a, bb = nice_limits(lo, hi)
|
|
488
|
+
return (a, bb)
|
|
489
|
+
|
|
490
|
+
def set_xlabel(self, s):
|
|
491
|
+
self._xlabel = str(s)
|
|
492
|
+
|
|
493
|
+
def set_ylabel(self, s):
|
|
494
|
+
self._ylabel = str(s)
|
|
495
|
+
|
|
496
|
+
def set_title(self, s):
|
|
497
|
+
self._title = str(s)
|
|
498
|
+
|
|
499
|
+
xlabel = set_xlabel
|
|
500
|
+
ylabel = set_ylabel
|
|
501
|
+
title = set_title
|
|
502
|
+
|
|
503
|
+
def set_xscale(self, s):
|
|
504
|
+
self._xscale = s
|
|
505
|
+
|
|
506
|
+
def set_yscale(self, s):
|
|
507
|
+
self._yscale = s
|
|
508
|
+
|
|
509
|
+
def grid(self, visible=True, **kw):
|
|
510
|
+
self._grid = bool(visible)
|
|
511
|
+
|
|
512
|
+
def legend(self, *a, **kw):
|
|
513
|
+
# labels already collected; renderer draws them
|
|
514
|
+
return self._legend_labels
|
|
515
|
+
|
|
516
|
+
def tick_params(self, **kw):
|
|
517
|
+
pass
|
|
518
|
+
|
|
519
|
+
def set_xticks(self, t):
|
|
520
|
+
self._xticks = list(t)
|
|
521
|
+
|
|
522
|
+
def set_yticks(self, t):
|
|
523
|
+
self._yticks = list(t)
|
|
524
|
+
|
|
525
|
+
def cla(self):
|
|
526
|
+
self.__init__(self.figure, self.rect)
|
|
527
|
+
|
|
528
|
+
# ---------- draw / save ----------
|
|
529
|
+
def _rendered_lines(self):
|
|
530
|
+
"""Lines after fast decimation — the core speedup vs matplotlib."""
|
|
531
|
+
out = []
|
|
532
|
+
for ln in self.lines:
|
|
533
|
+
x, y = ln["x"], ln["y"]
|
|
534
|
+
if x.size > 2000:
|
|
535
|
+
x, y = decimate(x, y)
|
|
536
|
+
out.append({**ln, "x": x, "y": y})
|
|
537
|
+
return out
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class Figure:
|
|
541
|
+
def __init__(self, figsize=None, dpi=None, facecolor=None, num=None):
|
|
542
|
+
fs = figsize or style.rcParams["figure.figsize"]
|
|
543
|
+
self.figsize = tuple(fs)
|
|
544
|
+
self.dpi = int(dpi or style.rcParams["figure.dpi"])
|
|
545
|
+
self.facecolor = facecolor or style.rcParams["figure.facecolor"]
|
|
546
|
+
self.num = num
|
|
547
|
+
self.axes: list[Axes] = []
|
|
548
|
+
self._suptitle = ""
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def pixel_size(self):
|
|
552
|
+
return (int(self.figsize[0] * self.dpi), int(self.figsize[1] * self.dpi))
|
|
553
|
+
|
|
554
|
+
def add_subplot(self, *args, **kw):
|
|
555
|
+
# support (nrows, ncols, index) or (111)
|
|
556
|
+
if len(args) == 1 and isinstance(args[0], int) and args[0] >= 100:
|
|
557
|
+
code = args[0]
|
|
558
|
+
nr, nc, ix = code // 100, (code // 10) % 10, code % 10
|
|
559
|
+
elif len(args) == 3:
|
|
560
|
+
nr, nc, ix = args
|
|
561
|
+
elif len(args) == 1 and isinstance(args[0], tuple):
|
|
562
|
+
nr, nc, ix = args[0]
|
|
563
|
+
else:
|
|
564
|
+
nr, nc, ix = 1, 1, 1
|
|
565
|
+
left = 0.125 + (0.775 / nc) * ((ix - 1) % nc)
|
|
566
|
+
# row from top
|
|
567
|
+
row = (ix - 1) // nc
|
|
568
|
+
h = 0.77 / nr
|
|
569
|
+
bottom = 0.11 + 0.77 - (row + 1) * h
|
|
570
|
+
w = 0.775 / nc
|
|
571
|
+
ax = Axes(self, rect=(left, bottom, max(w - 0.03, 0.05), max(h - 0.05, 0.05)))
|
|
572
|
+
self.axes.append(ax)
|
|
573
|
+
return ax
|
|
574
|
+
|
|
575
|
+
def add_axes(self, rect):
|
|
576
|
+
ax = Axes(self, rect=tuple(rect))
|
|
577
|
+
self.axes.append(ax)
|
|
578
|
+
return ax
|
|
579
|
+
|
|
580
|
+
def subplots(self, nrows=1, ncols=1, **kw):
|
|
581
|
+
for i in range(1, nrows * ncols + 1):
|
|
582
|
+
self.add_subplot(nrows, ncols, i)
|
|
583
|
+
if nrows * ncols == 1:
|
|
584
|
+
return self.axes[0]
|
|
585
|
+
import numpy as _np
|
|
586
|
+
return _np.array(self.axes, dtype=object).reshape(nrows, ncols)
|
|
587
|
+
|
|
588
|
+
def suptitle(self, s):
|
|
589
|
+
self._suptitle = str(s)
|
|
590
|
+
|
|
591
|
+
def tight_layout(self, **kw):
|
|
592
|
+
pass
|
|
593
|
+
|
|
594
|
+
def savefig(self, fname, dpi=None, **kw):
|
|
595
|
+
from .backends.renderer import render_figure
|
|
596
|
+
render_figure(self, fname, dpi=dpi or self.dpi)
|
|
597
|
+
|
|
598
|
+
def show(self):
|
|
599
|
+
from .backends.renderer import render_figure
|
|
600
|
+
render_figure(self, None)
|
|
601
|
+
|
|
602
|
+
def clf(self):
|
|
603
|
+
self.axes.clear()
|
|
604
|
+
|
|
605
|
+
def canvas_draw(self):
|
|
606
|
+
from .backends.renderer import render_to_image
|
|
607
|
+
return render_to_image(self)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def figure(figsize=None, dpi=None, facecolor=None, num=None):
|
|
611
|
+
return Figure(figsize=figsize, dpi=dpi, facecolor=facecolor, num=num)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def subplots(nrows=1, ncols=1, figsize=None, dpi=None, **kw):
|
|
615
|
+
fig = Figure(figsize=figsize, dpi=dpi)
|
|
616
|
+
axs = fig.subplots(nrows, ncols)
|
|
617
|
+
return fig, axs
|