{"spec_id":"skewt-logp-atmospheric","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nskewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-21\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, CrosshairTool, HoverTool, Label, WheelZoomTool\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data\nnp.random.seed(42)\n\npressure = 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        725,\n        700,\n        650,\n        600,\n        550,\n        500,\n        450,\n        400,\n        350,\n        300,\n        250,\n        200,\n        150,\n        100,\n    ]\n)\n\ntemperature = (\n    np.array([25, 23, 21, 19, 17, 15, 13, 11, 9, 7, 5, 3, 1, -3, -8, -14, -20, -28, -36, -44, -52, -58, -62, -66, -68])\n    + np.random.randn(25) * 0.5\n)\n\ndewpoint = (\n    np.array(\n        [20, 18, 16, 14, 12, 10, 7, 4, 1, -3, -7, -12, -17, -24, -32, -40, -45, -50, -54, -58, -62, -66, -70, -74, -78]\n    )\n    + np.random.randn(25) * 0.3\n)\ndewpoint = np.minimum(dewpoint, temperature - 0.5)\n\nSKEW_FACTOR = 45\np_range = np.logspace(np.log10(1000), np.log10(100), 50)\n\n# Skew-transform sounding coordinates\nlog_p_sounding = np.log10(pressure / 1000.0)\nx_temp = temperature - SKEW_FACTOR * log_p_sounding\nx_dewpoint = dewpoint - SKEW_FACTOR * log_p_sounding\n\n# Plot\nfig = figure(\n    width=3200,\n    height=1800,\n    title=\"skewt-logp-atmospheric · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Temperature (°C) — Skewed Coordinates\",\n    y_axis_label=\"Pressure (hPa)\",\n    y_axis_type=\"log\",\n    y_range=(1050, 95),\n    x_range=(-50, 50),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=60,\n)\n\n# Theme-adaptive chrome\nfig.background_fill_color = PAGE_BG\nfig.border_fill_color = PAGE_BG\nfig.outline_line_color = None\n\nfig.title.text_color = INK\nfig.title.text_font_size = \"50pt\"\nfig.xaxis.axis_label_text_color = INK\nfig.yaxis.axis_label_text_color = INK\nfig.xaxis.axis_label_text_font_size = \"42pt\"\nfig.yaxis.axis_label_text_font_size = \"42pt\"\nfig.xaxis.major_label_text_color = INK_SOFT\nfig.yaxis.major_label_text_color = INK_SOFT\nfig.xaxis.major_label_text_font_size = \"34pt\"\nfig.yaxis.major_label_text_font_size = \"34pt\"\nfig.xaxis.axis_line_color = INK_SOFT\nfig.yaxis.axis_line_color = INK_SOFT\nfig.xaxis.major_tick_line_color = INK_SOFT\nfig.yaxis.major_tick_line_color = INK_SOFT\nfig.xgrid.grid_line_color = INK\nfig.ygrid.grid_line_color = INK\nfig.xgrid.grid_line_alpha = 0.10\nfig.ygrid.grid_line_alpha = 0.10\n\n# Isotherms (45° skewed background reference lines)\nisotherm_temps = np.arange(-80, 50, 10)\nfor t in isotherm_temps:\n    log_p = np.log10(p_range / 1000.0)\n    x_iso = np.full_like(p_range, t) - SKEW_FACTOR * log_p\n    src = ColumnDataSource(data={\"x\": x_iso, \"y\": p_range})\n    fig.line(x=\"x\", y=\"y\", source=src, line_color=INK_MUTED, line_width=1.2, line_alpha=0.20)\n\n# Dry adiabats (lines of constant potential temperature)\np0 = 1000.0\nkappa = 0.286\nfor theta in np.arange(250, 400, 20):\n    t_adiabat = theta * (p_range / p0) ** kappa - 273.15\n    log_p = np.log10(p_range / 1000.0)\n    x_adiabat = t_adiabat - SKEW_FACTOR * log_p\n    mask = (t_adiabat > -80) & (t_adiabat < 60)\n    if np.any(mask):\n        src = ColumnDataSource(data={\"x\": x_adiabat[mask], \"y\": p_range[mask]})\n        fig.line(x=\"x\", y=\"y\", source=src, line_color=IMPRINT[4], line_width=1.5, line_alpha=0.50, line_dash=\"dashed\")\n\n# Moist adiabats (pseudoadiabats — simplified iterative approximation)\nfor t_start in np.arange(-10, 35, 10):\n    t_moist = [t_start]\n    p_prev, t_prev = 1000.0, t_start\n    for p_level in p_range[1:]:\n        dt = -0.006 * (p_prev - p_level) * 10 * 0.5\n        t_new = t_prev + dt\n        t_moist.append(t_new)\n        p_prev, t_prev = p_level, t_new\n    t_moist = np.array(t_moist)\n    log_p = np.log10(p_range / 1000.0)\n    x_moist = t_moist - SKEW_FACTOR * log_p\n    mask = (t_moist > -80) & (t_moist < 60)\n    if np.any(mask):\n        src = ColumnDataSource(data={\"x\": x_moist[mask], \"y\": p_range[mask]})\n        fig.line(\n            x=\"x\", y=\"y\", source=src, line_color=IMPRINT[5], line_width=1.5, line_alpha=0.50, line_dash=\"dotdash\"\n        )\n\n# Mixing ratio lines\nfor mr in [1, 2, 4, 8, 12, 16, 20]:\n    td_mr = -40 + 10 * np.log10(mr + 1)\n    log_p = np.log10(p_range / 1000.0)\n    x_mr = np.full_like(p_range, td_mr) - SKEW_FACTOR * log_p\n    src = ColumnDataSource(data={\"x\": x_mr, \"y\": p_range})\n    fig.line(x=\"x\", y=\"y\", source=src, line_color=IMPRINT[3], line_width=1.2, line_alpha=0.40, line_dash=\"dotted\")\n\n# Highlighted 0°C isotherm\nlog_p_ref = np.log10(p_range / 1000.0)\nx_freeze = -SKEW_FACTOR * log_p_ref\nsrc_freeze = ColumnDataSource(data={\"x\": x_freeze, \"y\": p_range})\nfig.line(\n    x=\"x\", y=\"y\", source=src_freeze, line_color=IMPRINT[2], line_width=3, line_alpha=0.85, legend_label=\"0°C Isotherm\"\n)\n\n# Temperature profile\nsrc_temp = ColumnDataSource(data={\"x\": x_temp, \"y\": pressure, \"temp_c\": np.round(temperature, 1)})\nfig.line(x=\"x\", y=\"y\", source=src_temp, line_color=IMPRINT[0], line_width=5, legend_label=\"Temperature\")\ntemp_scatter = fig.scatter(x=\"x\", y=\"y\", source=src_temp, size=14, color=IMPRINT[0], alpha=0.85)\n\n# Dewpoint profile\nsrc_dew = ColumnDataSource(data={\"x\": x_dewpoint, \"y\": pressure, \"dewpt_c\": np.round(dewpoint, 1)})\nfig.line(\n    x=\"x\", y=\"y\", source=src_dew, line_color=IMPRINT[1], line_width=5, line_dash=\"dashed\", legend_label=\"Dewpoint\"\n)\ndew_scatter = fig.scatter(x=\"x\", y=\"y\", source=src_dew, size=14, color=IMPRINT[1], alpha=0.85, marker=\"diamond\")\n\n# HoverTools for interactive temperature/pressure display\nfig.add_tools(HoverTool(renderers=[temp_scatter], tooltips=[(\"Pressure\", \"@y{0} hPa\"), (\"Temperature\", \"@temp_c °C\")]))\nfig.add_tools(HoverTool(renderers=[dew_scatter], tooltips=[(\"Pressure\", \"@y{0} hPa\"), (\"Dewpoint\", \"@dewpt_c °C\")]))\nfig.add_tools(CrosshairTool(), WheelZoomTool())\n\n# Reference line labels\nfig.add_layout(\n    Label(x=-34, y=340, text=\"Dry Adiabats\", text_font_size=\"33pt\", text_color=IMPRINT[4], text_alpha=0.90)\n)\nfig.add_layout(\n    Label(x=24, y=195, text=\"Moist Adiabats\", text_font_size=\"33pt\", text_color=IMPRINT[5], text_alpha=0.90)\n)\nfig.add_layout(\n    Label(x=-48, y=590, text=\"Mixing Ratio\", text_font_size=\"33pt\", text_color=IMPRINT[3], text_alpha=0.90)\n)\n\n# Legend\nfig.legend.location = \"top_left\"\nfig.legend.label_text_font_size = \"34pt\"\nfig.legend.label_text_color = INK_SOFT\nfig.legend.background_fill_color = ELEVATED_BG\nfig.legend.border_line_color = INK_SOFT\nfig.legend.glyph_height = 50\nfig.legend.glyph_width = 50\nfig.legend.spacing = 12\nfig.legend.padding = 18\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(fig)\n\n# Screenshot with headless Chrome (Selenium 4 / Selenium Manager)\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\n# Override emulated viewport to exactly W×H regardless of browser chrome overhead\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}