fix(skewt): stack overlapping critical-level labels + widen rail

LCL/LFC and 0 °C/WBZ landed within ~5 px of each other on the right-
edge label rail and stomped on each other's text. Sort by y, bump
each subsequent label down by at least 14 px from the previous one,
and draw a thin leader from the true tick to the bumped baseline so
nothing reads as misaligned.

Also widened the SVG from 720 → 820 user units so longer entries
like `LCL 925 mb` no longer get clipped by the right edge.
This commit is contained in:
Graham McIntire 2026-04-25 15:33:14 -05:00
parent 2fc434df56
commit 6967614dfb
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -12,8 +12,10 @@ defmodule MicrowavepropWeb.SkewtSvg do
"""
alias Microwaveprop.Weather.SkewtParams
# Plot canvas (SVG user units).
@width 720
# Plot canvas (SVG user units). The right margin (right edge of the
# plot box → SVG edge) is sized to fit the longest critical-level
# label rail entry, e.g. `LCL 925 mb`, without clipping.
@width 820
@height 640
@left 60
@right 660
@ -142,26 +144,46 @@ defmodule MicrowavepropWeb.SkewtSvg do
# in-plot tick. Mirrors SPC's `143 mb`, `158 mb (mix)`, etc. rail.
defp critical_level_markers([]), do: ""
# Walk the levels in y order and bump any text whose baseline would
# land within @critical_label_min_dy of the previous one. The tick
# always sits at the level's true y; only the label gets nudged with
# a thin leader so close levels (e.g. LCL/LFC, 0 °C/WBZ) read cleanly.
defp critical_level_markers(levels) do
Enum.flat_map(levels, fn
levels
|> Enum.flat_map(fn
%{label: label, pressure_mb: p} when is_number(p) ->
y = pressure_y(p)
if y >= @top and y <= @bottom do
[render_critical_marker(label, p, y)]
else
[]
end
if y >= @top and y <= @bottom, do: [{label, p, y}], else: []
_ ->
[]
end)
|> Enum.sort_by(fn {_lbl, _p, y} -> y end)
|> Enum.map_reduce(@top, fn {label, p, y}, last_text_y ->
text_y = max(y, last_text_y + @critical_label_min_dy)
iodata = render_critical_marker(label, p, y, text_y)
{iodata, text_y}
end)
|> elem(0)
end
defp render_critical_marker(label, pressure_mb, y) do
# Minimum vertical gap between two stacked label baselines (px in
# SVG user units). 14 leaves the 10 px text plus a hair of breathing
# room — anything tighter starts overlapping descenders.
@critical_label_min_dy 14
defp render_critical_marker(label, pressure_mb, y, text_y) do
leader =
if abs(text_y - y) > 0.5 do
~s|<line class="critical-leader" x1="#{@right}" y1="#{r(y)}" x2="#{@right + 4}" y2="#{r(text_y)}"/>|
else
""
end
[
~s|<line class="critical-tick" x1="#{@right - 8}" y1="#{r(y)}" x2="#{@right}" y2="#{r(y)}"/>|,
~s|<text class="critical-label" x="#{@right + 4}" y="#{r(y) + 3}">#{label} #{round(pressure_mb)} mb</text>|
leader,
~s|<text class="critical-label" x="#{@right + 6}" y="#{r(text_y) + 3}">#{label} #{round(pressure_mb)} mb</text>|
]
end
@ -245,6 +267,7 @@ defmodule MicrowavepropWeb.SkewtSvg do
.virtual-temp-line { fill: none; stroke: #fb7185; stroke-width: 1; stroke-dasharray: 1 3; stroke-linejoin: round; stroke-linecap: round; }
.wet-bulb-line { fill: none; stroke: #2563eb; stroke-width: 1.4; stroke-linejoin: round; stroke-linecap: round; opacity: 0.85; }
.critical-tick { stroke: currentColor; stroke-width: 1.2; }
.critical-leader { stroke: currentColor; stroke-opacity: 0.45; stroke-width: 0.8; }
.critical-label { font: 10px ui-sans-serif, system-ui, sans-serif; fill: currentColor; fill-opacity: 0.85; }
.skewt-cursor-line { stroke: currentColor; stroke-opacity: 0.5; stroke-width: 0.8; stroke-dasharray: 3 3; pointer-events: none; }
.skewt-cursor-dot-t { fill: #ff0000; pointer-events: none; }