{"spec_id":"skewt-logp-atmospheric","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nskewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-21\n\"\"\"\n\nimport os\nimport sys\n\n\nsys.path = sys.path[1:]  # prevent self-import when script is named altair.py\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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# Data\nnp.random.seed(42)\n\npressure = np.array([1000, 925, 850, 700, 500, 400, 300, 250, 200, 150, 100])\ntemperature = np.array([25, 20, 15, 5, -15, -28, -45, -52, -55, -55, -55])\ndewpoint = np.array([18, 15, 10, -5, -25, -38, -55, -62, -70, -75, -80])\n\nskew_factor = 40\nlog_p_base = np.log10(pressure)\nskew_offset = (np.log10(1000) - log_p_base) * skew_factor\ntemp_skewed = temperature + skew_offset\ndew_skewed = dewpoint + skew_offset\n\n# X-axis domain — tightened to reduce whitespace on the right\nx_domain = [-48, 60]\nX_MARGIN = 5  # pre-filter margin to exclude vl-convert layout inflation from out-of-range values\n\n# Unified color/dash scheme (single legend covers all 6 element types)\n# Data series first so Temperature gets Okabe-Ito position 1 (#009E73 brand green)\nALL_LABELS = [\"Temperature\", \"Dewpoint\", \"Isotherm\", \"Dry Adiabat\", \"Moist Adiabat\", \"Mixing Ratio\"]\nALL_COLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\nALL_DASHES = [[1, 0], [8, 4], [4, 4], [1, 0], [6, 3], [2, 2]]\n\nfull_color_scale = alt.Scale(domain=ALL_LABELS, range=ALL_COLORS)\nfull_dash_scale = alt.Scale(type=\"ordinal\", domain=ALL_LABELS, range=ALL_DASHES)\n\n# Reference lines — pre-filtered to x_domain ± margin to prevent layout inflation\nref_records = []\n\nfor t in np.arange(-80, 50, 10):\n    for p_val in np.linspace(100, 1000, 50):\n        lp = np.log10(p_val)\n        t_sk = t + (np.log10(1000) - lp) * skew_factor\n        if x_domain[0] - X_MARGIN <= t_sk <= x_domain[1] + X_MARGIN:\n            ref_records.append({\"pressure\": p_val, \"temp_skewed\": t_sk, \"label\": \"Isotherm\", \"line_id\": f\"iso_{t}\"})\n\nfor theta in np.arange(250, 450, 20):\n    for p_val in np.linspace(100, 1000, 50):\n        t_c = theta * (p_val / 1000) ** 0.286 - 273.15\n        lp = np.log10(p_val)\n        t_sk = t_c + (np.log10(1000) - lp) * skew_factor\n        if x_domain[0] - X_MARGIN <= t_sk <= x_domain[1] + X_MARGIN:\n            ref_records.append(\n                {\"pressure\": p_val, \"temp_skewed\": t_sk, \"label\": \"Dry Adiabat\", \"line_id\": f\"dry_{theta}\"}\n            )\n\nfor theta_e in np.arange(280, 380, 20):\n    for p_val in np.linspace(100, 1000, 50):\n        t_c = theta_e * (p_val / 1000) ** 0.23 - 273.15\n        lp = np.log10(p_val)\n        t_sk = t_c + (np.log10(1000) - lp) * skew_factor\n        if x_domain[0] - X_MARGIN <= t_sk <= x_domain[1] + X_MARGIN:\n            ref_records.append(\n                {\"pressure\": p_val, \"temp_skewed\": t_sk, \"label\": \"Moist Adiabat\", \"line_id\": f\"moist_{theta_e}\"}\n            )\n\nfor w in [1, 2, 4, 7, 10, 15, 20]:\n    for p_val in np.linspace(400, 1000, 30):\n        e = (w * p_val) / (622 + w)\n        if e > 0.1:\n            ln_e = np.log(e / 6.112)\n            denom = 17.67 - ln_e\n            if denom != 0:\n                td = (243.5 * ln_e) / denom\n                if -100 < td < 50:\n                    lp = np.log10(p_val)\n                    t_sk = td + (np.log10(1000) - lp) * skew_factor\n                    if x_domain[0] - X_MARGIN <= t_sk <= x_domain[1] + X_MARGIN:\n                        ref_records.append(\n                            {\"pressure\": p_val, \"temp_skewed\": t_sk, \"label\": \"Mixing Ratio\", \"line_id\": f\"mix_{w}\"}\n                        )\n\nref_df = pd.DataFrame(ref_records)\n\n# Sounding profile data — same 'label' field for unified color/dash scale\nprof_records = []\nfor i in range(len(pressure)):\n    prof_records.append({\"pressure\": pressure[i], \"temp_skewed\": temp_skewed[i], \"label\": \"Temperature\"})\n    prof_records.append({\"pressure\": pressure[i], \"temp_skewed\": dew_skewed[i], \"label\": \"Dewpoint\"})\nprof_df = pd.DataFrame(prof_records)\n\ny_scale = alt.Scale(type=\"log\", domain=[1000, 100])\n\n# Reference lines — thin, semi-transparent background grid\nref_lines = (\n    alt.Chart(ref_df)\n    .mark_line(opacity=0.40, strokeWidth=1.0)\n    .encode(\n        x=alt.X(\"temp_skewed:Q\", scale=alt.Scale(domain=x_domain), title=\"Temperature (°C, skewed)\"),\n        y=alt.Y(\"pressure:Q\", scale=y_scale, title=\"Pressure (hPa)\"),\n        detail=\"line_id:N\",\n        color=alt.Color(\"label:N\", scale=full_color_scale, legend=None),\n        strokeDash=alt.StrokeDash(\"label:N\", scale=full_dash_scale, legend=None),\n    )\n)\n\n# Sounding profiles — thick lines; legend here shows all 6 labels via shared scale\nprof_lines = (\n    alt.Chart(prof_df)\n    .mark_line(strokeWidth=3.5)\n    .encode(\n        x=alt.X(\"temp_skewed:Q\", scale=alt.Scale(domain=x_domain)),\n        y=alt.Y(\"pressure:Q\", scale=y_scale),\n        color=alt.Color(\n            \"label:N\",\n            scale=full_color_scale,\n            legend=alt.Legend(\n                title=\"\", labelFontSize=10, titleFontSize=10, symbolSize=200, symbolStrokeWidth=2, offset=6\n            ),\n        ),\n        strokeDash=alt.StrokeDash(\"label:N\", scale=full_dash_scale, legend=None),\n        tooltip=[\"label:N\", \"pressure:Q\", alt.Tooltip(\"temp_skewed:Q\", title=\"T skewed (°C)\", format=\".1f\")],\n    )\n)\n\n# Profile points at each sounding level\nprof_points = (\n    alt.Chart(prof_df)\n    .mark_circle(size=80, filled=True)\n    .encode(\n        x=alt.X(\"temp_skewed:Q\", scale=alt.Scale(domain=x_domain)),\n        y=alt.Y(\"pressure:Q\", scale=y_scale),\n        color=alt.Color(\"label:N\", scale=full_color_scale, legend=None),\n    )\n)\n\ntitle_text = \"skewt-logp-atmospheric · python · altair · anyplot.ai\"\n\n# Inner view 540×300 — filtered data keeps total PNG within 3200×1800 after padding\nchart = (\n    alt.layer(ref_lines, prof_lines, prof_points)\n    .resolve_scale(color=\"shared\", strokeDash=\"shared\")\n    .properties(\n        width=540, height=300, background=PAGE_BG, title=alt.Title(title_text, fontSize=16, color=INK, anchor=\"middle\")\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD-only to canonical 3200×1800 — do NOT crop (would clip title/axis labels)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save HTML\nchart.save(f\"plot-{THEME}.html\")\n"}