diff --git a/plots/skewt-logp-atmospheric/implementations/julia/makie.jl b/plots/skewt-logp-atmospheric/implementations/julia/makie.jl new file mode 100644 index 0000000000..e1f80202cb --- /dev/null +++ b/plots/skewt-logp-atmospheric/implementations/julia/makie.jl @@ -0,0 +1,323 @@ +# anyplot.ai +# skewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram +# Library: makie 0.22.10 | Julia 1.11.9 +# Quality: 85/100 | Created: 2026-05-22 + +using CairoMakie +using Colors +using Random + +Random.seed!(42) + +const THEME = get(ENV, "ANYPLOT_THEME", "light") +const PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17" +const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420" +const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8" +const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0" +const OKABE_ITO = [ + colorant"#009E73", + colorant"#D55E00", + colorant"#0072B2", + colorant"#CC79A7", + colorant"#E69F00", + colorant"#56B4E9", + colorant"#F0E442", +] + +# Skew-T parameters and thermodynamic constants +const SKEW = 45.0 +const Lv_CONST = 2.501e6 +const Rd_CONST = 287.0 +const Rv_CONST = 461.5 +const Cp_CONST = 1005.0 + +# Coordinate helpers +skew_x(T_C, P_hPa) = T_C + SKEW * log10(1000.0 / P_hPa) +y_p(P_hPa) = log10(P_hPa) + +# Saturation vapor pressure via Bolton (1980), hPa +sat_es(T_C) = 6.112 * exp(17.67 * T_C / (T_C + 243.5)) + +# Saturation mixing ratio, g/kg +ws_sat(T_C, P_hPa) = 622.0 * sat_es(T_C) / (P_hPa - sat_es(T_C)) + +# Log-spaced pressure array from 1000 → 100 hPa +const P_FINE = collect(exp10.(LinRange(log10(1000.0), log10(100.0), 300))) + +# Tropical convective sounding (standard atmosphere with instability) +const P_OBS = [1000.0, 950.0, 925.0, 900.0, 850.0, 800.0, 750.0, 700.0, + 650.0, 600.0, 550.0, 500.0, 450.0, 400.0, 350.0, 300.0, + 250.0, 200.0, 150.0, 100.0] + +const T_OBS = [28.4, 25.6, 23.8, 21.6, 17.2, 12.4, 8.0, 3.2, + -1.8, -7.2, -12.8, -19.2, -25.6, -33.2, -41.6, -50.4, + -59.2, -65.8, -68.4, -72.6] + +const TD_OBS = [24.8, 21.4, 19.6, 16.8, 10.4, 5.2, -0.8, -8.4, + -15.2, -22.6, -30.2, -38.4, -46.8, -52.6, -58.8, -63.2, + -68.4, -72.0, -74.2, -77.6] + +# Reference line helpers + +function isotherm_line(T0, p_arr) + return [skew_x(T0, P) for P in p_arr], y_p.(p_arr) +end + +function dry_adiabat_line(theta_K, p_arr) + Rcp = Rd_CONST / Cp_CONST + return [skew_x(theta_K * (P / 1000.0)^Rcp - 273.15, P) for P in p_arr], y_p.(p_arr) +end + +function mixing_ratio_line(ws_gkg, p_arr) + xs = map(p_arr) do P + es_t = ws_gkg * P / (622.0 + ws_gkg) + es_t <= 0.0 && return NaN + a = log(es_t / 6.112) + skew_x(243.5 * a / (17.67 - a), P) + end + return xs, y_p.(p_arr) +end + +function moist_adiabat_line(T0_C, P0_hPa, p_arr) + xs = Float64[] + ys = Float64[] + T_K = T0_C + 273.15 + for i in 1:length(p_arr) + P = p_arr[i] + P > P0_hPa + 0.5 && continue + push!(xs, skew_x(T_K - 273.15, P)) + push!(ys, y_p(P)) + if i < length(p_arr) + ws = max(0.0, ws_sat(T_K - 273.15, P) / 1000.0) + dT_dp = (Rd_CONST * T_K + Lv_CONST * ws) / + (P * (Cp_CONST + Lv_CONST^2 * ws / (Rv_CONST * T_K^2))) + T_K = T_K + dT_dp * (p_arr[i + 1] - P) + end + end + return xs, ys +end + +# Compute lifted parcel temperatures (Bolton 1980) and return LCL metadata. +# p_arr must be sorted descending (1000 → 100 hPa). +function lifted_parcel_temps(T_surf_C, Td_surf_C, P_surf_hPa, p_arr) + T_K = T_surf_C + 273.15 + Td_K = Td_surf_C + 273.15 + Rcp = Rd_CONST / Cp_CONST + + # LCL temperature and pressure (Bolton 1980) + T_LCL_K = 56.0 + 1.0 / (1.0 / (Td_K - 56.0) + log(T_K / Td_K) / 800.0) + P_LCL = P_surf_hPa * (T_LCL_K / T_K)^(Cp_CONST / Rd_CONST) + + T_moist = T_LCL_K + prev_P = P_LCL + T_out = Float64[] + + for P in p_arr + if P >= P_LCL + # Dry adiabatic lifting + push!(T_out, T_K * (P / P_surf_hPa)^Rcp - 273.15) + else + # Moist adiabatic lifting — one Euler step from prev level + ws = max(0.0, ws_sat(T_moist - 273.15, prev_P) / 1000.0) + dT_dp = (Rd_CONST * T_moist + Lv_CONST * ws) / + (prev_P * (Cp_CONST + Lv_CONST^2 * ws / (Rv_CONST * T_moist^2))) + T_moist = T_moist + dT_dp * (P - prev_P) + prev_P = P + push!(T_out, T_moist - 273.15) + end + end + return T_out, T_LCL_K - 273.15, P_LCL +end + +# Parcel path at both resolutions +parcel_Ts_obs, T_LCL_C, P_LCL_hPa = lifted_parcel_temps( + T_OBS[1], TD_OBS[1], P_OBS[1], P_OBS) +parcel_Ts_fine, _, _ = lifted_parcel_temps( + T_OBS[1], TD_OBS[1], P_OBS[1], P_FINE) + +# Figure +fig = Figure( + size = (1600, 900), + fontsize = 14, + backgroundcolor = PAGE_BG, +) + +ax = Axis( + fig[1, 1]; + title = "skewt-logp-atmospheric · julia · makie · anyplot.ai", + titlesize = 20, + titlecolor = INK, + xlabel = "Temperature (°C)", + ylabel = "Pressure (hPa)", + xlabelsize = 14, + ylabelsize = 14, + xticklabelsize = 12, + yticklabelsize = 12, + xlabelcolor = INK, + ylabelcolor = INK, + xticklabelcolor = INK_SOFT, + yticklabelcolor = INK_SOFT, + xtickcolor = INK_SOFT, + ytickcolor = INK_SOFT, + backgroundcolor = PAGE_BG, + leftspinecolor = INK_SOFT, + bottomspinecolor = INK_SOFT, + topspinevisible = false, + rightspinevisible = false, + xgridvisible = false, + ygridvisible = false, + yreversed = true, +) + +# Y-axis: log10(P) with 1000 hPa at bottom, 100 hPa at top +ylims!(ax, y_p(100.0) - 0.02, y_p(1000.0) + 0.02) +const P_YTICKS = [1000.0, 925.0, 850.0, 700.0, 500.0, 400.0, 300.0, 200.0, 100.0] +ax.yticks = (y_p.(P_YTICKS), string.(Int.(P_YTICKS))) + +# X-axis: skewed temperature coordinate, ticks at surface temps +xlims!(ax, -47.0, 88.0) +const T_XTICKS = Float64[-40, -30, -20, -10, 0, 10, 20, 30, 40] +ax.xticks = (T_XTICKS, string.(Int.(T_XTICKS)) .* "°") + +# Background isobars +isobar_col = RGBAf(INK.r, INK.g, INK.b, 0.08f0) +for P in [950.0, 925.0, 850.0, 800.0, 750.0, 700.0, 650.0, 600.0, 550.0, + 500.0, 450.0, 400.0, 350.0, 300.0, 250.0, 200.0, 150.0] + hlines!(ax, y_p(P); color = isobar_col, linewidth = 0.6) +end + +# Isotherms (every 10 °C) +iso_col = THEME == "light" ? + RGBAf(0.55f0, 0.55f0, 0.55f0, 0.28f0) : + RGBAf(0.62f0, 0.62f0, 0.62f0, 0.22f0) + +for T0 in -60.0:10.0:60.0 + xs, ys = isotherm_line(T0, P_FINE) + lines!(ax, xs, ys; color = iso_col, linewidth = 0.7) + x_bot = skew_x(T0, 1000.0) + if -47.0 <= x_bot <= 88.0 + text!(ax, x_bot, y_p(1000.0) - 0.014; + text = "$(Int(T0))°", + fontsize = 9, + color = INK_SOFT, + align = (:center, :top)) + end +end + +# Dry adiabats (potential temperature from -40 to 80 °C, every 10 °C) +dry_col = THEME == "light" ? + RGBAf(0.84f0, 0.37f0, 0.0f0, 0.40f0) : + RGBAf(0.90f0, 0.55f0, 0.20f0, 0.33f0) + +for (i, theta_C) in enumerate(-40.0:10.0:80.0) + xs, ys = dry_adiabat_line(theta_C + 273.15, P_FINE) + lbl = i == 1 ? "Dry adiabat" : nothing + if isnothing(lbl) + lines!(ax, xs, ys; color = dry_col, linewidth = 0.8, linestyle = :dash) + else + lines!(ax, xs, ys; color = dry_col, linewidth = 0.8, linestyle = :dash, label = lbl) + end +end + +# Moist adiabats (surface start temps -5 to 35 °C, every 5 °C) +moist_col = THEME == "light" ? + RGBAf(0.0f0, 0.447f0, 0.698f0, 0.42f0) : + RGBAf(0.25f0, 0.62f0, 0.87f0, 0.35f0) + +for (i, T_start) in enumerate(-5.0:5.0:35.0) + xs, ys = moist_adiabat_line(T_start, 1000.0, P_FINE) + lbl = i == 1 ? "Moist adiabat" : nothing + if isnothing(lbl) + lines!(ax, xs, ys; color = moist_col, linewidth = 0.8, linestyle = :dashdot) + else + lines!(ax, xs, ys; color = moist_col, linewidth = 0.8, linestyle = :dashdot, label = lbl) + end +end + +# Mixing ratio lines (g/kg), displayed only below 600 hPa +p_low = P_FINE[P_FINE .>= 599.0] +mix_col = THEME == "light" ? + RGBAf(0.0f0, 0.62f0, 0.45f0, 0.48f0) : + RGBAf(0.0f0, 0.75f0, 0.55f0, 0.40f0) + +for (i, ws_gkg) in enumerate([2.0, 4.0, 8.0, 12.0, 20.0]) + xs, ys = mixing_ratio_line(ws_gkg, p_low) + valid = .!isnan.(xs) .& isfinite.(xs) + lbl = i == 1 ? "Mixing ratio" : nothing + if any(valid) + if isnothing(lbl) + lines!(ax, xs[valid], ys[valid]; color = mix_col, linewidth = 0.7, linestyle = :dot) + else + lines!(ax, xs[valid], ys[valid]; color = mix_col, linewidth = 0.7, linestyle = :dot, label = lbl) + end + # Label at top of visible section (600 hPa) + x_top = xs[valid][end] + y_top = ys[valid][end] + if -47.0 <= x_top <= 88.0 + text!(ax, x_top, y_top; + text = "$(Int(ws_gkg))", + fontsize = 8, + color = mix_col, + align = (:center, :bottom)) + end + end +end + +# CAPE region — poly! fills the closed polygon between parcel and environment. +# This uses Makie's native polygon primitive for efficient filled-area rendering. +cape_mask = parcel_Ts_obs .> T_OBS +if any(cape_mask) + ci = findall(cape_mask) + env_xs_c = [skew_x(T_OBS[i], P_OBS[i]) for i in ci] + par_xs_c = [skew_x(parcel_Ts_obs[i], P_OBS[i]) for i in ci] + ys_c = y_p.(P_OBS[ci]) + cape_pts = Point2f.( + vcat(par_xs_c, reverse(env_xs_c)), + vcat(ys_c, reverse(ys_c)), + ) + cape_fill = RGBAf(OKABE_ITO[5].r, OKABE_ITO[5].g, OKABE_ITO[5].b, 0.22f0) + poly!(ax, cape_pts; color = cape_fill, strokewidth = 0, label = "CAPE") +end + +# Lifted parcel trace (smooth, fine resolution) +parcel_xs_fine = [skew_x(T, P) for (T, P) in zip(parcel_Ts_fine, P_FINE)] +lines!(ax, parcel_xs_fine, y_p.(P_FINE); + color = OKABE_ITO[4], linewidth = 2.0, linestyle = :dashdotdot, + label = "Lifted parcel") + +# LCL marker +lcl_x = skew_x(T_LCL_C, P_LCL_hPa) +lcl_y = y_p(P_LCL_hPa) +scatter!(ax, [lcl_x], [lcl_y]; + color = OKABE_ITO[4], markersize = 10, marker = :diamond, strokewidth = 0) +text!(ax, lcl_x + 1.5, lcl_y; + text = "LCL", + fontsize = 9, + color = OKABE_ITO[4], + align = (:left, :center)) + +# Sounding profiles +T_xs = [skew_x(T, P) for (T, P) in zip(T_OBS, P_OBS)] +TD_xs = [skew_x(Td, P) for (Td, P) in zip(TD_OBS, P_OBS)] +obs_ys = y_p.(P_OBS) + +lines!(ax, T_xs, obs_ys; + color = OKABE_ITO[1], linewidth = 3.5, label = "Temperature") +scatter!(ax, T_xs, obs_ys; + color = OKABE_ITO[1], markersize = 7, strokewidth = 0) + +lines!(ax, TD_xs, obs_ys; + color = OKABE_ITO[2], linewidth = 3.5, linestyle = :dash, label = "Dewpoint") +scatter!(ax, TD_xs, obs_ys; + color = OKABE_ITO[2], markersize = 7, strokewidth = 0) + +axislegend(ax; + position = :rt, + backgroundcolor = ELEVATED_BG, + labelcolor = INK, + framecolor = INK_SOFT, + framewidth = 0.8, + labelsize = 12, +) + +save("plot-$(THEME).png", fig; px_per_unit = 2) diff --git a/plots/skewt-logp-atmospheric/metadata/julia/makie.yaml b/plots/skewt-logp-atmospheric/metadata/julia/makie.yaml new file mode 100644 index 0000000000..1db0fa13a0 --- /dev/null +++ b/plots/skewt-logp-atmospheric/metadata/julia/makie.yaml @@ -0,0 +1,253 @@ +library: makie +language: julia +specification_id: skewt-logp-atmospheric +created: '2026-05-22T21:51:33Z' +updated: '2026-05-22T22:26:43Z' +generated_by: claude-sonnet +workflow_run: 26312661516 +issue: 3802 +language_version: 1.11.9 +library_version: 0.22.10 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/skewt-logp-atmospheric/julia/makie/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/skewt-logp-atmospheric/julia/makie/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 85 +review: + strengths: + - Mathematically correct Skew-T coordinate transformation (45° skew + log10 pressure + y-axis) implemented from first principles + - 'Complete feature set: dry adiabats, moist adiabats, mixing ratio lines, CAPE + region, lifted parcel trace, LCL marker — all from thermodynamic equations' + - 'Theme adaptation is flawless: all chrome tokens (PAGE_BG, ELEVATED_BG, INK, INK_SOFT, + isobar_col, reference line colors) flip correctly between light and dark; data + colors stay identical' + - 'First series (Temperature) correctly uses Okabe-Ito brand green #009E73; Dewpoint + uses position 2 (#D55E00)' + - Reference lines are intentionally alpha-blended to stay subordinate to the main + data profiles — appropriate visual hierarchy for a complex scientific diagram + - Uses poly! with Point2f polygon construction for CAPE area — idiomatic Makie for + filled region + weaknesses: + - CAPE fill uses alpha=0.22 with OKABE_ITO[5] (#E69F00 orange) — on the warm off-white + light background this is nearly invisible; increase alpha to 0.40–0.50 so the + CAPE region is clearly distinguishable from the background + - Isotherm axis labels use fontsize=9 and mixing ratio labels use fontsize=8 — too + small to read when chart is scaled to ~400 px mobile width; increase both to fontsize=10 + - Lifted parcel trace uses linewidth=2.0 which is noticeably thinner than the main + profiles (3.5) — increase to 2.5 for better visibility in regions where it runs + close to the environment temperature line + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) — correct + Chrome: Title "skewt-logp-atmospheric · julia · makie · anyplot.ai" in dark ink, clearly readable. Y-axis label "Pressure (hPa)" and X-axis label "Temperature (°C)" both readable with units. Tick labels (pressure levels and temperature degrees) visible in INK_SOFT color. + Data: Temperature profile in brand green #009E73 (linewidth 3.5, solid) — prominent and clearly visible. Dewpoint profile in #D55E00 orange-vermillion (linewidth 3.5, dashed) — clearly visible. Reference lines (isotherms, dry adiabats, moist adiabats, mixing ratio) rendered at low alpha as background context. Lifted parcel trace in #CC79A7 (linewidth 2.0, dashdotdot). CAPE fill in orange (#E69F00 alpha=0.22) — very subtle, nearly invisible on light background. LCL diamond marker visible near 800 hPa with small "LCL" label. + Legibility verdict: PASS (all primary text readable; small annotation labels fontsize 8-9 are a minor concern at mobile scale) + + Dark render (plot-dark.png): + Background: Near-black (#1A1A17) — correct + Chrome: All chrome tokens adapt to dark theme — title, axis labels, tick labels shown in light-colored INK (#F0EFE8) and INK_SOFT (#B8B7B0). Legend box uses ELEVATED_BG (#242420). No dark-on-dark failures detected. Isobar lines adapt to light-on-dark alpha. + Data: Data colors identical to light render — green temperature line, orange dashed dewpoint, reddish-purple lifted parcel trace. CAPE fill remains subtle at alpha=0.22. Reference lines visible against dark background. + Legibility verdict: PASS (all text readable against dark background; no dark-on-dark failures) + criteria_checklist: + visual_quality: + score: 24 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 6 + max: 8 + passed: true + comment: Title, axis labels, tick labels readable in both themes. Isotherm + labels (fontsize=9) and mixing ratio labels (fontsize=8) are very small + at mobile scale + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: No significant overlaps; small annotation labels placed carefully + - id: VQ-03 + name: Element Visibility + score: 4 + max: 6 + passed: true + comment: Main profiles clearly visible. CAPE fill (alpha=0.22) nearly invisible + on light background. Lifted parcel trace thinner than main profiles + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Okabe-Ito palette CVD-safe; good contrast between temperature and + dewpoint + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Canvas 3200x1800 confirmed. Good proportions. Legend slightly dense + with 7 entries + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Temperature (°C) and Pressure (hPa) with units; title format correct + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series #009E73, second #D55E00; backgrounds #FAF8F1/#1A1A17; + all chrome adapts correctly' + design_excellence: + score: 14 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Impressive thermodynamic implementation; intentional alpha transparency + hierarchy; professional scientific aesthetics + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: L-shaped spine; isobars replace grid appropriately; multiple linestyles + create visual hierarchy + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Temperature profile as focal point; CAPE region adds interpretation; + LCL marker annotates key derived quantity + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct Skew-T Log-P with logarithmic inverted pressure axis and + 45-degree skewed isotherms + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Dry adiabats, moist adiabats, mixing ratio lines, temperature (solid), + dewpoint (dashed), CAPE, lifted parcel, LCL all present; wind barbs omitted + (optional) + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Pressure logarithmic and inverted; temperature coordinate correctly + skewed 45 degrees + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format correct; legend labels all 7 series + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: 'All major Skew-T features: temperature/dewpoint profiles, three + reference line types, CAPE, lifted parcel, LCL' + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Tropical convective sounding — physically plausible temperatures, + realistic lapse rate, appropriate surface conditions + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Pressure 100-1000 hPa; temperature -72°C to +28°C — appropriate for + complete tropospheric sounding + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: true + comment: Helper functions are genuinely necessary for thermodynamic calculations; + no gratuitous abstraction + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Random.seed!(42); sounding data hardcoded — fully deterministic + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: CairoMakie, Colors, Random all used + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Well-organized with clear section comments; correct NaN handling + for mixing ratio + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: save("plot-$(THEME).png", fig; px_per_unit = 2); modern Makie size= + API + library_mastery: + score: 8 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Correct CairoMakie primitives (lines!, scatter!, poly!, hlines!, + text!, axislegend); RGBAf for alpha; Point2f for polygon + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: poly! for CAPE fill; custom log10 y-axis tick mapping; axislegend + with styled frame; diamond marker for LCL + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - manual-ticks + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: + - alpha-blending