{"spec_id":"skewt-logp-atmospheric","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nskewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 82/100 | Updated: 2026-05-21\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_y_log10,\n    scale_y_reverse,\n    theme,\n)\n\n\nLetsPlot.setup_html()\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# Okabe-Ito palette — positions 1-6 in order\nC_TEMP = \"#009E73\"  # position 1: temperature profile (primary series)\nC_DEWPT = \"#C475FD\"  # position 2: dewpoint profile\nC_DRY = \"#AE3030\"  # position 5: dry adiabats (background reference)\nC_MOIST = \"#BD8233\"  # position 4: moist adiabats (background reference)\nC_MIX = \"#2ABCCD\"  # position 6: mixing ratio lines (background reference)\n\n# Atmospheric sounding data (synthetic radiosonde profile)\nnp.random.seed(42)\n\npressure = np.array([1000, 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350, 300, 250, 200, 150, 100])\ntemperature = np.array([25, 22, 18, 14, 10, 6, 2, -3, -8, -14, -21, -29, -38, -47, -55, -58, -56, -55, -56])\ndewpoint = np.array([18, 16, 12, 8, 4, 0, -5, -12, -20, -28, -35, -42, -50, -58, -65, -70, -72, -75, -78])\n\n# Skew-T coordinate transformation: x_plot = T + skew_factor * log10(p0/p)\np0 = 1000\nskew_factor = 40\n\nlog_pressure = np.log10(p0 / pressure)\ntemp_skewed = temperature + skew_factor * log_pressure\ndewpoint_skewed = dewpoint + skew_factor * log_pressure\n\n# Include original values for interactive tooltips\ndf_temp = pd.DataFrame({\"pressure\": pressure, \"temp\": temperature, \"value\": temp_skewed})\ndf_dewpoint = pd.DataFrame({\"pressure\": pressure, \"dewpt\": dewpoint, \"value\": dewpoint_skewed})\n\n# Generate isotherms (skewed 45-degree temperature reference lines)\nisotherm_temps = np.arange(-80, 50, 10)\np_range = np.array([1000, 100])\nisotherm_data = []\nfor t in isotherm_temps:\n    log_p_vals = np.log10(p0 / p_range)\n    t_skewed = t + skew_factor * log_p_vals\n    isotherm_data.append({\"x_start\": t_skewed[0], \"x_end\": t_skewed[1], \"y_start\": p_range[0], \"y_end\": p_range[1]})\ndf_isotherms = pd.DataFrame(isotherm_data)\n\n# Generate dry adiabats (constant potential temperature lines)\ndry_adiabat_thetas = np.arange(250, 450, 20)\np_levels = np.linspace(1000, 100, 50)\ndry_adiabat_data = []\nfor theta in dry_adiabat_thetas:\n    temps = (theta * (p_levels / p0) ** 0.286) - 273.15\n    log_p_vals = np.log10(p0 / p_levels)\n    temps_skewed = temps + skew_factor * log_p_vals\n    for i in range(len(p_levels)):\n        dry_adiabat_data.append({\"pressure\": p_levels[i], \"temp_skewed\": temps_skewed[i], \"theta\": theta})\ndf_dry_adiabats = pd.DataFrame(dry_adiabat_data)\n\n# Generate moist adiabats (equivalent potential temperature lines)\nmoist_adiabat_theta_e = np.arange(280, 360, 10)\nmoist_adiabat_data = []\nfor theta_e in moist_adiabat_theta_e:\n    t_surface = theta_e - 273.15\n    for p in p_levels:\n        height_factor = np.log(p0 / p) * 2.5\n        t_moist = t_surface - 6.5 * height_factor * 0.8\n        log_p_val = np.log10(p0 / p)\n        t_skewed = t_moist + skew_factor * log_p_val\n        moist_adiabat_data.append({\"pressure\": p, \"temp_skewed\": t_skewed, \"theta_e\": theta_e})\ndf_moist_adiabats = pd.DataFrame(moist_adiabat_data)\n\n# Generate mixing ratio lines (constant water vapor mixing ratio)\nmixing_ratios = [1, 2, 4, 7, 10, 15, 20]\nmixing_data = []\nfor ws in mixing_ratios:\n    for p in p_levels[::5]:\n        t = 35 * np.log10(ws * p / 622) - 20\n        log_p_val = np.log10(p0 / p)\n        t_skewed = t + skew_factor * log_p_val\n        mixing_data.append({\"pressure\": p, \"temp_skewed\": t_skewed, \"ws\": ws})\ndf_mixing = pd.DataFrame(mixing_data)\n\n# Theme-adaptive chrome\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_border=element_blank(),\n    panel_grid_major=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    axis_ticks=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=16, face=\"bold\"),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK),\n    plot_margin=[8, 8, 8, 8],\n)\n\nplot = (\n    ggplot()\n    # Isotherms (gray diagonal reference lines)\n    + geom_segment(\n        aes(x=\"x_start\", xend=\"x_end\", y=\"y_start\", yend=\"y_end\"),\n        data=df_isotherms,\n        color=INK_SOFT,\n        size=0.4,\n        alpha=0.5,\n        tooltips=\"none\",\n    )\n    # Dry adiabats — Okabe-Ito orange dashed\n    + geom_path(\n        aes(x=\"temp_skewed\", y=\"pressure\", group=\"theta\"),\n        data=df_dry_adiabats,\n        color=C_DRY,\n        size=0.7,\n        alpha=0.7,\n        linetype=\"dashed\",\n        tooltips=\"none\",\n    )\n    # Moist adiabats — Okabe-Ito reddish purple dotdash\n    + geom_path(\n        aes(x=\"temp_skewed\", y=\"pressure\", group=\"theta_e\"),\n        data=df_moist_adiabats,\n        color=C_MOIST,\n        size=0.7,\n        alpha=0.7,\n        linetype=\"dotdash\",\n        tooltips=\"none\",\n    )\n    # Mixing ratio lines — Okabe-Ito sky blue dotted\n    + geom_path(\n        aes(x=\"temp_skewed\", y=\"pressure\", group=\"ws\"),\n        data=df_mixing,\n        color=C_MIX,\n        size=0.8,\n        alpha=0.8,\n        linetype=\"dotted\",\n        tooltips=\"none\",\n    )\n    # Temperature profile — Okabe-Ito green solid (primary series, position 1)\n    + geom_path(\n        aes(x=\"value\", y=\"pressure\"),\n        data=df_temp,\n        color=C_TEMP,\n        size=2.0,\n        tooltips=layer_tooltips().line(\"Temperature: @temp°C\").line(\"Pressure: @pressure hPa\"),\n    )\n    # Dewpoint profile — Okabe-Ito vermillion dashed (secondary series, position 2)\n    + geom_path(\n        aes(x=\"value\", y=\"pressure\"),\n        data=df_dewpoint,\n        color=C_DEWPT,\n        size=2.0,\n        linetype=\"dashed\",\n        tooltips=layer_tooltips().line(\"Dewpoint: @dewpt°C\").line(\"Pressure: @pressure hPa\"),\n    )\n    # Data points on profiles for clarity\n    + geom_point(\n        aes(x=\"value\", y=\"pressure\"),\n        data=df_temp,\n        color=C_TEMP,\n        size=3.0,\n        tooltips=layer_tooltips().line(\"Temperature: @temp°C\").line(\"Pressure: @pressure hPa\"),\n    )\n    + geom_point(\n        aes(x=\"value\", y=\"pressure\"),\n        data=df_dewpoint,\n        color=C_DEWPT,\n        size=3.0,\n        tooltips=layer_tooltips().line(\"Dewpoint: @dewpt°C\").line(\"Pressure: @pressure hPa\"),\n    )\n    # Logarithmic inverted pressure axis\n    + scale_y_log10()\n    + scale_y_reverse(limits=[1000, 100])\n    + labs(x=\"Temperature (°C)\", y=\"Pressure (hPa)\", title=\"skewt-logp-atmospheric · python · letsplot · anyplot.ai\")\n    + ggsize(800, 450)\n    + anyplot_theme\n)\n\n# Manual legend (positioned upper-right in skewed coordinate space)\nlegend_x_start = 95\nlegend_x_end = 112\nlegend_text_x = 114\nlegend_entries = [\n    {\"label\": \"Temperature\", \"color\": C_TEMP, \"linetype\": \"solid\", \"size\": 2.0, \"y\": 110},\n    {\"label\": \"Dewpoint\", \"color\": C_DEWPT, \"linetype\": \"dashed\", \"size\": 2.0, \"y\": 145},\n    {\"label\": \"Dry Adiabat\", \"color\": C_DRY, \"linetype\": \"dashed\", \"size\": 1.2, \"y\": 190},\n    {\"label\": \"Moist Adiabat\", \"color\": C_MOIST, \"linetype\": \"dotdash\", \"size\": 1.2, \"y\": 250},\n    {\"label\": \"Mixing Ratio\", \"color\": C_MIX, \"linetype\": \"dotted\", \"size\": 1.2, \"y\": 330},\n]\n\nfor entry in legend_entries:\n    plot = plot + geom_segment(\n        aes(x=\"x_start\", xend=\"x_end\", y=\"y\", yend=\"y\"),\n        data=pd.DataFrame([{\"x_start\": legend_x_start, \"x_end\": legend_x_end, \"y\": entry[\"y\"]}]),\n        color=entry[\"color\"],\n        size=entry[\"size\"],\n        linetype=entry[\"linetype\"],\n        tooltips=\"none\",\n    )\n    plot = plot + geom_text(x=legend_text_x, y=entry[\"y\"], label=entry[\"label\"], color=INK, size=10, hjust=0)\n\n# Save PNG and HTML for both themes\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}