Files
api-copp/route/radar_svg_gen.py
2026-04-01 14:18:52 +05:00

119 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
radar_svg_gen.py — серверная генерация SVG паутинной диаграммы.
Используется при сохранении результата: файл пишется в APP_PATH/data/radar/.
Не требует сторонних зависимостей.
"""
import math
import os
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 — максимум шкалы (14 для теста Голланда)
size — размер холста в пикселях
"""
n = len(items)
if n < 3:
return (
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'
f'<rect width="{size}" height="{size}" fill="#2F184B" rx="16"/>'
f'<text x="{size//2}" y="{size//2}" text-anchor="middle" fill="white" '
f'font-family="Arial,sans-serif" font-size="16">Нет данных</text></svg>'
)
cx = cy = size / 2
R = size * 0.27
lbl_r = R + size * 0.13
safe_max = max_val if max_val > 0 else 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(pts: list[tuple]) -> str:
return " ".join(f"{x:.2f},{y:.2f}" for x, y in pts)
lines: list[str] = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">',
f'<rect width="{size}" height="{size}" fill="#2F184B" rx="16"/>',
]
# Концентрические сетки
for lvl in range(1, 6):
pts = [axis_xy(i, lvl / 5) for i in range(n)]
lines.append(
f'<polygon points="{poly_pts(pts)}" '
f'fill="none" stroke="rgba(244,239,250,0.18)" stroke-width="1"/>'
)
# Оси от центра
for i in range(n):
x, y = axis_xy(i)
lines.append(
f'<line x1="{cx:.2f}" y1="{cy:.2f}" x2="{x:.2f}" y2="{y:.2f}" '
f'stroke="rgba(244,239,250,0.22)" stroke-width="1"/>'
)
# Полигон данных
data_pts = [axis_xy(i, min(it["value"] / safe_max, 1.0)) for i, it in enumerate(items)]
lines.append(
f'<polygon points="{poly_pts(data_pts)}" '
f'fill="rgba(155,114,207,0.55)" stroke="#F4EFFA" stroke-width="2.5"/>'
)
# Точки на вершинах
for x, y in data_pts:
lines.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="5" fill="#F4EFFA"/>')
# Подписи
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 "#F4EFFA"
name = _xml_escape(item["dimension_name"])
val = int(item["value"])
lines.append(
f'<text x="{lx:.2f}" y="{ly:.2f}" text-anchor="middle" '
f'font-family="Arial,Helvetica,sans-serif" font-size="14" '
f'font-weight="bold" fill="{col}">{name}</text>'
)
lines.append(
f'<text x="{lx:.2f}" y="{ly + 20:.2f}" text-anchor="middle" '
f'font-family="Arial,Helvetica,sans-serif" font-size="13" '
f'fill="#F4EFFA">{val}</text>'
)
lines.append("</svg>")
return "\n".join(lines)
def _xml_escape(s: str) -> str:
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def save_radar_svg(result_id: str, items: list[dict], max_val: float = 14.0) -> str:
"""
Генерирует SVG и сохраняет его на диск.
Возвращает относительный путь к файлу (относительно APP_PATH).
Файл пишется в: {APP_PATH}/data/radar/{result_id}.svg
Возвращает: data/radar/{result_id}.svg
"""
app_path = os.getenv("APP_PATH", "/app")
radar_dir = os.path.join(app_path, "data", "radar")
os.makedirs(radar_dir, exist_ok=True)
filename = f"{result_id}.svg"
full_path = os.path.join(radar_dir, filename)
svg = generate_radar_svg(items, max_val)
with open(full_path, "w", encoding="utf-8") as f:
f.write(svg)
return f"data/radar/{filename}"