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