defmodule PhiaUi.Components.RadarChart do @moduledoc """ Spider / radar chart — pure SVG, zero JS. Plots multi-dimensional data on a polar grid. Each series is rendered as a filled polygon that fades in on entrance. ## Examples <.radar_chart data={[ %{axis: "Speed", value: 80}, %{axis: "Power", value: 65}, %{axis: "Endurance", value: 90}, %{axis: "Agility", value: 75}, %{axis: "Recovery", value: 70} ]} /> <.radar_chart series={[ %{name: "2023", data: [%{axis: "Q1", value: 80}, %{axis: "Q2", value: 60}]}, %{name: "2024", data: [%{axis: "Q1", value: 90}, %{axis: "Q2", value: 75}]} ]} /> """ use Phoenix.Component import PhiaUi.ClassMerger, only: [cn: 1] alias PhiaUi.Components.Data.ChartHelpers @cx 150.0 @cy 140.0 @r 110.0 @vw 320 @vh 290 attr :data, :list, default: [], doc: "Single-series: `[%{axis, value}]`." attr :series, :list, default: [], doc: "Multi-series: `[%{name, data: [%{axis, value}]}]`." attr :max, :any, default: 100, doc: "Maximum axis value (integer or float)." attr :colors, :list, default: [], doc: "Override default palette." attr :show_grid, :boolean, default: true, doc: "Show concentric polygon grid." attr :animate, :boolean, default: true attr :animation_duration, :integer, default: 700 attr :class, :string, default: nil attr :rest, :global def radar_chart(assigns) do series = cond do assigns.series != [] -> assigns.series assigns.data != [] -> [%{name: "Series 1", data: assigns.data}] true -> [] end axes = case series do [] -> [] [first | _] -> Enum.map(first.data, & &1.axis) end n = length(axes) # Axis angles: start from top (-pi/2), evenly spaced angles = Enum.map(0..(max(n - 1, 0)), fn i -> -:math.pi() / 2 + i / max(n, 1) * 2 * :math.pi() end) # Grid levels (20%, 40%, 60%, 80%, 100%) grid_levels = [0.2, 0.4, 0.6, 0.8, 1.0] grid_polygons = Enum.map(grid_levels, fn level -> points = angles |> Enum.map(fn a -> x = @cx + @r * level * :math.cos(a) y = @cy + @r * level * :math.sin(a) "#{Float.round(x, 2)},#{Float.round(y, 2)}" end) |> Enum.join(" ") points end) # Axis lines (from center to edge) axis_lines = Enum.map(angles, fn a -> %{ x1: @cx, y1: @cy, x2: Float.round(@cx + @r * :math.cos(a), 2), y2: Float.round(@cy + @r * :math.sin(a), 2) } end) # Axis labels (slightly beyond edge) label_r = @r + 14 axis_labels = axes |> Enum.with_index() |> Enum.map(fn {ax, i} -> a = Enum.at(angles, i) x = @cx + label_r * :math.cos(a) y = @cy + label_r * :math.sin(a) anchor = cond do :math.cos(a) > 0.1 -> "start" :math.cos(a) < -0.1 -> "end" true -> "middle" end %{label: ax, x: Float.round(x, 2), y: Float.round(y, 2), anchor: anchor} end) # Series polygons series_polygons = series |> Enum.with_index() |> Enum.map(fn {s, si} -> points = s.data |> Enum.with_index() |> Enum.map(fn {item, i} -> a = Enum.at(angles, i, 0.0) ratio = item.value / max(assigns.max, 1) x = @cx + @r * ratio * :math.cos(a) y = @cy + @r * ratio * :math.sin(a) "#{Float.round(x, 2)},#{Float.round(y, 2)}" end) |> Enum.join(" ") %{ points: points, color: ChartHelpers.chart_color(si, assigns.colors), delay_ms: si * 120 } end) assigns = assigns |> assign(:series_polygons, series_polygons) |> assign(:grid_polygons, grid_polygons) |> assign(:axis_lines, axis_lines) |> assign(:axis_labels, axis_labels) |> assign(:viewbox, "0 0 #{@vw} #{@vh}") ~H"""
""" end end