{"spec_id":"psychrometric-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# psychrometric-basic: Psychrometric Chart for HVAC\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-06-16\n\nusing CairoMakie\nusing Colors\n\n# --- Theme tokens (see prompts/default-style-guide.md) -----------------------\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst GRID        = RGBAf(INK.r, INK.g, INK.b, 0.12)\n\n# Imprint palette — one hue per psychrometric property family\nconst RH_GREEN  = colorant\"#009E73\"  # 1 — relative humidity / saturation curve\nconst COMF_LAV  = colorant\"#C475FD\"  # 2 — comfort zone fill\nconst WB_BLUE   = colorant\"#4467A3\"  # 3 — wet-bulb temperature\nconst ENT_OCHRE = colorant\"#BD8233\"  # 4 — enthalpy (energy)\nconst VOL_CYAN  = colorant\"#2ABCCD\"  # 6 — specific volume\nconst PROC_ROSE = colorant\"#954477\"  # 7 — HVAC process path\n\n# --- Psychrometric model at sea-level pressure (101.325 kPa) ------------------\n# Standard atmosphere moist-air properties from the ASHRAE relations.\nconst P = 101325.0                              # Pa\ndry_bulb = collect(range(-10.0, 50.0, length = 400))   # °C, x-axis primary\n\n# Magnus saturation vapour pressure over water (Pa)\np_ws  = 610.94 .* exp.(17.625 .* dry_bulb ./ (dry_bulb .+ 243.04))\n# Saturation humidity ratio (g water / kg dry air)\nW_sat = 1000 .* 0.621945 .* p_ws ./ (P .- p_ws)\n\n# --- Figure ------------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title             = \"psychrometric-basic · julia · makie · anyplot.ai\",\n    titlesize         = 20,\n    titlecolor        = INK,\n    xlabel            = \"Dry-Bulb Temperature (°C)\",\n    ylabel            = \"Humidity Ratio (g water / kg dry air)\",\n    xlabelsize        = 15,\n    ylabelsize        = 15,\n    xlabelcolor       = INK,\n    ylabelcolor       = INK,\n    xticklabelsize    = 12,\n    yticklabelsize    = 12,\n    xticklabelcolor   = INK_SOFT,\n    yticklabelcolor   = INK_SOFT,\n    xtickcolor        = INK_SOFT,\n    ytickcolor        = INK_SOFT,\n    xticks            = -10:5:50,\n    yticks            = 0:5:30,\n    yaxisposition     = :right,          # humidity ratio reads on the right, as on a real chart\n    backgroundcolor   = PAGE_BG,\n    leftspinevisible  = false,\n    topspinevisible   = false,\n    rightspinecolor   = INK_SOFT,\n    bottomspinecolor  = INK_SOFT,\n    xgridcolor        = GRID,\n    ygridcolor        = GRID,\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n    limits            = (-10, 50, 0, 30),\n)\n\n# --- Comfort zone (≈ 20–26 °C, 30–60 % RH) -----------------------------------\ncomfort_T   = range(20.0, 26.0, length = 40)\ncomf_bottom = [Point2f(t, 1000 * 0.621945 * (0.30 * pw) / (P - 0.30 * pw))\n               for (t, pw) in zip(comfort_T,\n                   610.94 .* exp.(17.625 .* comfort_T ./ (comfort_T .+ 243.04)))]\ncomf_top    = [Point2f(t, 1000 * 0.621945 * (0.60 * pw) / (P - 0.60 * pw))\n               for (t, pw) in zip(reverse(comfort_T),\n                   610.94 .* exp.(17.625 .* reverse(comfort_T) ./ (reverse(comfort_T) .+ 243.04)))]\npoly!(ax, vcat(comf_bottom, comf_top); color = (COMF_LAV, 0.20),\n      strokecolor = (COMF_LAV, 0.85), strokewidth = 1.5, label = \"Comfort zone\")\ntext!(ax, 23.0, 8.0; text = \"Comfort\\nzone\", color = INK_SOFT, fontsize = 12,\n      align = (:center, :center), font = :bold)\n\n# --- Lines of constant specific volume (m³/kg) -------------------------------\n# Thinned family + lower alpha so the RH/saturation hierarchy reads cleanly upper-right.\nfor (k, v) in enumerate(0.80:0.03:0.95)\n    Wv = 1000 .* ((v .* 101.325 ./ (0.287042 .* (dry_bulb .+ 273.15))) .- 1) ./ 1.607858\n    Wc = [(0.0 <= w <= ws) ? w : NaN for (w, ws) in zip(Wv, W_sat)]\n    lines!(ax, dry_bulb, Wc; color = (VOL_CYAN, 0.45), linewidth = 1.0,\n           linestyle = :dashdot, label = k == 1 ? \"Specific volume (m³/kg)\" : nothing)\nend\n\n# --- Lines of constant enthalpy (kJ/kg) --------------------------------------\nfor (k, h) in enumerate(0.0:10.0:120.0)\n    Wh = 1000 .* (h .- 1.006 .* dry_bulb) ./ (2501 .+ 1.86 .* dry_bulb)\n    Wc = [(0.0 <= w <= ws) ? w : NaN for (w, ws) in zip(Wh, W_sat)]\n    lines!(ax, dry_bulb, Wc; color = (ENT_OCHRE, 0.6), linewidth = 1.0,\n           linestyle = :dot, label = k == 1 ? \"Enthalpy (kJ/kg)\" : nothing)\nend\n\n# --- Lines of constant wet-bulb temperature (°C) -----------------------------\nfor (k, twb) in enumerate(-5.0:5.0:30.0)\n    pws_wb = 610.94 * exp(17.625 * twb / (twb + 243.04))\n    Ws_wb  = 0.621945 * pws_wb / (P - pws_wb)\n    Ww = 1000 .* ((2501 .- 2.326 * twb) .* Ws_wb .- 1.006 .* (dry_bulb .- twb)) ./\n         (2501 .+ 1.86 .* dry_bulb .- 4.186 * twb)\n    Wc = [(0.0 <= w <= ws) ? w : NaN for (w, ws) in zip(Ww, W_sat)]\n    lines!(ax, dry_bulb, Wc; color = (WB_BLUE, 0.5), linewidth = 1.0,\n           linestyle = :dash, label = k == 1 ? \"Wet-bulb temp (°C)\" : nothing)\nend\n\n# --- Relative-humidity curves (10 %–100 %), saturation prominent -------------\nfor rh in 0.1:0.1:1.0\n    Wr = 1000 .* 0.621945 .* (rh .* p_ws) ./ (P .- rh .* p_ws)\n    saturated = rh == 1.0\n    lines!(ax, dry_bulb, Wr;\n           color     = saturated ? RH_GREEN : (RH_GREEN, 0.7),\n           linewidth = saturated ? 3.4 : 1.4,\n           label     = saturated ? \"Relative humidity\" : nothing)\nend\n\n# --- Direct property labels --------------------------------------------------\n# Relative humidity: along the W ≈ 16 g/kg reading line; curves that never reach it\n# (the drier 20 % line) are anchored a few °C inside the right edge so the label\n# clears the right spine.\nfor rh in (0.2, 0.4, 0.6, 0.8, 1.0)\n    Wr = 1000 .* 0.621945 .* (rh .* p_ws) ./ (P .- rh .* p_ws)\n    i = findfirst(>=(16.0), Wr)\n    if i === nothing\n        i = findlast(<=(47.0), dry_bulb)\n        text!(ax, dry_bulb[i], Wr[i]; text = \"$(round(Int, rh * 100))%\",\n              color = RH_GREEN, fontsize = 12, align = (:right, :bottom), font = :bold)\n    else\n        text!(ax, dry_bulb[i], Wr[i]; text = \"$(round(Int, rh * 100))%\",\n              color = RH_GREEN, fontsize = 12, align = (:center, :bottom), font = :bold)\n    end\nend\n\n# Enthalpy: in the empty triangle just outside the saturation curve\nfor h in (20.0, 40.0, 60.0, 80.0, 100.0)\n    Wh = 1000 .* (h .- 1.006 .* dry_bulb) ./ (2501 .+ 1.86 .* dry_bulb)\n    Wc = [(0.0 <= w <= ws) ? w : -Inf for (w, ws) in zip(Wh, W_sat)]\n    j = argmax(Wc)\n    text!(ax, dry_bulb[j] - 0.7, Wc[j] + 0.5; text = \"$(round(Int, h))\",\n          color = ENT_OCHRE, fontsize = 11, align = (:right, :bottom), font = :bold)\nend\n\n# Wet-bulb: at the dry end (lower right) of each line\nfor twb in (5.0, 15.0, 25.0)\n    pws_wb = 610.94 * exp(17.625 * twb / (twb + 243.04))\n    Ws_wb  = 0.621945 * pws_wb / (P - pws_wb)\n    Ww = 1000 .* ((2501 .- 2.326 * twb) .* Ws_wb .- 1.006 .* (dry_bulb .- twb)) ./\n         (2501 .+ 1.86 .* dry_bulb .- 4.186 * twb)\n    Wc = [(0.3 <= w <= ws) ? w : Inf for (w, ws) in zip(Ww, W_sat)]\n    j = argmin(Wc)\n    text!(ax, dry_bulb[j] + 0.4, Wc[j]; text = \"$(round(Int, twb))\",\n          color = WB_BLUE, fontsize = 11, align = (:left, :center), font = :bold)\nend\n\n# Specific volume: at the mid-point of each valid segment\nfor v in (0.80, 0.86, 0.92)\n    Wv = 1000 .* ((v .* 101.325 ./ (0.287042 .* (dry_bulb .+ 273.15))) .- 1) ./ 1.607858\n    valid = findall(i -> 0.5 <= Wv[i] <= W_sat[i], eachindex(dry_bulb))\n    isempty(valid) && continue\n    m = valid[cld(length(valid), 2)]\n    text!(ax, dry_bulb[m], Wv[m]; text = \"$(v)\", color = VOL_CYAN, fontsize = 11,\n          align = (:center, :bottom), font = :bold)\nend\n\n# --- Example HVAC process: cooling + dehumidification ------------------------\n# State 1: warm humid return air (30 °C, 50 % RH) → State 2: cool supply air (14 °C, 90 % RH)\npw1 = 0.50 * 610.94 * exp(17.625 * 30 / (30 + 243.04))\npw2 = 0.90 * 610.94 * exp(17.625 * 14 / (14 + 243.04))\nW1  = 1000 * 0.621945 * pw1 / (P - pw1)\nW2  = 1000 * 0.621945 * pw2 / (P - pw2)\n\nlines!(ax, [30.0, 14.0], [W1, W2]; color = PROC_ROSE, linewidth = 3.4,\n       label = \"Cooling + dehumidification\")\narrows!(ax, [30.0 + 0.8 * (14.0 - 30.0)], [W1 + 0.8 * (W2 - W1)],\n        [0.2 * (14.0 - 30.0)], [0.2 * (W2 - W1)];\n        color = PROC_ROSE, linewidth = 3.4, arrowsize = 18)\nscatter!(ax, [30.0, 14.0], [W1, W2]; color = PROC_ROSE, markersize = 13,\n         strokecolor = PAGE_BG, strokewidth = 2)\ntext!(ax, 30.5, W1; text = \"①  return air\", color = INK, fontsize = 13,\n      align = (:left, :center), font = :bold)\ntext!(ax, 13.0, W2 - 1.7; text = \"②  supply air\", color = INK, fontsize = 13,\n      align = (:right, :top), font = :bold)\n\n# --- Legend ------------------------------------------------------------------\naxislegend(ax; position = :rb, labelsize = 12, labelcolor = INK,\n           backgroundcolor = ELEVATED_BG, framecolor = INK_SOFT,\n           framevisible = true, padding = (10, 10, 8, 8), rowgap = 3)\n\n# --- Save --------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}