""" radar_svg.py — генерация SVG паутинной диаграммы для сохранения. Не требует сторонних зависимостей (чистый Python). Отрицательные значения — красный полигон, положительные — фиолетовый. """ import math import base64 def generate_radar_svg(items: list[dict], max_val: float = 14.0, size: int = 520) -> str: """ Возвращает SVG-строку паутинной диаграммы. items — список осей (dimension_name, dimension_color, value) max_val — максимум шкалы size — размер холста в пикселях """ n = len(items) if n < 3: return ( f'' f'Нет данных' ) cx = size / 2 cy = size / 2 R = size * 0.255 lbl_r = R + size * 0.105 vals = [it["value"] for it in items] max_abs = max((abs(v) for v in vals), default=1.0) if max_val > 0: max_abs = max(max_abs, max_val) max_abs = max(max_abs, 1.0) def axis_xy(i: int, frac: float = 1.0) -> tuple[float, float]: a = math.radians(-90.0 + 360.0 * i / n) return cx + R * frac * math.cos(a), cy + R * frac * math.sin(a) def poly(pts: list[tuple]) -> str: return " ".join(f"{x:.2f},{y:.2f}" for x, y in pts) lines: list[str] = [ f'', ] # ── Сетка ───────────────────────────────────────────────────────────────── for lvl in range(1, 6): frac = lvl / 5 pts = [axis_xy(i, frac) for i in range(n)] alpha = 0.12 + 0.09 * frac lines.append( f'' ) # ── Оси от центра ───────────────────────────────────────────────────────── for i in range(n): x, y = axis_xy(i) lines.append( f'' ) # ── Полигоны данных ─────────────────────────────────────────────────────── neg_pts = [axis_xy(i, max(0.0, min(-it["value"] / max_abs, 1.0))) for i, it in enumerate(items)] pos_pts = [axis_xy(i, max(0.0, min( it["value"] / max_abs, 1.0))) for i, it in enumerate(items)] lines.append(f'') lines.append(f'') # ── Точки на вершинах ───────────────────────────────────────────────────── for i, it in enumerate(items): v = it["value"] if abs(v) > 0.05: x, y = axis_xy(i, abs(v) / max_abs) col = "#FF7070" if v < 0 else "#A080FF" lines.append( f'' ) # ── Метки шкалы (первая ось) ────────────────────────────────────────────── for lvl in range(1, 6): frac = lvl / 5 tx, ty = axis_xy(0, frac) lines.append( f'{max_abs * frac:.0f}' ) # ── Подписи осей ─────────────────────────────────────────────────────────── for i, item in enumerate(items): a = math.radians(-90.0 + 360.0 * i / n) lx = cx + lbl_r * math.cos(a) ly = cy + lbl_r * math.sin(a) col = item.get("dimension_color") or "#A080FF" name = _xml_escape(item["dimension_name"]) val = int(item["value"]) val_col = "#FF7070" if item["value"] < 0 else "#A080FF" lines.append( f'{name}' ) lines.append( f'{val}' ) lines.append("") return "\n".join(lines) def _xml_escape(s: str) -> str: return s.replace("&", "&").replace("<", "<").replace(">", ">") def get_radar_data_uri(items: list[dict], max_val: float = 14.0) -> str: """ Возвращает data URI вида «data:image/svg+xml;base64,…». Передайте в page.launch_url(...) чтобы открыть/сохранить изображение. """ svg = generate_radar_svg(items, max_val) b64 = base64.b64encode(svg.encode("utf-8")).decode() return f"data:image/svg+xml;base64,{b64}"