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

@@ -5,7 +5,6 @@ radar_svg_gen.py — серверная генерация SVG паутинно
"""
import math
import os
import base64
def generate_radar_svg(
@@ -18,15 +17,16 @@ def generate_radar_svg(
show_scale_labels: bool = True,
) -> str:
"""
Возвращает строку SVG паутинной диаграммы.
Возвращает SVG паутинной диаграммы.
items — список осей: [{dimension_name, dimension_color, value}, ...]
max_val — максимум шкалы; 0 = авто (вычисляется из items)
size — размер холста в пикселях
scale_padding — запас над максимальным значением (доля, 0.2 = +20%)
min_scale — минимальное значение шкалы (чтобы маленькие значения не выглядели огромными)
min_scale — минимальное значение шкалы
grid_levels — количество концентрических колец
show_scale_labels — рисовать ли подписи значений на одной из осей
Отрицательные значения окрашиваются красным, положительные — фиолетовым.
"""
n = len(items)
if n < 3:
@@ -37,40 +37,41 @@ def generate_radar_svg(
)
# ---- масштаб ----
data_max = max((it["value"] for it in items), default=1.0)
if max_val <= 0:
max_val = data_max
safe_max = max(max_val * (1.0 + scale_padding), min_scale, 1.0)
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 + scale_padding), min_scale, 1.0)
# ---- геометрия ----
cx = cy = size / 2
# Радиус сетки: оставляем место для подписей со всех сторон
lbl_margin = size * 0.25 # запас под подписи
R = (size / 2) - lbl_margin # сетка заполняет максимум пространства
lbl_r = R + size * 0.06 # подписи чуть дальше от сетки
cx = size / 2
cy = size / 2
lbl_margin = size * 0.22
R = (size / 2) - lbl_margin
lbl_r = R + size * 0.07
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}">',
]
# Концентрические сетки
# ── Сетка ─────────────────────────────────────────────────────────────────
for lvl in range(1, grid_levels + 1):
frac = lvl / grid_levels
pts = [axis_xy(i, frac) for i in range(n)]
alpha = 0.28 + 0.14 * (lvl / grid_levels) # внешние кольца чуть ярче
frac = lvl / grid_levels
pts = [axis_xy(i, frac) for i in range(n)]
alpha = 0.15 + 0.12 * frac
lines.append(
f'<polygon points="{poly_pts(pts)}" '
f'fill="none" stroke="rgba(170,130,230,{alpha:.2f})" stroke-width="1.2"/>'
f'<polygon points="{poly(pts)}" fill="none" '
f'stroke="rgba(170,130,230,{alpha:.2f})" stroke-width="1.2"/>'
)
# Оси от центра
# ── Оси ───────────────────────────────────────────────────────────────────
for i in range(n):
x, y = axis_xy(i)
lines.append(
@@ -78,43 +79,50 @@ def generate_radar_svg(
f'stroke="rgba(170,130,230,0.45)" stroke-width="1.2"/>'
)
# Подписи значений на первой оси (вертикально вверх)
# ── Метки шкалы (первая ось) ──────────────────────────────────────────────
if show_scale_labels:
for lvl in range(1, grid_levels + 1):
frac = lvl / grid_levels
tick_val = safe_max * frac
tx, ty = axis_xy(0, frac)
frac = lvl / grid_levels
tick_val = max_abs * frac
tx, ty = axis_xy(0, frac)
lines.append(
f'<text x="{tx + 4:.2f}" y="{ty:.2f}" '
f'font-family="Arial,sans-serif" font-size="10" '
f'fill="#A070D0">{tick_val:.0f}</text>'
f'fill="rgba(170,130,230,0.70)">{tick_val:.0f}</text>'
)
# Полигон данных
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(180,140,240,0.35)" stroke="#A070D0" stroke-width="3.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="6" fill="#A070D0"/>')
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.60)" stroke-width="1.5"/>'
)
# ── Подписи осей ───────────────────────────────────────────────────────────
font_size = max(11, min(14, int(size / 40)))
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 "#2F184B"
col = item.get("dimension_color") or "#A070D0"
val = int(item["value"])
val_color = "#FF7070" if item["value"] < 0 else "#A080FF"
word_lines = _wrap_label(item["dimension_name"])
# Вертикальное центрирование: сдвигаем вверх на половину суммарной высоты
line_h = font_size + 3
total_h = line_h * len(word_lines) + (font_size + 2) # +строка со значением
y0 = ly - total_h / 2 + line_h
line_h = font_size + 3
total_h = line_h * len(word_lines) + (font_size + 2)
y0 = ly - total_h / 2 + line_h
for k, part in enumerate(word_lines):
lines.append(
@@ -125,7 +133,7 @@ def generate_radar_svg(
lines.append(
f'<text x="{lx:.2f}" y="{y0 + len(word_lines) * line_h:.2f}" '
f'text-anchor="middle" font-family="Arial,Helvetica,sans-serif" '
f'font-size="{font_size + 2}" fill="#A070D0">{val}</text>'
f'font-size="{font_size + 2}" fill="{val_color}">{val}</text>'
)
lines.append("</svg>")