prop/lib/microwaveprop_web/components/skew_t_chart.ex
Graham McIntire c3616b31c7
Surface HRRR weather details on /path
Adds an Atmospheric Profile section to the link calculator that shows
the HRRR data actually feeding the prediction: valid/run time, a
per-point table (src/mid/dst) with surface temp, dewpoint, pressure,
HPBL, PWAT, surface refractivity and dN/dh, plus a skew-T log-P chart
for the midpoint vertical profile.

Pulls the skew-T chart function out of contact_live into a shared
component so both LiveViews can render it without duplication.
2026-04-15 11:57:51 -05:00

180 lines
5.3 KiB
Elixir

defmodule MicrowavepropWeb.Components.SkewTChart do
@moduledoc """
Function component that renders a skew-T log-P diagram from a
pressure-level profile (list of `%{"pres", "tmpc", "dwpc"}` maps).
Returns empty output when the profile has fewer than three usable
levels.
"""
use Phoenix.Component
alias MicrowavepropWeb.SkewT
attr :profile, :list, required: true
def skew_t_chart(assigns) do
chart = SkewT.build(assigns.profile)
usable_points = if chart, do: length(chart.t_points), else: 0
assigns = assign(assigns, chart: chart, usable_points: usable_points)
~H"""
<div :if={@chart && @usable_points >= 2} class="mt-3">
<div class="text-xs font-semibold uppercase tracking-wider opacity-60 mb-1">
Skew-T log-P
</div>
<svg
viewBox={"0 0 #{@chart.width} #{@chart.height}"}
class="w-full max-w-[600px] bg-base-100 rounded border border-base-300"
role="img"
aria-label="Skew-T log-P diagram"
>
<%!-- Plot background --%>
<rect
x={@chart.padding_left}
y={@chart.padding_top}
width={@chart.plot_width}
height={@chart.plot_height}
fill="currentColor"
class="text-base-200/40"
/>
<%!-- Clip background curves to the plot area so skewed lines
don't bleed over the axes. --%>
<defs>
<clipPath id="skewt-clip">
<rect
x={@chart.padding_left}
y={@chart.padding_top}
width={@chart.plot_width}
height={@chart.plot_height}
/>
</clipPath>
</defs>
<g clip-path="url(#skewt-clip)">
<%!-- Dry adiabats (orange curves) --%>
<path
:for={adiabat <- @chart.dry_adiabats}
d={adiabat.path}
fill="none"
stroke="#f59e0b"
stroke-width="0.6"
stroke-opacity="0.45"
/>
<%!-- Mixing ratio lines (dashed teal) --%>
<path
:for={line <- @chart.mixing_ratio_lines}
d={line.path}
fill="none"
stroke="#0d9488"
stroke-width="0.6"
stroke-opacity="0.5"
stroke-dasharray="2 3"
/>
<%!-- Isotherms (gray) --%>
<line
:for={iso <- @chart.isotherms}
x1={iso.x1}
y1={iso.y1}
x2={iso.x2}
y2={iso.y2}
stroke="currentColor"
stroke-width="0.5"
class="text-base-content/25"
/>
<%!-- Isobars (gray, across plot) --%>
<line
:for={bar <- @chart.isobars}
x1={bar.x_start}
y1={bar.y}
x2={bar.x_end}
y2={bar.y}
stroke="currentColor"
stroke-width="0.5"
class="text-base-content/25"
/>
<%!-- Dewpoint trace --%>
<path
d={@chart.td_path}
fill="none"
stroke={@chart.dewpoint_color}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle
:for={{x, y} <- @chart.td_points}
cx={x}
cy={y}
r="2"
fill={@chart.dewpoint_color}
/>
<%!-- Temperature trace --%>
<path
d={@chart.t_path}
fill="none"
stroke={@chart.temp_color}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle
:for={{x, y} <- @chart.t_points}
cx={x}
cy={y}
r="2"
fill={@chart.temp_color}
/>
</g>
<%!-- Pressure axis labels (left side) --%>
<text
:for={bar <- @chart.isobars}
x={@chart.padding_left - 6}
y={bar.label_y}
font-size="10"
text-anchor="end"
fill="currentColor"
class="text-base-content/60"
>
{bar.label}
</text>
<%!-- Temperature axis labels (bottom, only those inside the visible band) --%>
<text
:for={iso <- @chart.isotherms}
:if={iso.label_x && iso.t_c >= -40 && iso.t_c <= 40 && rem(iso.t_c, 20) == 0}
x={iso.label_x}
y={iso.label_y}
font-size="10"
text-anchor="middle"
fill="currentColor"
class="text-base-content/60"
>
{iso.t_c}°C
</text>
<%!-- Legend --%>
<g transform={"translate(#{@chart.padding_left + 8}, #{@chart.padding_top + 12})"}>
<line x1="0" y1="0" x2="18" y2="0" stroke={@chart.temp_color} stroke-width="2" />
<text x="22" y="3" font-size="10" fill="currentColor" class="text-base-content/70">
Temperature
</text>
<line x1="0" y1="14" x2="18" y2="14" stroke={@chart.dewpoint_color} stroke-width="2" />
<text x="22" y="17" font-size="10" fill="currentColor" class="text-base-content/70">
Dewpoint
</text>
</g>
</svg>
<div class="text-[11px] opacity-60 mt-1">
Orange curves: dry adiabats · dashed teal: saturation mixing ratio ·
thin gray: isotherms and isobars
</div>
</div>
"""
end
end