"""Crackle Check pictures: the maps beside their control, where the scores fall, and the answer scale (numpy, Pillow).
Everything here draws what the gate already measured; nothing changes the answer. Map tiles are WebP data URIs and
the charts are inline SVG that take the page's text color, so they read in light and dark themes alike. The colors
follow the overlay: gray for CT and for map values under the control's threshold T, copper to amber to ivory above T,
so amber always means "passes the control's 99th percentile". Everything drawn is model output, not a reading.
"""
from __future__ import annotations
import base64
import html
import io
import numpy as np
import gate
# Charcoal to copper to amber to ivory, for map values from T up to the region's top value.
RAMP = ((0.0, (0x5a, 0x34, 0x1f)), (0.35, (0xb7, 0x7a, 0x54)), (0.7, (0xf5, 0x9e, 0x1a)), (1.0, (0xee, 0xe7, 0xd9)))
AXIS_MAX = 8.0 # the answer scale runs 0 to 8 times the control; larger values sit at the end with a "+"
BINS = 40
def ramp(t):
"""RGB float (0 to 1) for t in 0 to 1 along RAMP; t is an array."""
t = np.clip(np.asarray(t, np.float32), 0, 1)
xs = [p for p, _ in RAMP]
return np.stack([np.interp(t, xs, [c[i] / 255.0 for _, c in RAMP]) for i in range(3)], -1)
def ct_gray(ct):
"""A CT layer stretched to its 1st to 99th percentile, float 0 to 1."""
ct = np.asarray(ct, np.float32)
lo, hi = np.percentile(ct, [1, 99])
return np.clip((ct - lo) / max(hi - lo, 1e-6), 0, 1)
def heat(m, threshold, top, papyrus_mask):
"""One map as RGB uint8: under T a dim gray that rises with the value, at or above T the copper ramp.
The same T and top are used for a map and its control, so the two tiles are on one scale."""
m = np.asarray(m, np.float32)
below = np.clip(m / max(threshold, 1e-6), 0, 1) * 0.42 + 0.08
rgb = np.stack([below] * 3, -1)
over = m > threshold
if over.any():
rgb[over] = ramp((m[over] - threshold) / max(top - threshold, 1e-6))
rgb[~papyrus_mask] = 0.03
return (rgb * 255).astype(np.uint8)
def data_uri(rgb, max_side=480, quality=82):
"""An RGB array as a WebP data URI, at most max_side px a side."""
from PIL import Image
im = Image.fromarray(np.asarray(rgb, np.uint8))
if max(im.size) > max_side:
k = max_side / max(im.size)
im = im.resize((max(1, round(im.size[0] * k)), max(1, round(im.size[1] * k))), Image.LANCZOS)
b = io.BytesIO()
im.save(b, "WEBP", quality=quality, method=6)
return "data:image/webp;base64," + base64.b64encode(b.getvalue()).decode("ascii")
def _f(x):
return f"{x:.1f}".rstrip("0").rstrip(".")
def gauge_svg(excess=None, width=440):
"""The answer scale as SVG: No under 1.5 times the control, Maybe to 3, Yes from 3, with a marker at excess."""
h, top, bar = 70, 22, 14
x = lambda v: 8 + (width - 16) * min(max(v, 0.0), AXIS_MAX) / AXIS_MAX # noqa: E731
zones = ((0, gate.WEAK_EXCESS, "var(--cc-no, #4a5563)", "No signal found"),
(gate.WEAK_EXCESS, gate.INK_EXCESS, "var(--cc-maybe, #8a5a00)", "Maybe"),
(gate.INK_EXCESS, AXIS_MAX, "var(--cc-yes, #1f7a4d)", "Yes"))
parts = [f'")
return "".join(parts)
def histogram_svg(F, R, mask, threshold, width=440, height=170):
"""Where the scores fall on the papyrus: the control's (reversed) scores in gray, yours in amber, T dashed.
Bar heights are the share of papyrus pixels per bin on a square-root axis, so the 1 % tail stays visible."""
f, r = np.asarray(F, np.float32)[mask], np.asarray(R, np.float32)[mask]
hi = float(max(f.max(), r.max(), threshold * 1.05, 1e-3))
lo = float(min(f.min(), r.min(), threshold * 0.95)) if f.size else 0.0
edges = np.linspace(lo, hi, BINS + 1)
hf = np.histogram(f, edges)[0] / max(f.size, 1)
hr = np.histogram(r, edges)[0] / max(r.size, 1)
left, right, top, bottom = 8, 8, 26, 30
pw, ph = width - left - right, height - top - bottom
peak = float(np.sqrt(max(hf.max(), hr.max(), 1e-9)))
bw = pw / BINS
X = lambda v: left + pw * (v - lo) / max(hi - lo, 1e-9) # noqa: E731
Y = lambda share: top + ph * (1 - np.sqrt(share) / peak) # noqa: E731
def bars(h, color, opacity, inset):
out = []
for i, s in enumerate(h):
if s <= 0:
continue
y = Y(s)
out.append(f'
'
f'{(share or 0):.1%} of papyrus above T