-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraph_render.py
More file actions
74 lines (61 loc) · 2.67 KB
/
Copy pathgraph_render.py
File metadata and controls
74 lines (61 loc) · 2.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"""Pure-PIL sparkline renderer for Stream Deck keys.
No matplotlib, no subprocess: draws the polyline supersampled with
ImageDraw and downscales for antialiasing. Standalone module (no
StreamController imports) so it can be tested outside the app.
"""
from PIL import Image, ImageChops, ImageDraw
def render_graph(values, v_max, v_min=0.0, size=144, supersample=3,
line_rgba=(255, 255, 255, 255), fill_rgba=None,
line_width=3.0, bg_rgba=None, radius=18):
"""Render a sparkline of `values` into a square RGBA image.
values: newest-last list; None entries are gaps (carried forward once a
value has been seen, so short dropouts don't break the line).
v_max/v_min: y-axis range; values outside are clamped.
bg_rgba: optional rounded-rectangle background color.
"""
ss = max(1, int(supersample))
big = size * ss
img = Image.new("RGBA", (big, big), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
if bg_rgba:
draw.rounded_rectangle((0, 0, big - 1, big - 1),
radius=radius * ss, fill=tuple(bg_rgba))
span = max(v_max - v_min, 1e-9)
lw = max(1, round(line_width * ss))
pad = lw # keep the stroke inside the canvas at 0% and 100%
points = []
prev = None
n = len(values)
for i, v in enumerate(values):
if v is None:
v = prev
if v is None:
continue # still leading gap
prev = v
frac = min(max((v - v_min) / span, 0.0), 1.0)
x = i * (big - 1) / max(n - 1, 1)
y = (big - 1 - pad) - frac * (big - 1 - 2 * pad)
points.append((x, y))
if len(points) >= 2:
if fill_rgba:
poly = points + [(points[-1][0], big - 1), (points[0][0], big - 1)]
fill_layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
ImageDraw.Draw(fill_layer).polygon(poly, fill=tuple(fill_rgba))
img = Image.alpha_composite(img, fill_layer)
draw = ImageDraw.Draw(img)
draw.line(points, fill=tuple(line_rgba), width=lw, joint="curve")
if bg_rgba:
# Clip the plot to the rounded background so nothing spills out
mask = Image.new("L", img.size, 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, big - 1, big - 1), radius=radius * ss, fill=255)
img.putalpha(ImageChops.multiply(img.getchannel("A"), mask))
if ss > 1:
img = img.resize((size, size), Image.LANCZOS)
return img
def auto_max(values, floor=1.0):
"""Y-axis max for auto-scaled metrics: a bit above the window's peak."""
valid = [v for v in values if v is not None]
if not valid:
return floor
return max(max(valid) * 1.15, floor)