143 lines
5.8 KiB
Python
143 lines
5.8 KiB
Python
"""
|
||
RadarChart — переиспользуемый виджет паутинной диаграммы (ft.Stack).
|
||
|
||
Параметры:
|
||
items — список осей из API /responses/{id}/radar:
|
||
[{dimension_name, dimension_color, dimension_position, value}, ...]
|
||
max_val — максимум на шкале (default 14 — для теста Голланда)
|
||
"""
|
||
import math
|
||
import flet as ft
|
||
import flet.canvas as cv
|
||
import designer as ds
|
||
|
||
# ── размеры ─────────────────────────────────────────────────────────────────
|
||
_CS = 260 # размер canvas (×2 = итоговая зона графика)
|
||
_CX = _CS // 2
|
||
_CY = _CS // 2
|
||
_R = 80 # радиус внешнего кольца
|
||
_PAD = 95 # отступ вокруг canvas — оставляем место для подписей
|
||
_LBL_R = _R + 50 # расстояние от центра до центра подписи (в координатах Stack)
|
||
_W_LBL = 130 # ширина контейнера подписи (достаточно для «Конвенциональный»)
|
||
|
||
|
||
def _axis_xy(n: int, 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 _closed_path(pts: list[tuple]) -> list:
|
||
els = [cv.Path.MoveTo(*pts[0])]
|
||
for p in pts[1:]:
|
||
els.append(cv.Path.LineTo(*p))
|
||
els.append(cv.Path.Close())
|
||
return els
|
||
|
||
|
||
class RadarChart(ft.Stack):
|
||
"""Паутинная диаграмма типов/сфер по данным radar API."""
|
||
|
||
def __init__(self, items: list[dict], max_val: float = 14.0):
|
||
n = len(items)
|
||
shapes: list = []
|
||
|
||
# ── сетка (5 уровней) ────────────────────────────────────────────────
|
||
for lvl in range(1, 6):
|
||
ring = [_axis_xy(n, i, lvl / 5) for i in range(n)]
|
||
shapes.append(cv.Path(
|
||
_closed_path(ring),
|
||
paint=ft.Paint(
|
||
style=ft.PaintingStyle.STROKE,
|
||
stroke_width=1,
|
||
color="#30F4EFFA",
|
||
),
|
||
))
|
||
|
||
# ── оси от центра ────────────────────────────────────────────────────
|
||
for i in range(n):
|
||
x, y = _axis_xy(n, i)
|
||
shapes.append(cv.Path(
|
||
[cv.Path.MoveTo(_CX, _CY), cv.Path.LineTo(x, y)],
|
||
paint=ft.Paint(stroke_width=1, color="#40F4EFFA"),
|
||
))
|
||
|
||
# ── полигон данных ───────────────────────────────────────────────────
|
||
safe_max = max_val if max_val > 0 else 1.0
|
||
data_pts = [
|
||
_axis_xy(n, i, min(it["value"] / safe_max, 1.0))
|
||
for i, it in enumerate(items)
|
||
]
|
||
shapes.append(cv.Path(
|
||
_closed_path(data_pts),
|
||
paint=ft.Paint(style=ft.PaintingStyle.FILL, color="#609B72CF"),
|
||
))
|
||
shapes.append(cv.Path(
|
||
_closed_path(data_pts),
|
||
paint=ft.Paint(
|
||
style=ft.PaintingStyle.STROKE,
|
||
stroke_width=2.5,
|
||
color="#F4EFFA",
|
||
),
|
||
))
|
||
for px, py in data_pts:
|
||
shapes.append(cv.Circle(px, py, 4, paint=ft.Paint(color="#F4EFFA")))
|
||
|
||
canvas = cv.Canvas(shapes=shapes, width=_CS, height=_CS)
|
||
|
||
# ── подписи оси в Stack-координатах ──────────────────────────────────
|
||
sw = _CS + 2 * _PAD
|
||
sh = _CS + 2 * _PAD
|
||
ccx = _PAD + _CX # центр графика в Stack
|
||
ccy = _PAD + _CY
|
||
|
||
labels: list[ft.Control] = []
|
||
for i, item in enumerate(items):
|
||
a = math.radians(-90.0 + 360.0 * i / n)
|
||
lx = ccx + _LBL_R * math.cos(a)
|
||
ly = ccy + _LBL_R * math.sin(a)
|
||
col = item.get("dimension_color") or ds.clolors.ferst
|
||
|
||
labels.append(ft.Container(
|
||
left=lx - _W_LBL / 2,
|
||
top=ly - 24,
|
||
width=_W_LBL,
|
||
content=ft.Column(
|
||
spacing=1,
|
||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||
controls=[
|
||
ft.Text(
|
||
item["dimension_name"],
|
||
size=10,
|
||
color=col,
|
||
text_align=ft.TextAlign.CENTER,
|
||
weight=ft.FontWeight.BOLD,
|
||
# разрешаем перенос только по словам (не по символам)
|
||
overflow=ft.TextOverflow.FADE,
|
||
max_lines=2,
|
||
no_wrap=False,
|
||
),
|
||
ft.Text(
|
||
str(int(item["value"])),
|
||
size=11,
|
||
color=ds.clolors.ferst,
|
||
text_align=ft.TextAlign.CENTER,
|
||
),
|
||
],
|
||
),
|
||
))
|
||
|
||
super().__init__(
|
||
width=sw,
|
||
height=sh,
|
||
controls=[
|
||
ft.Container(
|
||
left=_PAD,
|
||
top=_PAD,
|
||
width=_CS,
|
||
height=_CS,
|
||
content=canvas,
|
||
),
|
||
*labels,
|
||
],
|
||
)
|