new shems

This commit is contained in:
2026-04-01 14:18:52 +05:00
parent 4e9b89fbe5
commit 22bbc3a0a6
15 changed files with 1793 additions and 133 deletions

118
route/radar_svg_gen.py Normal file
View File

@@ -0,0 +1,118 @@
"""
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}"