- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
181 lines
5.4 KiB
Elixir
181 lines
5.4 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
|
|
|
|
@spec skew_t_chart(map()) :: Phoenix.LiveView.Rendered.t()
|
|
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
|