update diogram

This commit is contained in:
2026-04-07 15:37:04 +05:00
parent 0528393f2f
commit 0b7a5bc454
5 changed files with 277 additions and 115 deletions

View File

@@ -1,8 +1,7 @@
"""
radar_svg.py — генерация SVG паутинной диаграммы для сохранения на устройство.
radar_svg.py — генерация SVG паутинной диаграммы для сохранения.
Не требует сторонних зависимостей (чистый Python).
SVG содержит корректные шрифты и цвета, может быть открыт/сохранён в браузере.
Отрицательные значения — красный полигон, положительные — фиолетовый.
"""
import math
import base64
@@ -18,73 +17,97 @@ def generate_radar_svg(items: list[dict], max_val: float = 14.0, size: int = 520
"""
n = len(items)
if n < 3:
return f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}"><text x="10" y="20" fill="white">Нет данных</text></svg>'
return (
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'
f'<text x="{size//2}" y="{size//2}" text-anchor="middle" fill="#888" '
f'font-family="Arial,sans-serif" font-size="16">Нет данных</text></svg>'
)
cx = cy = size / 2
R = size * 0.27 # радиус внешнего кольца
lbl_r = R + size * 0.12 # расстояние до подписей от центра
safe_max = max_val if max_val > 0 else 1.0
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(pts: list[tuple]) -> str:
def poly(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)]
frac = lvl / 5
pts = [axis_xy(i, frac) for i in range(n)]
alpha = 0.12 + 0.09 * frac
lines.append(
f'<polygon points="{poly_pts(pts)}" '
f'fill="none" stroke="rgba(244,239,250,0.18)" stroke-width="1"/>'
f'<polygon points="{poly(pts)}" fill="none" '
f'stroke="rgba(170,130,230,{alpha:.2f})" 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"/>'
f'stroke="rgba(170,130,230,0.45)" 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"/>'
)
# ── Полигоны данных ───────────────────────────────────────────────────────
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)]
# ── точки на вершинах ────────────────────────────────────────────────────
for x, y in data_pts:
lines.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="5" fill="#F4EFFA"/>')
lines.append(f'<polygon points="{poly(neg_pts)}" fill="rgba(220,50,50,0.30)" stroke="#FF7070" stroke-width="3.5"/>')
lines.append(f'<polygon points="{poly(pos_pts)}" fill="rgba(100,60,200,0.32)" stroke="#A080FF" stroke-width="3.5"/>')
# ── подписи осей ─────────────────────────────────────────────────────────
# ── Точки на вершинах ─────────────────────────────────────────────────────
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'<circle cx="{x:.2f}" cy="{y:.2f}" r="6" fill="{col}" '
f'stroke="rgba(255,255,255,0.70)" stroke-width="1.5"/>'
)
# ── Метки шкалы (первая ось) ──────────────────────────────────────────────
for lvl in range(1, 6):
frac = lvl / 5
tx, ty = axis_xy(0, frac)
lines.append(
f'<text x="{tx + 4:.2f}" y="{ty - 2:.2f}" font-family="Arial,sans-serif" '
f'font-size="9" fill="rgba(170,130,230,0.65)">{max_abs * frac:.0f}</text>'
)
# ── Подписи осей ───────────────────────────────────────────────────────────
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"
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'<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>'
f'fill="{val_col}">{val}</text>'
)
lines.append("</svg>")