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.
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