{"spec_id":"skewt-logp-atmospheric","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nskewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-21\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID = \"rgba(26, 26, 23, 0.10)\" if THEME == \"light\" else \"rgba(240, 239, 232, 0.10)\"\n\nBRAND = \"#009E73\"  # Okabe-Ito pos 1 — temperature profile\nC_DEWPT = \"#C475FD\"  # Okabe-Ito pos 2 — dewpoint profile\n\n# Reference line colors: subtle, theme-adaptive\nC_ISO = \"rgba(80, 80, 80, 0.22)\" if THEME == \"light\" else \"rgba(180, 180, 180, 0.28)\"\nC_DRY = \"rgba(196, 117, 253, 0.22)\" if THEME == \"light\" else \"rgba(196, 117, 253, 0.38)\"\nC_MOIST = \"rgba(68, 103, 163, 0.22)\" if THEME == \"light\" else \"rgba(68, 103, 163, 0.38)\"\nC_MIX = \"rgba(0, 158, 115, 0.22)\" if THEME == \"light\" else \"rgba(0, 158, 115, 0.38)\"\n\n# Skew-T transform: °C shift per log10 decade of pressure from 1000 hPa\nSKEW = 30.0\n\n\ndef skew_x(T, P):\n    return T + SKEW * np.log10(1000.0 / P)\n\n\n# Atmospheric constants\nRd = 287.05  # J/(kg·K)\nCp = 1004.0  # J/(kg·K)\nLv = 2.5e6  # J/kg\nRv = 461.5  # J/(kg·K)\nRd_Cp = Rd / Cp  # ≈ 0.2854\n\n\ndef moist_adiabat(T0_C, P_start=1000.0, P_end=100.0, n=150):\n    \"\"\"Integrate moist adiabat from P_start to P_end starting at T0_C.\"\"\"\n    P = np.linspace(P_start, P_end, n)\n    T = np.zeros(n)\n    T[0] = T0_C\n    for i in range(1, n):\n        T_K = T[i - 1] + 273.15\n        es = 6.112 * np.exp(17.67 * T[i - 1] / (T[i - 1] + 243.5))\n        rs = 0.622 * es / max(P[i - 1] - es, 0.001)\n        num = Rd * T_K + Lv * rs\n        den = P[i - 1] * (Cp + Lv**2 * rs / (Rv * T_K**2))\n        T[i] = T[i - 1] + (num / den) * (P[i] - P[i - 1])\n    return P, T\n\n\n# Data: synthetic warm-moist sounding, US Great Plains summer\npressure = np.array(\n    [1000, 975, 950, 925, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, 100]\n)\ntemperature = np.array(\n    [\n        32.0,\n        29.0,\n        26.0,\n        23.0,\n        20.5,\n        16.0,\n        11.0,\n        6.0,\n        2.0,\n        -3.5,\n        -8.5,\n        -15.0,\n        -21.5,\n        -29.0,\n        -37.5,\n        -48.0,\n        -56.0,\n        -62.0,\n        -64.5,\n        -68.0,\n        -73.0,\n    ]\n)\ndewpoint = np.array(\n    [\n        24.0,\n        22.0,\n        20.5,\n        18.0,\n        14.5,\n        9.0,\n        2.5,\n        -5.0,\n        -13.0,\n        -23.0,\n        -33.0,\n        -43.0,\n        -53.0,\n        -62.0,\n        -70.0,\n        -76.0,\n        -81.0,\n        -84.0,\n        -86.0,\n        -88.0,\n        -90.0,\n    ]\n)\n\n# Lifting Condensation Level (LCL) — surface parcel T=32°C, Td=24°C\n# At DALR=9.8°C/km and dewpoint cooling ~1.8°C/km, they meet at z≈1 km ≈ 900 hPa\nLCL_P = 900.0\nLCL_T = 22.0\n\n# Pressure array for reference lines\nP_ref = np.linspace(100.0, 1000.0, 200)\n\nfig = go.Figure()\n\n# --- Reference lines (background layer) ---\n\n# Isotherms (constant temperature, appear as diagonal lines when skewed)\nfor idx, T_iso in enumerate(range(-50, 55, 10)):\n    fig.add_trace(\n        go.Scatter(\n            x=skew_x(T_iso * np.ones_like(P_ref), P_ref),\n            y=P_ref,\n            mode=\"lines\",\n            line=dict(color=C_ISO, width=0.8),\n            legendgroup=\"iso\",\n            showlegend=(idx == 0),\n            name=\"Isotherms\",\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Dry adiabats (constant potential temperature θ)\nfor idx, theta in enumerate(range(290, 430, 10)):\n    T_dry = theta * (P_ref / 1000.0) ** Rd_Cp - 273.15\n    mask = (T_dry > -55) & (T_dry < 55)\n    x_d = np.where(mask, skew_x(T_dry, P_ref), np.nan)\n    if not np.all(np.isnan(x_d)):\n        fig.add_trace(\n            go.Scatter(\n                x=x_d,\n                y=P_ref,\n                mode=\"lines\",\n                line=dict(color=C_DRY, width=0.8),\n                legendgroup=\"dry\",\n                showlegend=(idx == 0),\n                name=\"Dry Adiabats\",\n                hoverinfo=\"skip\",\n            )\n        )\n\n# Moist adiabats (saturated adiabatic lapse rate, numerically integrated)\nfor idx, T0 in enumerate(range(-10, 45, 10)):\n    P_m, T_m = moist_adiabat(T0)\n    fig.add_trace(\n        go.Scatter(\n            x=skew_x(T_m, P_m),\n            y=P_m,\n            mode=\"lines\",\n            line=dict(color=C_MOIST, width=0.8, dash=\"dot\"),\n            legendgroup=\"moist\",\n            showlegend=(idx == 0),\n            name=\"Moist Adiabats\",\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Mixing ratio lines (constant water vapor mixing ratio)\nfor idx, r_gkg in enumerate([1, 2, 4, 8, 16]):\n    r = r_gkg / 1000.0\n    es = r * P_ref / (r + 0.622)\n    with np.errstate(divide=\"ignore\", invalid=\"ignore\"):\n        log_r = np.log(es / 6.112)\n        T_mix = 243.5 * log_r / (17.67 - log_r)\n    mask = np.isfinite(T_mix) & (T_mix > -50) & (T_mix < 35)\n    x_mix = np.where(mask, skew_x(T_mix, P_ref), np.nan)\n    fig.add_trace(\n        go.Scatter(\n            x=x_mix,\n            y=P_ref,\n            mode=\"lines\",\n            line=dict(color=C_MIX, width=0.8, dash=\"dash\"),\n            legendgroup=\"mix\",\n            showlegend=(idx == 0),\n            name=\"Mixing Ratio\",\n            hoverinfo=\"skip\",\n        )\n    )\n\n# --- Atmospheric sounding profiles ---\n\n# Dewpoint (dashed blue)\nfig.add_trace(\n    go.Scatter(\n        x=skew_x(dewpoint, pressure),\n        y=pressure,\n        mode=\"lines+markers\",\n        name=\"Dewpoint\",\n        line=dict(color=C_DEWPT, width=2.5, dash=\"dash\"),\n        marker=dict(color=C_DEWPT, size=7),\n        customdata=np.column_stack([dewpoint, pressure]),\n        hovertemplate=\"%{customdata[1]:.0f} hPa | Td: %{customdata[0]:.1f}°C<extra></extra>\",\n    )\n)\n\n# Temperature (solid green, Okabe-Ito pos 1)\nfig.add_trace(\n    go.Scatter(\n        x=skew_x(temperature, pressure),\n        y=pressure,\n        mode=\"lines+markers\",\n        name=\"Temperature\",\n        line=dict(color=BRAND, width=3.0),\n        marker=dict(color=BRAND, size=7),\n        customdata=np.column_stack([temperature, pressure]),\n        hovertemplate=\"%{customdata[1]:.0f} hPa | T: %{customdata[0]:.1f}°C<extra></extra>\",\n    )\n)\n\n# --- Layout ---\n\n# x-axis ticks: at P=1000 hPa, skew=0 so tick position = actual temperature\ntick_T = list(range(-50, 55, 10))\n\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    title=dict(\n        text=\"skewt-logp-atmospheric · python · plotly · anyplot.ai\",\n        font=dict(size=16, color=INK),\n        x=0.5,\n        xanchor=\"center\",\n    ),\n    xaxis=dict(\n        title=dict(text=\"Temperature (°C)\", font=dict(size=12, color=INK)),\n        tickfont=dict(size=10, color=INK_SOFT),\n        linecolor=INK_SOFT,\n        zeroline=False,\n        showgrid=False,\n        tickmode=\"array\",\n        tickvals=[float(t) for t in tick_T],\n        ticktext=[f\"{t}°\" for t in tick_T],\n        range=[-50.0, 75.0],\n    ),\n    yaxis=dict(\n        title=dict(text=\"Pressure (hPa)\", font=dict(size=12, color=INK)),\n        tickfont=dict(size=10, color=INK_SOFT),\n        gridcolor=GRID,\n        linecolor=INK_SOFT,\n        zeroline=False,\n        type=\"log\",\n        range=[3.0, 2.0],  # log10(1000)=3 at bottom → 1000 hPa, log10(100)=2 at top\n        tickmode=\"array\",\n        tickvals=[100, 150, 200, 250, 300, 400, 500, 600, 700, 850, 925, 1000],\n        ticktext=[\"100\", \"150\", \"200\", \"250\", \"300\", \"400\", \"500\", \"600\", \"700\", \"850\", \"925\", \"1000\"],\n        showgrid=True,\n    ),\n    legend=dict(\n        bgcolor=ELEVATED_BG,\n        bordercolor=INK_SOFT,\n        borderwidth=1,\n        font=dict(size=10, color=INK_SOFT),\n        x=0.02,\n        y=0.98,\n        xanchor=\"left\",\n        yanchor=\"top\",\n    ),\n    margin=dict(l=80, r=40, t=80, b=60),\n    annotations=[\n        dict(\n            x=skew_x(LCL_T, LCL_P),\n            y=LCL_P,\n            xref=\"x\",\n            yref=\"y\",\n            text=\"LCL ≈ 900 hPa\",\n            showarrow=True,\n            arrowhead=2,\n            arrowcolor=INK_SOFT,\n            arrowwidth=1.5,\n            ax=55,\n            ay=-30,\n            font=dict(size=10, color=INK),\n            bgcolor=ELEVATED_BG,\n            bordercolor=INK_SOFT,\n            borderwidth=1,\n            borderpad=4,\n            opacity=0.9,\n        )\n    ],\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}