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 ADDED
@@ -0,0 +1,38 @@
1
+ """Deprecated alias: ``import plotlib`` now maps to ``plotlibs``.
2
+
3
+ The library was renamed from ``plotlib`` to ``plotlibs`` (see
4
+ https://github.com/salim-studio/plotlibs). This shim keeps old code working.
5
+ Please migrate::
6
+
7
+ # old
8
+ import plotlib as pl
9
+ # new
10
+ import plotlibs as pl
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import warnings
15
+
16
+ warnings.warn(
17
+ "The package was renamed 'plotlib' -> 'plotlibs'. "
18
+ "Please use 'import plotlibs as pl'. The 'plotlib' alias will be removed in 1.0.",
19
+ DeprecationWarning,
20
+ stacklevel=2,
21
+ )
22
+
23
+ from plotlibs import * # noqa: F401,F403
24
+ from plotlibs import ( # noqa: F401
25
+ Figure, figure, subplots, plt, pyplot, style, rcParams,
26
+ data, db, eda, ml,
27
+ plot, scatter, bar, barh, hist, imshow, pie, fill_between,
28
+ step, errorbar, axhline, axvline, boxplot, violinplot, kde, density,
29
+ heatmap, corr, countplot, area, stackplot, hist2d, stem,
30
+ xlabel, ylabel, title, legend, grid, xlim, ylim,
31
+ savefig, show, close, gca, gcf,
32
+ load_csv, save_csv, describe, db_connect, read_sql, to_sql,
33
+ quick_eda, scatter_matrix, plot_missing,
34
+ plot_history, plot_confusion_matrix, plot_roc, plot_pr,
35
+ plot_feature_importance, figure_to_image,
36
+ )
37
+
38
+ __version__ = "0.3.0"
plotlibs/__init__.py ADDED
@@ -0,0 +1,110 @@
1
+ """plotlibs — fast, beautiful Python plotting for everyone.
2
+
3
+ Drop-in matplotlib alternative (4x faster), plus one-liners for
4
+ DataFrames, SQL databases, EDA, machine learning and deep learning.
5
+
6
+ import plotlibs as pl
7
+ pl.plot([1, 2, 3], [1, 4, 9], label="x^2")
8
+ pl.xlabel("x"); pl.ylabel("y"); pl.legend(); pl.savefig("out.png")
9
+
10
+ # Data analysts:
11
+ df = pl.load_csv("sales.csv")
12
+ pl.quick_eda(df)
13
+
14
+ # Databases:
15
+ conn = pl.db_connect("sales.db")
16
+ df2 = pl.read_sql("SELECT * FROM sales", conn)
17
+
18
+ # ML / DL:
19
+ pl.plot_history({"loss": [...], "val_loss": [...]})
20
+ pl.plot_confusion_matrix(y_true, y_pred)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from . import pyplot as plt
25
+ from .figure import Figure, figure, subplots
26
+ from . import style
27
+ from .style import rcParams
28
+
29
+ # Data + databases + EDA + ML
30
+ from . import data as data
31
+ from . import db as db
32
+ from . import eda as eda
33
+ from . import ml as ml
34
+
35
+ __version__ = "0.3.0"
36
+ __brand__ = "plotlibs"
37
+ __all__ = ["Figure", "figure", "subplots", "plt", "pyplot", "style", "rcParams",
38
+ "data", "db", "eda", "ml",
39
+ "plot", "scatter", "bar", "barh", "hist", "imshow", "pie",
40
+ "fill_between", "boxplot", "violinplot", "kde", "heatmap",
41
+ "corr", "countplot", "area", "hist2d", "stem",
42
+ "savefig", "show", "figure_to_image",
43
+ "load_csv", "read_sql", "db_connect", "quick_eda",
44
+ "plot_history", "plot_confusion_matrix", "plot_roc"]
45
+
46
+ from . import pyplot as pyplot # noqa: E402
47
+
48
+ plot = pyplot.plot
49
+ scatter = pyplot.scatter
50
+ bar = pyplot.bar
51
+ barh = pyplot.barh
52
+ hist = pyplot.hist
53
+ imshow = pyplot.imshow
54
+ pie = pyplot.pie
55
+ fill_between = pyplot.fill_between
56
+ step = pyplot.step
57
+ errorbar = pyplot.errorbar
58
+ axhline = pyplot.axhline
59
+ axvline = pyplot.axvline
60
+ boxplot = pyplot.boxplot
61
+ violinplot = pyplot.violinplot
62
+ kde = pyplot.kde
63
+ density = pyplot.kde
64
+ heatmap = pyplot.heatmap
65
+ corr = pyplot.corr
66
+ countplot = pyplot.countplot
67
+ area = pyplot.area
68
+ stackplot = pyplot.area
69
+ hist2d = pyplot.hist2d
70
+ stem = pyplot.stem
71
+ xlabel = pyplot.xlabel
72
+ ylabel = pyplot.ylabel
73
+ title = pyplot.title
74
+ legend = pyplot.legend
75
+ grid = pyplot.grid
76
+ xlim = pyplot.xlim
77
+ ylim = pyplot.ylim
78
+ savefig = pyplot.savefig
79
+ show = pyplot.show
80
+ close = pyplot.close
81
+ gca = pyplot.gca
82
+ gcf = pyplot.gcf
83
+ figure_fn = figure
84
+ subplots = subplots
85
+
86
+ # --- data / DB shortcuts ---
87
+ load_csv = data.load_csv
88
+ save_csv = data.save_csv
89
+ describe = data.describe
90
+
91
+ db_connect = db.connect
92
+ read_sql = db.read_sql
93
+ to_sql = db.to_sql
94
+
95
+ # --- EDA shortcuts ---
96
+ quick_eda = eda.quick_eda
97
+ scatter_matrix = eda.scatter_matrix
98
+ plot_missing = eda.plot_missing
99
+
100
+ # --- ML/DL shortcuts ---
101
+ plot_history = ml.plot_history
102
+ plot_confusion_matrix = ml.plot_confusion_matrix
103
+ plot_roc = ml.plot_roc
104
+ plot_pr = ml.plot_pr
105
+ plot_feature_importance = ml.plot_feature_importance
106
+
107
+
108
+ def figure_to_image(fig=None):
109
+ from .backends.renderer import render_to_image
110
+ return render_to_image(fig or pyplot.gcf())
File without changes
@@ -0,0 +1,520 @@
1
+ """plotlibs.backends.renderer — Pillow-based fast raster renderer."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import math
6
+
7
+ import numpy as np
8
+ from PIL import Image, ImageDraw, ImageFont
9
+
10
+ from ..colors import to_rgb
11
+ from ..fast import fmt_tick, nice_ticks
12
+ from ..style import rcParams
13
+
14
+ _FONT_CACHE: dict[int, object] = {}
15
+
16
+
17
+ def _font(size: int):
18
+ size = max(8, int(size))
19
+ if size not in _FONT_CACHE:
20
+ try:
21
+ _FONT_CACHE[size] = ImageFont.load_default(size=size)
22
+ except TypeError:
23
+ _FONT_CACHE[size] = ImageFont.load_default()
24
+ return _FONT_CACHE[size]
25
+
26
+
27
+ def _parse_color(c):
28
+ if isinstance(c, tuple) and len(c) == 3 and all(isinstance(v, (int, np.integer)) for v in c):
29
+ return tuple(int(v) for v in c)
30
+ return to_rgb(c)
31
+
32
+
33
+ def _cmap_color(name, t: float):
34
+ """Tiny fast viridis/gray/jet approximation, vectorized externally."""
35
+ t = max(0.0, min(1.0, float(t)))
36
+ if name in ("gray", "grey", "binary", "gist_yarg"):
37
+ v = int(t * 255)
38
+ return (v, v, v)
39
+ if name in ("hot",):
40
+ return (int(t * 255), int(t * t * 255), int(t * t * t * 128))
41
+ if name in ("cool", "jet", "hsv", "rainbow"):
42
+ r = int(255 * max(0, min(1, 1.5 - abs(4 * t - 3))))
43
+ g = int(255 * max(0, min(1, 1.5 - abs(4 * t - 2))))
44
+ b = int(255 * max(0, min(1, 1.5 - abs(4 * t - 1))))
45
+ return (r, g, b)
46
+ # viridis approx
47
+ r = int(68 + t * (253 - 68))
48
+ g = int(1 + t * (231 - 1))
49
+ b = int(84 + t * (37 - 84))
50
+ return (r, g, b)
51
+
52
+
53
+ def _data_to_px(x, y, xlim, ylim, box):
54
+ """Vectorized data -> pixel mapping. box=(x0,y0,x1,y1) PIL coords."""
55
+ x0, y0, x1, y1 = box
56
+ span_x = (xlim[1] - xlim[0]) or 1.0
57
+ span_y = (ylim[1] - ylim[0]) or 1.0
58
+ px = x0 + (np.asarray(x, dtype=float) - xlim[0]) / span_x * (x1 - x0)
59
+ py = y1 - (np.asarray(y, dtype=float) - ylim[0]) / span_y * (y1 - y0)
60
+ return px, py
61
+
62
+
63
+ def _draw_line(draw: ImageDraw.ImageDraw, px, py, color, width, linestyle, marker, ms):
64
+ width = max(1, int(round(width)))
65
+ pts = list(zip(px.astype(int), py.astype(int)))
66
+ if len(pts) >= 2 and linestyle not in ("none", "None", ""):
67
+ if linestyle in ("--", ":"):
68
+ # dashed: draw segments with gaps (fast, chunked)
69
+ dash = 6 if linestyle == "--" else 2
70
+ gap = 4 if linestyle == "--" else 3
71
+ for i in range(len(pts) - 1):
72
+ if (i % (dash + gap)) < dash:
73
+ draw.line([pts[i], pts[i + 1]], fill=color, width=width)
74
+ else:
75
+ draw.line(pts, fill=color, width=width, joint="curve")
76
+ if marker and len(pts):
77
+ r = max(2, int(ms / 2))
78
+ step = max(1, len(pts) // 200) # cap markers for speed
79
+ for (mx, my) in pts[::step]:
80
+ if marker == "s":
81
+ draw.rectangle([mx - r, my - r, mx + r, my + r], fill=color, outline=color)
82
+ elif marker in ("^", "v"):
83
+ d = r + 1
84
+ tri = [(mx, my - d), (mx - d, my + d), (mx + d, my + d)] if marker == "^" else \
85
+ [(mx, my + d), (mx - d, my - d), (mx + d, my - d)]
86
+ draw.polygon(tri, fill=color)
87
+ elif marker in ("x", "+"):
88
+ draw.line([mx - r, my, mx + r, my], fill=color, width=width)
89
+ draw.line([mx, my - r, mx, my + r], fill=color, width=width)
90
+ else: # o, ., D, *, d -> circle
91
+ draw.ellipse([mx - r, my - r, mx + r, my + r], fill=color, outline=color)
92
+
93
+
94
+ def _draw_axes(img: Image.Image, draw: ImageDraw.ImageDraw, ax, W, H):
95
+ l, b, w, h = ax.rect
96
+ x0, y1 = int(l * W), int((1 - b) * H)
97
+ x1, y0 = int((l + w) * W), int((1 - (b + h)) * H)
98
+ box = (x0, y0, x1, y1)
99
+ # face
100
+ try:
101
+ face = _parse_color(ax.facecolor)
102
+ except Exception:
103
+ face = (255, 255, 255)
104
+ draw.rectangle(box, fill=face)
105
+ xlim, ylim = ax.get_xlim(), ax.get_ylim()
106
+
107
+ # grid (under data)
108
+ if ax._grid:
109
+ for t in nice_ticks(xlim[0], xlim[1]):
110
+ if not (xlim[0] <= t <= xlim[1]):
111
+ continue
112
+ gx = int(x0 + (t - xlim[0]) / ((xlim[1] - xlim[0]) or 1) * (x1 - x0))
113
+ draw.line([(gx, y0), (gx, y1)], fill=(210, 210, 210), width=1)
114
+ for t in nice_ticks(ylim[0], ylim[1]):
115
+ if not (ylim[0] <= t <= ylim[1]):
116
+ continue
117
+ gy = int(y1 - (t - ylim[0]) / ((ylim[1] - ylim[0]) or 1) * (y1 - y0))
118
+ draw.line([(x0, gy), (x1, gy)], fill=(210, 210, 210), width=1)
119
+
120
+ # images (imshow) first + heatmaps (جديد: corr/heatmap/hist2d)
121
+ for im in list(ax.images) + [dict(data=h["data"], cmap=h.get("cmap", "viridis"),
122
+ vmin=h.get("vmin"), vmax=h.get("vmax"),
123
+ origin="upper", extent=h.get("extent"))
124
+ for h in getattr(ax, "heatmaps", [])]:
125
+ data = im["data"]
126
+ try:
127
+ if np.asarray(data).ndim == 2:
128
+ data = np.asarray(data, dtype=float)
129
+ vmin = im["vmin"] if im["vmin"] is not None else float(np.nanmin(data))
130
+ vmax = im["vmax"] if im["vmax"] is not None else float(np.nanmax(data))
131
+ norm = (data - vmin) / ((vmax - vmin) or 1.0)
132
+ norm = np.clip(norm, 0, 1)
133
+ cmap = im.get("cmap", "viridis")
134
+ hh, ww = norm.shape
135
+ rgb = np.empty((hh, ww, 3), dtype=np.uint8)
136
+ if cmap in ("gray", "grey", "binary", "gist_yarg", "Greys"):
137
+ v = (norm * 255).astype(np.uint8)
138
+ rgb[..., 0] = v; rgb[..., 1] = v; rgb[..., 2] = v
139
+ elif cmap in ("hot",):
140
+ rgb[..., 0] = (norm * 255).astype(np.uint8)
141
+ rgb[..., 1] = ((norm ** 2) * 255).astype(np.uint8)
142
+ rgb[..., 2] = ((norm ** 3) * 128).astype(np.uint8)
143
+ elif cmap in ("cool", "jet", "hsv", "rainbow", "plasma", "inferno", "magma"):
144
+ t = norm
145
+ rgb[..., 0] = (255 * np.clip(1.5 - np.abs(4 * t - 3), 0, 1)).astype(np.uint8)
146
+ rgb[..., 1] = (255 * np.clip(1.5 - np.abs(4 * t - 2), 0, 1)).astype(np.uint8)
147
+ rgb[..., 2] = (255 * np.clip(1.5 - np.abs(4 * t - 1), 0, 1)).astype(np.uint8)
148
+ elif cmap in ("RdBu", "RdBu_r", "coolwarm", "bwr", "seismic"):
149
+ # أحمر-أزرق للارتباط: -1 أحمر ... +1 أزرق
150
+ t = np.clip(norm, 0, 1)
151
+ rgb[..., 0] = (255 * (1 - t)).astype(np.uint8)
152
+ rgb[..., 1] = (255 * (1 - np.abs(2 * t - 1))).astype(np.uint8)
153
+ rgb[..., 2] = (255 * t).astype(np.uint8)
154
+ else: # viridis approx
155
+ rgb[..., 0] = (68 + norm * 185).astype(np.uint8)
156
+ rgb[..., 1] = (1 + norm * 230).astype(np.uint8)
157
+ rgb[..., 2] = (84 - norm * 47).astype(np.uint8)
158
+ pil = Image.fromarray(rgb, "RGB")
159
+ else:
160
+ a = np.asarray(data)
161
+ if a.dtype != np.uint8:
162
+ a = np.clip(a, 0, 255).astype(np.uint8) if np.nanmax(a) > 1 else (a * 255).astype(np.uint8)
163
+ pil = Image.fromarray(a)
164
+ if im.get("origin", "upper") == "upper":
165
+ pil = pil.transpose(Image.FLIP_TOP_BOTTOM)
166
+ pil = pil.resize((max(1, x1 - x0), max(1, y1 - y0)), Image.BILINEAR)
167
+ img.paste(pil, (x0, y0))
168
+ except Exception:
169
+ pass
170
+ # heatmap annotations + labels
171
+ for h in getattr(ax, "heatmaps", []):
172
+ try:
173
+ m = np.asarray(h["data"], dtype=float)
174
+ nr, nc = m.shape
175
+ if h.get("annot") and nr <= 14 and nc <= 14:
176
+ for i in range(nr):
177
+ for j in range(nc):
178
+ # خلية (j,i) -> بكسل
179
+ cx = int(x0 + (j + 0.5) / nc * (x1 - x0))
180
+ cy = int(y0 + (i + 0.5) / nr * (y1 - y0))
181
+ draw.text((cx - 12, cy - 7), f"{m[nr - 1 - i, j]:.2f}",
182
+ fill=(255, 255, 255), font=_font(8))
183
+ if h.get("xticks") and nc == len(h["xticks"]):
184
+ for j, lab in enumerate(h["xticks"]):
185
+ cx = int(x0 + (j + 0.5) / nc * (x1 - x0))
186
+ draw.text((cx - 12, y1 + 5), str(lab)[:10], fill=(0, 0, 0), font=_font(8))
187
+ if h.get("yticks") and nr == len(h["yticks"]):
188
+ for i, lab in enumerate(h["yticks"]):
189
+ cy = int(y0 + (i + 0.5) / nr * (y1 - y0))
190
+ draw.text((max(2, x0 - 52), cy - 7), str(lab)[:10], fill=(0, 0, 0), font=_font(8))
191
+ except Exception:
192
+ pass
193
+
194
+ # fill_between
195
+ for f in ax.fills:
196
+ c = _parse_color(f["color"])
197
+ px1, py1 = _data_to_px(f["x"], f["y1"], xlim, ylim, box)
198
+ _, py2 = _data_to_px(f["x"], f["y2"], xlim, ylim, box)
199
+ poly = list(zip(px1.astype(int), py1.astype(int))) + \
200
+ list(zip(px1.astype(int)[::-1], py2.astype(int)[::-1]))
201
+ if len(poly) >= 3:
202
+ draw.polygon(poly, fill=c + (90,) if len(c) == 3 else c)
203
+
204
+ # bars + hists
205
+ def _rect_from_data(dx0, dy0, dx1, dy1, color):
206
+ pxa, pya = _data_to_px([dx0, dx1], [dy0, dy1], xlim, ylim, box)
207
+ xa, xb = int(min(pxa[0], pxa[1])), int(max(pxa[0], pxa[1]))
208
+ ya, yb = int(min(pya[0], pya[1])), int(max(pya[0], pya[1]))
209
+ if xb - xa < 1:
210
+ xb = xa + 1
211
+ draw.rectangle([xa, ya, xb, yb], fill=color, outline=tuple(max(0, v - 40) for v in color))
212
+
213
+ for bb in ax.bars:
214
+ c = _parse_color(bb["color"])
215
+ n = len(bb["h"])
216
+ widths = bb["w"] if isinstance(bb["w"], np.ndarray) else np.full(n, float(bb["w"]))
217
+ xs = np.asarray(bb["x"]).ravel()
218
+ if xs.size != n:
219
+ xs = np.arange(n)
220
+ for i in range(n):
221
+ if not bb["horizontal"]:
222
+ cx = float(xs[i]) if i < xs.size else i
223
+ _rect_from_data(cx - widths[i] / 2, float(bb["bottom"][i]),
224
+ cx + widths[i] / 2, float(bb["bottom"][i] + bb["h"][i]), c)
225
+ else:
226
+ cy = float(xs[i]) if i < xs.size else i
227
+ _rect_from_data(float(bb["bottom"][i]), cy - widths[i] / 2,
228
+ float(bb["bottom"][i] + bb["h"][i]), cy + widths[i] / 2, c)
229
+
230
+ for hh in ax.hists:
231
+ c = _parse_color(hh["color"])
232
+ for i, cnt in enumerate(hh["counts"]):
233
+ _rect_from_data(float(hh["edges"][i]), 0.0, float(hh["edges"][i + 1]), float(cnt), c)
234
+
235
+ # stacked area (جديد)
236
+ for a in getattr(ax, "areas", []):
237
+ try:
238
+ xs = np.asarray(a["x"], dtype=float)
239
+ px_base, _ = _data_to_px(xs, np.zeros_like(xs), xlim, ylim, box)
240
+ cumul = np.zeros_like(xs, dtype=float)
241
+ for j, yj in enumerate(a["ys"]):
242
+ yj = np.asarray(yj, dtype=float)[: len(xs)]
243
+ prev = cumul.copy()
244
+ cumul = cumul + yj
245
+ c = _parse_color(a["colors"][j] if j < len(a["colors"]) else (79, 70, 229))
246
+ _, py_top = _data_to_px(xs, cumul, xlim, ylim, box)
247
+ _, py_bot = _data_to_px(xs, prev, xlim, ylim, box)
248
+ poly = list(zip(px_base.astype(int), py_top.astype(int))) + \
249
+ list(zip(px_base.astype(int)[::-1], py_bot.astype(int)[::-1]))
250
+ if len(poly) >= 3:
251
+ draw.polygon(poly, fill=c)
252
+ if len(px_base):
253
+ draw.line(list(zip(px_base.astype(int), py_top.astype(int))), fill=tuple(max(0, v - 50) for v in c), width=2)
254
+ except Exception:
255
+ pass
256
+
257
+ # KDE curves (جديد)
258
+ for k in getattr(ax, "kdes", []):
259
+ try:
260
+ c = _parse_color(k["color"])
261
+ px, py = _data_to_px(k["x"], k["y"], xlim, ylim, box)
262
+ px = np.clip(px, x0 - 50, x1 + 50)
263
+ py = np.clip(py, y0 - 50, y1 + 50)
264
+ if k.get("fill"):
265
+ _, py0 = _data_to_px(k["x"], np.zeros_like(k["y"]), xlim, ylim, box)
266
+ poly = list(zip(px.astype(int), py.astype(int))) + \
267
+ list(zip(px.astype(int)[::-1], py0.astype(int)[::-1]))
268
+ if len(poly) >= 3:
269
+ draw.polygon(poly, fill=c + (90,) if len(c) == 3 else c)
270
+ _draw_line(draw, px, py, c, 2.0, "-", None, 6)
271
+ except Exception:
272
+ pass
273
+
274
+ # stems (جديد)
275
+ for s in getattr(ax, "stems", []):
276
+ try:
277
+ c = _parse_color(s["color"])
278
+ px, py = _data_to_px(s["x"], s["y"], xlim, ylim, box)
279
+ _, py0 = _data_to_px(s["x"], np.zeros_like(s["y"]), xlim, ylim, box)
280
+ for i in range(len(px)):
281
+ draw.line([(int(px[i]), int(py0[i])), (int(px[i]), int(py[i]))], fill=c, width=1)
282
+ draw.ellipse([int(px[i]) - 3, int(py[i]) - 3, int(px[i]) + 3, int(py[i]) + 3], fill=c)
283
+ except Exception:
284
+ pass
285
+
286
+ # boxplots (جديد)
287
+ for bg in getattr(ax, "boxes", []):
288
+ try:
289
+ from ..fast import box_stats
290
+ n = len(bg["cols"])
291
+ base_c = _parse_color(bg["color"])
292
+ for i, col in enumerate(bg["cols"]):
293
+ st = box_stats(np.asarray(col, dtype=float))
294
+ pos = float(i + 1)
295
+ pxx, _ = _data_to_px([pos], [0], xlim, ylim, box)
296
+ cx = int(pxx[0])
297
+ _, pyy = _data_to_px([0, 0, 0, 0, 0],
298
+ [st["lo"], st["q1"], st["med"], st["q3"], st["hi"]],
299
+ xlim, ylim, box)
300
+ y_lo, y_q1, y_med, y_q3, y_hi = [int(v) for v in pyy]
301
+ wpx = max(14, int((x1 - x0) / max(n * 3, 1)))
302
+ # whiskers
303
+ draw.line([(cx, y_lo), (cx, y_hi)], fill=(60, 60, 60), width=1)
304
+ draw.line([(cx - wpx // 3, y_lo), (cx + wpx // 3, y_lo)], fill=(60, 60, 60), width=1)
305
+ draw.line([(cx - wpx // 3, y_hi), (cx + wpx // 3, y_hi)], fill=(60, 60, 60), width=1)
306
+ # box q1-q3
307
+ draw.rectangle([cx - wpx // 2, min(y_q1, y_q3), cx + wpx // 2, max(y_q1, y_q3)],
308
+ fill=base_c, outline=(20, 20, 20))
309
+ draw.line([(cx - wpx // 2, y_med), (cx + wpx // 2, y_med)], fill=(255, 255, 255), width=2)
310
+ # mean
311
+ _, pymean = _data_to_px([0], [st["mean"]], xlim, ylim, box)
312
+ draw.ellipse([cx - 3, int(pymean[0]) - 3, cx + 3, int(pymean[0]) + 3], fill=(0, 0, 0))
313
+ # outliers
314
+ if st["out"].size:
315
+ _, pyo = _data_to_px(np.zeros_like(st["out"]), st["out"], xlim, ylim, box)
316
+ for yo in pyo:
317
+ draw.ellipse([cx - 2, int(yo) - 2, cx + 2, int(yo) + 2], outline=(120, 120, 120))
318
+ if bg.get("labels") and i < len(bg["labels"]):
319
+ draw.text((cx - 14, y1 + 5), str(bg["labels"][i])[:12], fill=(0, 0, 0), font=_font(8))
320
+ except Exception:
321
+ pass
322
+
323
+ # violins (جديد: KDE عمودي مرسوم كمرآة)
324
+ for vg in getattr(ax, "violins", []):
325
+ try:
326
+ from ..fast import gaussian_kde_1d
327
+ n = len(vg["cols"])
328
+ base_c = _parse_color(vg["color"])
329
+ for i, col in enumerate(vg["cols"]):
330
+ v = np.asarray(col, dtype=float)
331
+ v = v[np.isfinite(v)]
332
+ if v.size < 3:
333
+ continue
334
+ pos = float(i + 1)
335
+ pxx, _ = _data_to_px([pos], [0], xlim, ylim, box)
336
+ cx = int(pxx[0])
337
+ xs, ys = gaussian_kde_1d(v, points=int(vg.get("points", 120)))
338
+ _, pyy = _data_to_px(np.zeros_like(ys), ys if False else xs, xlim, ylim, box)
339
+ # ys هنا كثافة -> عرض أفقي
340
+ mx = ys.max() or 1.0
341
+ wpx = max(18, int((x1 - x0) / max(n * 2.5, 1)))
342
+ half = (ys / mx * (wpx / 2)).astype(int)
343
+ right = [(cx + int(h), int(p)) for h, p in zip(half, pyy)]
344
+ left = [(cx - int(h), int(p)) for h, p in zip(half[::-1], pyy[::-1])]
345
+ if len(right) + len(left) >= 6:
346
+ draw.polygon(right + left, fill=base_c, outline=(20, 20, 20))
347
+ if vg.get("labels") and i < len(vg["labels"]):
348
+ draw.text((cx - 14, y1 + 5), str(vg["labels"][i])[:12], fill=(0, 0, 0), font=_font(8))
349
+ except Exception:
350
+ pass
351
+
352
+ # lines (decimated — speedup core)
353
+ for ln in ax._rendered_lines():
354
+ c = _parse_color(ln["color"])
355
+ if ln["x"].size == 0:
356
+ continue
357
+ px, py = _data_to_px(ln["x"], ln["y"], xlim, ylim, box)
358
+ # clip to box to avoid Pillow huge-coord slowdown
359
+ px = np.clip(px, x0 - 50, x1 + 50)
360
+ py = np.clip(py, y0 - 50, y1 + 50)
361
+ _draw_line(draw, px, py, c, ln["linewidth"], ln["linestyle"], ln["marker"], ln["markersize"])
362
+
363
+ # scatters
364
+ for sc in ax.scatters:
365
+ n = len(sc["x"])
366
+ px, py = _data_to_px(sc["x"], sc["y"], xlim, ylim, box)
367
+ cols = sc["colors"]
368
+ single = isinstance(cols, tuple)
369
+ base = _parse_color(cols) if single else None
370
+ s = sc["s"]
371
+ if s is None:
372
+ r = 3
373
+ elif np.ndim(s) == 0:
374
+ r = max(1, int(math.sqrt(float(s)) / 2))
375
+ else:
376
+ r = None
377
+ # fast path: tiny squares for huge clouds
378
+ if n > 20000:
379
+ for i in range(0, n, 2):
380
+ xi, yi = int(px[i]), int(py[i])
381
+ if x0 <= xi <= x1 and y0 <= yi <= y1:
382
+ cc = base if single else _parse_color(cols[i]) if i < len(cols) else base
383
+ img.putpixel((xi, yi), cc) if False else draw.point((xi, yi), fill=cc or (0, 0, 0))
384
+ continue
385
+ for i in range(n):
386
+ xi, yi = int(px[i]), int(py[i])
387
+ if not (x0 - 20 <= xi <= x1 + 20 and y0 - 20 <= yi <= y1 + 20):
388
+ continue
389
+ cc = base if single else (_parse_color(cols[i]) if i < len(cols) else (79, 70, 229))
390
+ rr = r if r is not None else max(1, int(math.sqrt(float(np.asarray(s).ravel()[i])) / 2))
391
+ draw.ellipse([xi - rr, yi - rr, xi + rr, yi + rr], fill=cc, outline=cc)
392
+
393
+ # hlines / vlines / errorbars (simplified)
394
+ for hl in ax._hlines:
395
+ if hl["kind"] == "axhline":
396
+ _, pyy = _data_to_px([xlim[0]], [hl["y"]], xlim, ylim, box)
397
+ draw.line([(x0, int(pyy[0])), (x1, int(pyy[0]))], fill=_parse_color(hl["color"]), width=int(hl["linewidth"]))
398
+ for vl in ax._vlines:
399
+ if vl["kind"] == "axvline":
400
+ pxx, _ = _data_to_px([vl["x"]], [ylim[0]], xlim, ylim, box)
401
+ draw.line([(int(pxx[0]), y0), (int(pxx[0]), y1)], fill=_parse_color(vl["color"]), width=int(vl["linewidth"]))
402
+
403
+ # pie
404
+ for p in ax.pies:
405
+ cx, cy = (x0 + x1) // 2, (y0 + y1) // 2
406
+ rr = min(x1 - x0, y1 - y0) // 2 - 10
407
+ ang = float(p["startangle"])
408
+ cols = p["colors"]
409
+ for i, fr in enumerate(p["fracs"]):
410
+ sweep = float(fr) * 360
411
+ cc = _parse_color(cols[i]) if cols and i < len(cols) else [(79, 70, 229), (255, 127, 14), (44, 160, 44), (214, 39, 40)][i % 4]
412
+ draw.pieslice([cx - rr, cy - rr, cx + rr, cy + rr], start=ang, end=ang + sweep, fill=cc,
413
+ outline=(255, 255, 255))
414
+ ang += sweep
415
+
416
+ # spines
417
+ edge = _parse_color(rcParams.get("axes.edgecolor", "black"))
418
+ draw.rectangle(box, outline=edge, width=1)
419
+
420
+ # ticks + labels (مع دعم التسميات الفئوية من bar/count)
421
+ fs = int(rcParams.get("xtick.labelsize", 9))
422
+ font = _font(fs)
423
+ cat_labels = getattr(ax, "_xticklabels", None)
424
+ cat_pos = getattr(ax, "_xtickpos", None)
425
+ if cat_labels is not None and cat_pos is not None and len(cat_labels):
426
+ try:
427
+ for p, lab in zip(np.asarray(cat_pos, dtype=float), cat_labels):
428
+ if not (xlim[0] - 1 <= p <= xlim[1] + 1):
429
+ continue
430
+ gx = int(x0 + (p - xlim[0]) / ((xlim[1] - xlim[0]) or 1) * (x1 - x0))
431
+ draw.line([(gx, y1), (gx, y1 + 4)], fill=(0, 0, 0), width=1)
432
+ draw.text((gx - 12, y1 + 5), str(lab)[:12], fill=(0, 0, 0), font=font)
433
+ except Exception:
434
+ pass
435
+ else:
436
+ for t in nice_ticks(xlim[0], xlim[1]):
437
+ if not (xlim[0] <= t <= xlim[1]):
438
+ continue
439
+ gx = int(x0 + (t - xlim[0]) / ((xlim[1] - xlim[0]) or 1) * (x1 - x0))
440
+ draw.line([(gx, y1), (gx, y1 + 4)], fill=(0, 0, 0), width=1)
441
+ draw.text((gx - 10, y1 + 5), fmt_tick(t), fill=(0, 0, 0), font=font)
442
+ for t in nice_ticks(ylim[0], ylim[1]):
443
+ if not (ylim[0] <= t <= ylim[1]):
444
+ continue
445
+ gy = int(y1 - (t - ylim[0]) / ((ylim[1] - ylim[0]) or 1) * (y1 - y0))
446
+ draw.line([(x0 - 4, gy), (x0, gy)], fill=(0, 0, 0), width=1)
447
+ draw.text((max(2, x0 - 38), gy - 7), fmt_tick(t), fill=(0, 0, 0), font=font)
448
+
449
+ # xlabel / ylabel / title
450
+ if ax._xlabel:
451
+ draw.text(((x0 + x1) // 2 - 20, y1 + 22), ax._xlabel, fill=(0, 0, 0), font=_font(int(rcParams["axes.labelsize"])))
452
+ if ax._ylabel:
453
+ draw.text((max(2, x0 - 55), (y0 + y1) // 2 - 10), ax._ylabel, fill=(0, 0, 0), font=_font(int(rcParams["axes.labelsize"])))
454
+ if ax._title:
455
+ draw.text(((x0 + x1) // 2 - len(ax._title) * 3, max(2, y0 - 22)), ax._title, fill=(0, 0, 0),
456
+ font=_font(int(rcParams["axes.titlesize"])))
457
+
458
+ # legend (fast, top-right)
459
+ labels = [(ln.get("label"), _parse_color(ln["color"])) for ln in ax.lines if ln.get("label")]
460
+ labels += [(s.get("label"), _parse_color(s["colors"]) if isinstance(s["colors"], tuple) else (79, 70, 229))
461
+ for s in ax.scatters if s.get("label")]
462
+ labels += [(k.get("label"), _parse_color(k["color"])) for k in getattr(ax, "kdes", []) if k.get("label")]
463
+ for a in getattr(ax, "areas", []):
464
+ for lb, cc in zip(a.get("labels", []), a.get("colors", [])):
465
+ if lb:
466
+ labels.append((lb, _parse_color(cc)))
467
+ if labels:
468
+ lx1, ly1 = x1 - 8, y0 + 8
469
+ lw_box, lh = 110, 18 * len(labels) + 10
470
+ draw.rectangle([lx1 - lw_box, ly1, lx1, ly1 + lh], fill=(255, 255, 255), outline=(150, 150, 150))
471
+ for i, (lb, cc) in enumerate(labels):
472
+ yy = ly1 + 8 + i * 18
473
+ draw.line([(lx1 - lw_box + 8, yy), (lx1 - lw_box + 28, yy)], fill=cc, width=2)
474
+ draw.text((lx1 - lw_box + 32, yy - 8), str(lb)[:16], fill=(0, 0, 0), font=_font(9))
475
+
476
+
477
+ def render_to_image(fig) -> Image.Image:
478
+ W, H = fig.pixel_size
479
+ try:
480
+ bg = to_rgb(fig.facecolor)
481
+ except Exception:
482
+ bg = (255, 255, 255)
483
+ img = Image.new("RGB", (W, H), bg)
484
+ draw = ImageDraw.Draw(img, "RGBA")
485
+ if not fig.axes:
486
+ from ..figure import Axes as _A
487
+ fig.axes.append(_A(fig))
488
+ for ax in fig.axes:
489
+ _draw_axes(img, draw, ax, W, H)
490
+ if getattr(fig, "_suptitle", ""):
491
+ draw.text((W // 2 - len(fig._suptitle) * 4, 6), fig._suptitle, fill=(0, 0, 0),
492
+ font=_font(int(rcParams["axes.titlesize"]) + 1))
493
+ return img
494
+
495
+
496
+ def render_figure(fig, fname=None, dpi=None, show=False):
497
+ img = render_to_image(fig)
498
+ if fname is None or show:
499
+ try:
500
+ img.show()
501
+ except Exception:
502
+ # headless: save to memory / show via matplotlib if present
503
+ try:
504
+ import matplotlib.pyplot as _plt
505
+ import numpy as _np
506
+ _plt.figure(figsize=fig.figsize, dpi=fig.dpi)
507
+ _plt.imshow(_np.asarray(img))
508
+ _plt.axis("off")
509
+ _plt.show()
510
+ except Exception:
511
+ pass
512
+ if fname is None:
513
+ return img
514
+ fn = str(fname)
515
+ lo = fn.lower()
516
+ if lo.endswith((".pdf", ".svg", ".eps")):
517
+ img.save(fn)
518
+ else:
519
+ img.save(fn, dpi=(dpi or fig.dpi, dpi or fig.dpi))
520
+ return img