Chris Parmer — home

Psychrometric Chart

HVAC — humidity ratio vs. dry-bulb temperature

Example from the compendium of canonical charts

HVAC — Psychrometric Chart (humidity ratio vs. dry-bulb temperature)

Python Code

"""HVAC — Psychrometric Chart (humidity ratio vs. dry-bulb temperature)."""


import numpy as np
import plotly.graph_objects as go


P = 101325.0  # atmospheric pressure (Pa)

# Relative humidity levels to plot
RH_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

# Cycle through palette
COLORS = [
    "#52B3D0",  # teal  — 10%
    "#55B685",  # green — 20%
    "#845EEE",  # violet— 30%
    "#52B3D0",  # teal  — 40%
    "#DA5597",  # pink  — 50%
    "#52B3D0",
    "#55B685",
    "#845EEE",
    "#52B3D0",
    "#845EEE",  # 100% saturation — same as violet but bolder
]


def saturation_pressure(T_C):
    """Arden-Buck equation for saturation vapor pressure (Pa)."""
    return 611.21 * np.exp((18.678 - T_C / 234.5) * (T_C / (257.14 + T_C)))


def humidity_ratio(phi_frac, T_C):
    """Humidity ratio W (kg_w/kg_da) at relative humidity phi (0-1)."""
    pv = phi_frac * saturation_pressure(T_C)
    return 0.621945 * pv / (P - pv)


def generate():
    T = np.linspace(0, 50, 300)

    fig = go.Figure()

    # Constant-RH lines
    for i, rh in enumerate(RH_LEVELS):
        W = humidity_ratio(rh / 100.0, T)
        is_sat = rh == 100
        color = COLORS[i]
        fig.add_trace(go.Scatter(
            x=T,
            y=W * 1000,   # convert to g/kg for readability
            mode="lines",
            line=dict(
                color=color,
                width=2.5 if is_sat else 1.5,
                dash="solid" if is_sat else "solid",
            ),
            name=f"{rh}% RH",
            hovertemplate=f"{rh}% RH<br>T=%{{x:.1f}} °C<br>W=%{{y:.2f}} g/kg<extra></extra>",
        ))
        # Label at T=48°C
        label_T = 47.0
        label_W = humidity_ratio(rh / 100.0, label_T) * 1000
        fig.add_annotation(
            x=label_T,
            y=label_W,
            text=f"{rh}%",
            showarrow=False,
            font=dict(size=9, color=color),
            xanchor="left",
            yanchor="middle",
            xshift=4,
        )

    # Shade comfort zone: 20-26°C, 40-60% RH
    T_comfort = np.concatenate([
        np.linspace(20, 26, 50),   # lower RH=40% edge
        np.linspace(26, 20, 50),   # upper RH=60% edge
    ])
    W_comfort = np.concatenate([
        humidity_ratio(0.40, np.linspace(20, 26, 50)) * 1000,
        humidity_ratio(0.60, np.linspace(26, 20, 50)) * 1000,
    ])
    fig.add_trace(go.Scatter(
        x=T_comfort,
        y=W_comfort,
        fill="toself",
        fillcolor="rgba(85, 182, 133, 0.15)",
        line=dict(color="rgba(85, 182, 133, 0.5)", width=1),
        name="Comfort zone",
        hoverinfo="skip",
    ))
    fig.add_annotation(
        x=23, y=humidity_ratio(0.50, 23) * 1000,
        text="Comfort<br>zone",
        showarrow=False,
        font=dict(size=11, color=GREEN),
        xanchor="center",
    )

    fig.update_layout(
        xaxis=dict(title="Dry-Bulb Temperature (°C)", range=[0, 52]),
        yaxis=dict(title="Humidity Ratio W (g/kg dry air)", rangemode="tozero"),
        legend=dict(
            x=0.01, y=0.99,
            xanchor="left", yanchor="top",
            font=dict(size=10),
            tracegroupgap=0,
        ),
        margin=dict(t=50, b=60, l=70, r=80),
    )

    return fig


if __name__ == "__main__":
    generate()

Made with Plotly