|
| 1 | +""" pyplots.ai |
| 2 | +skewt-logp-atmospheric: Skew-T Log-P Atmospheric Diagram |
| 3 | +Library: seaborn 0.13.2 | Python 3.13.11 |
| 4 | +Quality: 90/100 | Created: 2026-01-17 |
| 5 | +""" |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +import seaborn as sns |
| 11 | +from matplotlib.ticker import ScalarFormatter |
| 12 | + |
| 13 | + |
| 14 | +# Set seaborn style with whitegrid for subtle grid lines, using talk context for better scaling |
| 15 | +sns.set_theme(style="whitegrid", context="talk", font_scale=1.1) |
| 16 | +sns.set_palette("colorblind") |
| 17 | + |
| 18 | +# Synthetic atmospheric sounding data (typical mid-latitude summer profile) |
| 19 | +np.random.seed(42) |
| 20 | +pressure = np.array([1000, 925, 850, 700, 500, 400, 300, 250, 200, 150, 100]) |
| 21 | +temperature = np.array([25, 18, 12, 2, -20, -35, -50, -55, -58, -60, -55]) |
| 22 | +dewpoint = np.array([18, 14, 8, -5, -30, -45, -60, -65, -68, -70, -65]) |
| 23 | + |
| 24 | +# Create figure with custom transform for skew-T |
| 25 | +fig, ax = plt.subplots(figsize=(16, 9)) |
| 26 | + |
| 27 | +# Set up the axes with log scale for pressure (inverted) |
| 28 | +ax.set_yscale("log") |
| 29 | +ax.set_ylim(1050, 100) # Inverted: surface at bottom |
| 30 | +ax.set_xlim(-50, 50) |
| 31 | + |
| 32 | +# Custom skew transform (45 degrees) |
| 33 | +skew_angle = 45 |
| 34 | +skew_slope = np.tan(np.radians(skew_angle)) |
| 35 | + |
| 36 | +# Draw isotherms (temperature lines, skewed) |
| 37 | +isotherm_temps = np.arange(-80, 60, 10) |
| 38 | +p_range = np.logspace(np.log10(100), np.log10(1050), 100) |
| 39 | +for t in isotherm_temps: |
| 40 | + x_iso = t + skew_slope * (np.log(1000 / p_range)) |
| 41 | + ax.plot(x_iso, p_range, color="#cccccc", linewidth=0.8, alpha=0.7, zorder=1) |
| 42 | + |
| 43 | +# Draw isobars (horizontal pressure lines) |
| 44 | +isobar_levels = [1000, 925, 850, 700, 500, 400, 300, 250, 200, 150, 100] |
| 45 | +for p in isobar_levels: |
| 46 | + ax.axhline(y=p, color="#dddddd", linewidth=0.8, alpha=0.7, zorder=1) |
| 47 | + |
| 48 | +# Draw dry adiabats (potential temperature lines) |
| 49 | +theta_values = np.arange(250, 450, 20) # Potential temperatures in K |
| 50 | +for theta in theta_values: |
| 51 | + p_adiabat = np.logspace(np.log10(100), np.log10(1050), 100) |
| 52 | + # T = theta * (p/1000)^(R/cp), where R/cp ≈ 0.286 |
| 53 | + t_adiabat = theta * (p_adiabat / 1000) ** 0.286 - 273.15 |
| 54 | + x_adiabat = t_adiabat + skew_slope * (np.log(1000 / p_adiabat)) |
| 55 | + ax.plot(x_adiabat, p_adiabat, color="#8B4513", linewidth=0.6, alpha=0.5, zorder=1) |
| 56 | + |
| 57 | +# Draw moist adiabats (saturated adiabats - simplified) |
| 58 | +theta_e_values = np.arange(270, 370, 20) # Equivalent potential temperatures |
| 59 | +for theta_e in theta_e_values: |
| 60 | + p_moist = np.logspace(np.log10(100), np.log10(1050), 100) |
| 61 | + # Simplified moist adiabat approximation |
| 62 | + t_moist = (theta_e - 30) * (p_moist / 1000) ** 0.3 - 273.15 |
| 63 | + x_moist = t_moist + skew_slope * (np.log(1000 / p_moist)) |
| 64 | + ax.plot(x_moist, p_moist, color="#228B22", linewidth=0.6, alpha=0.4, linestyle="--", zorder=1) |
| 65 | + |
| 66 | +# Draw mixing ratio lines (constant water vapor mixing ratio) |
| 67 | +mixing_ratios = [1, 2, 4, 7, 10, 15, 20] # g/kg |
| 68 | +for w in mixing_ratios: |
| 69 | + p_mix = np.logspace(np.log10(400), np.log10(1050), 50) |
| 70 | + # Simplified mixing ratio to dewpoint: Td ≈ 35 * log10(w) - 15 + adjustment for pressure |
| 71 | + t_mix = 35 * np.log10(w) - 20 + 5 * np.log10(p_mix / 1000) |
| 72 | + x_mix = t_mix + skew_slope * (np.log(1000 / p_mix)) |
| 73 | + ax.plot(x_mix, p_mix, color="#4169E1", linewidth=0.5, alpha=0.4, linestyle=":", zorder=1) |
| 74 | + |
| 75 | +# Apply skew transform to data for seaborn plotting |
| 76 | +x_temp = temperature + skew_slope * np.log(1000 / pressure) |
| 77 | +x_dew = dewpoint + skew_slope * np.log(1000 / pressure) |
| 78 | + |
| 79 | +# Create DataFrame for seaborn plotting |
| 80 | +df = pd.DataFrame( |
| 81 | + { |
| 82 | + "x": np.concatenate([x_temp, x_dew]), |
| 83 | + "pressure": np.concatenate([pressure, pressure]), |
| 84 | + "profile": ["Temperature"] * len(pressure) + ["Dewpoint"] * len(pressure), |
| 85 | + } |
| 86 | +) |
| 87 | + |
| 88 | +# Plot profiles using seaborn lineplot |
| 89 | +sns.lineplot( |
| 90 | + data=df, |
| 91 | + x="x", |
| 92 | + y="pressure", |
| 93 | + hue="profile", |
| 94 | + style="profile", |
| 95 | + markers={"Temperature": "o", "Dewpoint": "s"}, |
| 96 | + dashes={"Temperature": "", "Dewpoint": (5, 2)}, |
| 97 | + palette={"Temperature": "#E74C3C", "Dewpoint": "#306998"}, |
| 98 | + linewidth=4, |
| 99 | + markersize=10, |
| 100 | + ax=ax, |
| 101 | + zorder=5, |
| 102 | + legend=True, |
| 103 | +) |
| 104 | + |
| 105 | +# Configure pressure axis - remove overlapping ticks (925 removed to avoid overlap with 1000) |
| 106 | +ax.yaxis.set_major_formatter(ScalarFormatter()) |
| 107 | +display_ticks = [1000, 850, 700, 500, 400, 300, 250, 200, 150, 100] |
| 108 | +ax.set_yticks(display_ticks) |
| 109 | +ax.set_yticklabels([str(p) for p in display_ticks]) |
| 110 | + |
| 111 | +# Labels and title with proper middle dot separator |
| 112 | +ax.set_xlabel("Temperature (°C)", fontsize=20) |
| 113 | +ax.set_ylabel("Pressure (hPa)", fontsize=20) |
| 114 | +ax.set_title("skewt-logp-atmospheric · seaborn · pyplots.ai", fontsize=24) |
| 115 | +ax.tick_params(axis="both", labelsize=16) |
| 116 | + |
| 117 | +# Use seaborn's despine to clean up the appearance (keep left and bottom spines only) |
| 118 | +sns.despine(ax=ax, top=True, right=True) |
| 119 | + |
| 120 | +# Configure legend from seaborn lineplot |
| 121 | +legend = ax.get_legend() |
| 122 | +legend.set_title("") |
| 123 | +for text in legend.get_texts(): |
| 124 | + text.set_fontsize(16) |
| 125 | +legend.get_frame().set_alpha(0.9) |
| 126 | + |
| 127 | +# Add reference line labels with better positioning and improved visibility |
| 128 | +ax.text(45, 600, "Isotherms", fontsize=14, color="#555555", rotation=45, ha="center", fontweight="bold") |
| 129 | +ax.text(-25, 108, "Dry Adiabats", fontsize=13, color="#6B3510", ha="center", fontweight="bold") |
| 130 | +ax.text(0, 108, "Moist Adiabats", fontsize=13, color="#1A6B1A", ha="center", fontweight="bold") |
| 131 | +ax.text(22, 108, "Mixing Ratio", fontsize=13, color="#2E4A8B", ha="center", fontweight="bold") |
| 132 | + |
| 133 | +# Add subtle background and configure grid styling |
| 134 | +ax.set_facecolor("#fafafa") |
| 135 | +ax.grid(True, which="major", alpha=0.3, linestyle="-", linewidth=0.5, color="#888888") |
| 136 | + |
| 137 | +plt.tight_layout() |
| 138 | +plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments