{"spec_id":"skewt-logp-atmospheric","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nskewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-05-21\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_segment,\n    ggplot,\n    labs,\n    scale_color_manual,\n    scale_linetype_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\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\"\n\n# Skew-T transformation parameters\nSKEW_FACTOR = np.tan(np.radians(45))  # ~1.0 for 45-degree isotherms\nP_SURFACE = 1000  # Reference pressure (hPa)\nP_MIN = 100\nP_MAX = 1050\nT_MIN = -80\nT_MAX = 50\n\n\ndef skew_transform(temp, pressure):\n    log_p = np.log10(pressure / P_SURFACE)\n    return temp + SKEW_FACTOR * log_p * 40\n\n\n# Generate synthetic atmospheric sounding data\nnp.random.seed(42)\n\npressure_levels = np.array(\n    [\n        1000,\n        975,\n        950,\n        925,\n        900,\n        875,\n        850,\n        825,\n        800,\n        775,\n        750,\n        700,\n        650,\n        600,\n        550,\n        500,\n        450,\n        400,\n        350,\n        300,\n        250,\n        200,\n        175,\n        150,\n        125,\n        100,\n    ]\n)\n\n# Temperature profile (realistic lapse rate with tropopause)\nbase_temp = 25\ntemp_profile = []\nfor p in pressure_levels:\n    altitude_km = 44.3308 * (1 - (p / 1013.25) ** 0.190284)\n    if p > 200:  # Troposphere\n        t = base_temp - 6.5 * altitude_km + np.random.normal(0, 1)\n    else:  # Stratosphere (isothermal/slight warming)\n        t = -55 + (200 - p) * 0.02 + np.random.normal(0, 0.5)\n    temp_profile.append(t)\ntemp_profile = np.array(temp_profile)\n\n# Dewpoint profile (moisture decreases with altitude)\ndewpoint_profile = []\nfor p, t in zip(pressure_levels, temp_profile, strict=True):\n    if p > 700:\n        depression = 5 + np.random.uniform(0, 5)\n    elif p > 400:\n        depression = 15 + np.random.uniform(0, 10)\n    else:\n        depression = 30 + np.random.uniform(0, 15)\n    dewpoint_profile.append(min(t - depression, t))\ndewpoint_profile = np.array(dewpoint_profile)\n\n# Reference lines\n\n# Isotherms (skewed 45°)\nisotherm_data = []\nfor t_ref in np.arange(-100, 60, 10):\n    p_range = np.array([P_MIN, P_MAX])\n    xs = skew_transform(np.array([t_ref, t_ref]), p_range)\n    isotherm_data.append({\"x\": xs[0], \"xend\": xs[1], \"y\": P_MIN, \"yend\": P_MAX, \"line_type\": \"Isotherms (10°C)\"})\nisotherm_df = pd.DataFrame(isotherm_data)\n\n# Isobars (horizontal reference lines — structural, no legend entry)\nisobar_data = []\nfor p in [1000, 850, 700, 500, 300, 200, 100]:\n    isobar_data.append({\"x\": skew_transform(T_MIN - 20, p), \"xend\": skew_transform(T_MAX + 20, p), \"y\": p, \"yend\": p})\nisobar_df = pd.DataFrame(isobar_data)\n\n# Dry adiabats (constant potential temperature)\ndry_adiabat_data = []\nfor theta in np.arange(-40, 100, 10):\n    pts_x, pts_y = [], []\n    for p in np.arange(P_MIN, P_MAX + 1, 25):\n        t = (theta + 273.15) * (p / 1000) ** 0.286 - 273.15\n        pts_x.append(skew_transform(t, p))\n        pts_y.append(p)\n    for i in range(len(pts_x) - 1):\n        dry_adiabat_data.append(\n            {\"x\": pts_x[i], \"xend\": pts_x[i + 1], \"y\": pts_y[i], \"yend\": pts_y[i + 1], \"line_type\": \"Dry Adiabats\"}\n        )\ndry_adiabat_df = pd.DataFrame(dry_adiabat_data)\n\n# Moist adiabats (saturated adiabatic lapse rate)\nmoist_adiabat_data = []\nfor theta_e in np.arange(-30, 50, 10):\n    pts_x, pts_y = [], []\n    t_cur = theta_e + 5\n    for p in np.arange(P_MAX, P_MIN - 1, -25):\n        pts_x.append(skew_transform(t_cur, p))\n        pts_y.append(p)\n        t_cur -= (5.5 if t_cur > 0 else 6.0) * 0.05\n    for i in range(len(pts_x) - 1):\n        moist_adiabat_data.append(\n            {\"x\": pts_x[i], \"xend\": pts_x[i + 1], \"y\": pts_y[i], \"yend\": pts_y[i + 1], \"line_type\": \"Moist Adiabats\"}\n        )\nmoist_adiabat_df = pd.DataFrame(moist_adiabat_data)\n\n# Mixing ratio lines (constant saturation mixing ratio)\nmixing_ratio_data = []\nfor w in [1, 2, 4, 7, 10, 15, 20]:\n    pts_x, pts_y = [], []\n    for p in np.arange(P_MIN + 50, P_MAX + 1, 25):\n        e = w * p / (622 + w)\n        if e > 0:\n            td = (243.5 * np.log(e / 6.112)) / (17.67 - np.log(e / 6.112))\n            if T_MIN - 20 < td < T_MAX + 20:\n                pts_x.append(skew_transform(td, p))\n                pts_y.append(p)\n    if len(pts_x) > 1:\n        for i in range(len(pts_x) - 1):\n            mixing_ratio_data.append(\n                {\"x\": pts_x[i], \"xend\": pts_x[i + 1], \"y\": pts_y[i], \"yend\": pts_y[i + 1], \"line_type\": \"Mixing Ratios\"}\n            )\nmixing_ratio_df = pd.DataFrame(mixing_ratio_data)\n\nall_ref_lines = pd.concat([isotherm_df, dry_adiabat_df, moist_adiabat_df, mixing_ratio_df])\n\n# Profile data — use \"line_type\" column (matches ref lines, enables unified legend)\ntemp_skewed = skew_transform(temp_profile, pressure_levels)\ndewpoint_skewed = skew_transform(dewpoint_profile, pressure_levels)\nprofile_df = pd.DataFrame(\n    {\n        \"pressure\": np.concatenate([pressure_levels, pressure_levels]),\n        \"x\": np.concatenate([temp_skewed, dewpoint_skewed]),\n        \"line_type\": [\"Temperature\"] * len(pressure_levels) + [\"Dewpoint\"] * len(pressure_levels),\n    }\n)\n\nx_min = skew_transform(T_MIN - 10, P_MAX)\nx_max = skew_transform(T_MAX + 10, P_MIN)\n\npressure_breaks = [1000, 850, 700, 500, 400, 300, 200, 150, 100]\npressure_labels = [str(p) for p in pressure_breaks]\n\nLEGEND_ORDER = [\"Temperature\", \"Dewpoint\", \"Isotherms (10°C)\", \"Dry Adiabats\", \"Moist Adiabats\", \"Mixing Ratios\"]\nLINE_COLORS = {\n    \"Temperature\": \"#009E73\",\n    \"Dewpoint\": \"#C475FD\",\n    \"Isotherms (10°C)\": \"#4467A3\",\n    \"Dry Adiabats\": \"#BD8233\",\n    \"Moist Adiabats\": \"#AE3030\",\n    \"Mixing Ratios\": \"#2ABCCD\",\n}\nLINE_STYLES = {\n    \"Temperature\": \"solid\",\n    \"Dewpoint\": \"dashed\",\n    \"Isotherms (10°C)\": \"solid\",\n    \"Dry Adiabats\": \"dashed\",\n    \"Moist Adiabats\": \"dotted\",\n    \"Mixing Ratios\": \"dashdot\",\n}\n\nplot = (\n    ggplot(profile_df, aes(x=\"x\", y=\"pressure\"))\n    # Isobars first (background structural lines, no legend entry)\n    + geom_segment(\n        aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\"),\n        data=isobar_df,\n        color=INK_SOFT,\n        size=0.7,\n        linetype=\"solid\",\n        inherit_aes=False,\n    )\n    # Reference lines with color/linetype aesthetic for legend\n    + geom_segment(\n        aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\", color=\"line_type\", linetype=\"line_type\"),\n        data=all_ref_lines,\n        size=0.6,\n        inherit_aes=False,\n    )\n    # Temperature and dewpoint profiles (drawn on top)\n    + geom_line(\n        aes(x=\"x\", y=\"pressure\", color=\"line_type\", linetype=\"line_type\", group=\"line_type\"),\n        data=profile_df,\n        size=2.5,\n        inherit_aes=False,\n    )\n    + scale_color_manual(values=LINE_COLORS, name=\"Lines\", breaks=LEGEND_ORDER)\n    + scale_linetype_manual(values=LINE_STYLES, name=\"Lines\", breaks=LEGEND_ORDER)\n    + scale_y_continuous(trans=\"log10\", limits=(1100, 90), breaks=pressure_breaks, labels=pressure_labels)\n    + scale_x_continuous(breaks=[], labels=[], limits=(x_min - 10, x_max + 20))\n    + annotate(\n        \"text\",\n        x=skew_transform(temp_profile[np.argmin(np.abs(pressure_levels - 600))], 600) + 6,\n        y=600,\n        label=\"T\",\n        color=\"#009E73\",\n        size=12,\n        fontweight=\"bold\",\n    )\n    + annotate(\n        \"text\",\n        x=skew_transform(dewpoint_profile[np.argmin(np.abs(pressure_levels - 600))], 600) - 6,\n        y=600,\n        label=\"Td\",\n        color=\"#C475FD\",\n        size=12,\n        fontweight=\"bold\",\n    )\n    + labs(\n        x=\"Temperature (°C, skewed 45°)\",\n        y=\"Pressure (hPa)\",\n        title=\"skewt-logp-atmospheric · python · plotnine · anyplot.ai\",\n    )\n    + theme(\n        figure_size=(6, 6),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_border=element_rect(color=INK_SOFT, fill=None),\n        axis_line=element_line(color=INK_SOFT, size=0.8),\n        text=element_text(size=7),\n        axis_title=element_text(size=10, color=INK),\n        axis_title_x=element_text(margin={\"t\": 8}),\n        axis_title_y=element_text(margin={\"r\": 8}),\n        axis_text_x=element_blank(),\n        axis_ticks_major_x=element_blank(),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        plot_title=element_text(size=9, ha=\"center\", color=INK),\n        legend_position=\"right\",\n        legend_title=element_text(size=9, fontweight=\"bold\", color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_key_width=20,\n        legend_key_height=12,\n        axis_ticks_major_y=element_line(color=INK_SOFT, size=0.8),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\", verbose=False)\n"}