diff --git a/lib/microwaveprop_web/live/path_live.ex b/lib/microwaveprop_web/live/path_live.ex index 071a3768..4592b099 100644 --- a/lib/microwaveprop_web/live/path_live.ex +++ b/lib/microwaveprop_web/live/path_live.ex @@ -825,9 +825,8 @@ defmodule MicrowavepropWeb.PathLive do
Refrac Gradient
- {format_number(@result.conditions.min_refractivity_gradient || 0)} + {format_number(@result.conditions.min_refractivity_gradient || 0)} N/km
-
N/km
@@ -846,46 +845,7 @@ defmodule MicrowavepropWeb.PathLive do <%!-- 18-Hour Forecast --%> <%= if @result.forecast != [] do %> -
-
- 18-Hour Propagation Forecast · {@result.band_config.label} -
-
- <%= for point <- @result.forecast do %> - <% pct = max(point.score, 2) %> -
-
- <% end %> -
-
- <%= if length(@result.forecast) > 0 do %> - {format_utc_hour(List.first(@result.forecast).valid_time)} UTC - {format_utc_hour(List.last(@result.forecast).valid_time)} UTC - <% end %> -
-
- <% best = Enum.max_by(@result.forecast, & &1.score) %> - - Best: - - {best.score} - - at {format_utc_hour(best.valid_time)} UTC - - <% worst = Enum.min_by(@result.forecast, & &1.score) %> - - Worst: - - {worst.score} - - at {format_utc_hour(worst.valid_time)} UTC - -
-
+ <.forecast_chart forecast={@result.forecast} band_config={@result.band_config} /> <% end %> <%!-- Share link --%> @@ -902,6 +862,195 @@ defmodule MicrowavepropWeb.PathLive do """ end + attr :forecast, :list, required: true + attr :band_config, :map, required: true + + defp forecast_chart(assigns) do + points = build_forecast_points(assigns.forecast) + + assigns = + assigns + |> assign(:points, points) + |> assign(:polyline, Enum.map_join(points, " ", fn p -> "#{p.x},#{p.y}" end)) + |> assign(:now_pt, Enum.find(points, & &1.now?) || hd(points)) + |> assign(:best_pt, Enum.max_by(points, & &1.score)) + |> assign(:worst_pt, Enum.min_by(points, & &1.score)) + |> assign(:trend, forecast_trend(points)) + |> assign(:x_labels, forecast_x_labels(points)) + + ~H""" +
+
+
+ {length(@forecast)}-Hour Propagation Forecast · {@band_config.label} +
+
+ {Phoenix.HTML.raw(@trend)} +
+
+ + <%= for s <- [0, 50, 100] do %> + <% y = 6 + (100 - s) / 100 * 70 %> + + + {s} + + <% end %> + + <%= for pt <- @points do %> + + <% end %> + + + + {@best_pt.score} + + <%= for {x, label} <- @x_labels do %> + + {label} + + <% end %> + +
+ + Best: + + {@best_pt.score} + + at {@best_pt.label} UTC + + + Worst: + + {@worst_pt.score} + + at {@worst_pt.label} UTC + +
+
+ """ + end + + defp build_forecast_points(forecast) do + n = length(forecast) + now = DateTime.utc_now() + + # Map each forecast point to SVG coords (viewBox 0 0 300 96) + # plot area: x in [28, 294], y in [6, 76] + indexed = Enum.with_index(forecast) + + now_idx = + indexed + |> Enum.min_by(fn {p, _i} -> abs(DateTime.diff(p.valid_time, now, :second)) end) + |> elem(1) + + Enum.map(indexed, fn {p, i} -> + x = + if n <= 1 do + 161 + else + 28 + i / (n - 1) * 266 + end + + y = 6 + (100 - p.score) / 100 * 70 + + %{ + x: Float.round(x * 1.0, 2), + y: Float.round(y * 1.0, 2), + score: p.score, + valid_time: p.valid_time, + label: format_utc_hour(p.valid_time), + now?: i == now_idx + } + end) + end + + defp forecast_trend(points) do + now_idx = Enum.find_index(points, & &1.now?) || 0 + future = Enum.drop(points, now_idx) + + case future do + [first | _] -> + last = List.last(future) + diff = last.score - first.score + + cond do + diff > 5 -> ~s(▲ Improving) + diff < -5 -> ~s(▼ Declining) + true -> ~s(→ Steady) + end + + _ -> + ~s(→ Steady) + end + end + + defp forecast_x_labels(points) do + n = length(points) + + indices = + if n <= 3 do + Enum.to_list(0..(n - 1)) + else + [0, div(n - 1, 2), n - 1] + end + + Enum.map(indices, fn i -> + p = Enum.at(points, i) + {p.x, p.label} + end) + end + attr :label, :string, required: true attr :value, :any, required: true attr :unit, :string, required: true